github.com/armen/terraform@v0.5.2-0.20150529052519-caa8117a08f1/builtin/provisioners/chef/resource_provisioner.go (about) 1 package chef 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "log" 10 "os" 11 "path" 12 "regexp" 13 "strings" 14 "text/template" 15 "time" 16 17 "github.com/hashicorp/terraform/communicator" 18 "github.com/hashicorp/terraform/communicator/remote" 19 "github.com/hashicorp/terraform/terraform" 20 "github.com/mitchellh/go-linereader" 21 "github.com/mitchellh/mapstructure" 22 ) 23 24 const ( 25 clienrb = "client.rb" 26 defaultEnv = "_default" 27 firstBoot = "first-boot.json" 28 logfileDir = "logfiles" 29 linuxConfDir = "/etc/chef" 30 validationKey = "validation.pem" 31 windowsConfDir = "C:/chef" 32 ) 33 34 const clientConf = ` 35 log_location STDOUT 36 chef_server_url "{{ .ServerURL }}" 37 validation_client_name "{{ .ValidationClientName }}" 38 node_name "{{ .NodeName }}" 39 40 {{ if .HTTPProxy }} 41 http_proxy "{{ .HTTPProxy }}" 42 ENV['http_proxy'] = "{{ .HTTPProxy }}" 43 ENV['HTTP_PROXY'] = "{{ .HTTPProxy }}" 44 {{ end }} 45 46 {{ if .HTTPSProxy }} 47 https_proxy "{{ .HTTPSProxy }}" 48 ENV['https_proxy'] = "{{ .HTTPSProxy }}" 49 ENV['HTTPS_PROXY'] = "{{ .HTTPSProxy }}" 50 {{ end }} 51 52 {{ if .NOProxy }}no_proxy "{{ join .NOProxy "," }}"{{ end }} 53 {{ if .SSLVerifyMode }}ssl_verify_mode {{ .SSLVerifyMode }}{{ end }} 54 ` 55 56 // Provisioner represents a specificly configured chef provisioner 57 type Provisioner struct { 58 Attributes interface{} `mapstructure:"attributes"` 59 Environment string `mapstructure:"environment"` 60 LogToFile bool `mapstructure:"log_to_file"` 61 HTTPProxy string `mapstructure:"http_proxy"` 62 HTTPSProxy string `mapstructure:"https_proxy"` 63 NOProxy []string `mapstructure:"no_proxy"` 64 NodeName string `mapstructure:"node_name"` 65 PreventSudo bool `mapstructure:"prevent_sudo"` 66 RunList []string `mapstructure:"run_list"` 67 ServerURL string `mapstructure:"server_url"` 68 SkipInstall bool `mapstructure:"skip_install"` 69 SSLVerifyMode string `mapstructure:"ssl_verify_mode"` 70 ValidationClientName string `mapstructure:"validation_client_name"` 71 ValidationKeyPath string `mapstructure:"validation_key_path"` 72 Version string `mapstructure:"version"` 73 74 installChefClient func(terraform.UIOutput, communicator.Communicator) error 75 createConfigFiles func(terraform.UIOutput, communicator.Communicator) error 76 runChefClient func(terraform.UIOutput, communicator.Communicator) error 77 useSudo bool 78 } 79 80 // ResourceProvisioner represents a generic chef provisioner 81 type ResourceProvisioner struct{} 82 83 // Apply executes the file provisioner 84 func (r *ResourceProvisioner) Apply( 85 o terraform.UIOutput, 86 s *terraform.InstanceState, 87 c *terraform.ResourceConfig) error { 88 // Decode the raw config for this provisioner 89 p, err := r.decodeConfig(c) 90 if err != nil { 91 return err 92 } 93 94 // Set some values based on the targeted OS 95 switch s.Ephemeral.ConnInfo["type"] { 96 case "ssh", "": // The default connection type is ssh, so if the type is empty use ssh 97 p.installChefClient = p.sshInstallChefClient 98 p.createConfigFiles = p.sshCreateConfigFiles 99 p.runChefClient = p.runChefClientFunc(linuxConfDir) 100 p.useSudo = !p.PreventSudo && s.Ephemeral.ConnInfo["user"] != "root" 101 case "winrm": 102 p.installChefClient = p.winrmInstallChefClient 103 p.createConfigFiles = p.winrmCreateConfigFiles 104 p.runChefClient = p.runChefClientFunc(windowsConfDir) 105 p.useSudo = false 106 default: 107 return fmt.Errorf("Unsupported connection type: %s", s.Ephemeral.ConnInfo["type"]) 108 } 109 110 // Get a new communicator 111 comm, err := communicator.New(s) 112 if err != nil { 113 return err 114 } 115 116 // Wait and retry until we establish the connection 117 err = retryFunc(comm.Timeout(), func() error { 118 err := comm.Connect(o) 119 return err 120 }) 121 if err != nil { 122 return err 123 } 124 defer comm.Disconnect() 125 126 if !p.SkipInstall { 127 if err := p.installChefClient(o, comm); err != nil { 128 return err 129 } 130 } 131 132 o.Output("Creating configuration files...") 133 if err := p.createConfigFiles(o, comm); err != nil { 134 return err 135 } 136 137 o.Output("Starting initial Chef-Client run...") 138 if err := p.runChefClient(o, comm); err != nil { 139 return err 140 } 141 142 return nil 143 } 144 145 // Validate checks if the required arguments are configured 146 func (r *ResourceProvisioner) Validate(c *terraform.ResourceConfig) (ws []string, es []error) { 147 p, err := r.decodeConfig(c) 148 if err != nil { 149 es = append(es, err) 150 return ws, es 151 } 152 153 if p.NodeName == "" { 154 es = append(es, fmt.Errorf("Key not found: node_name")) 155 } 156 if p.RunList == nil { 157 es = append(es, fmt.Errorf("Key not found: run_list")) 158 } 159 if p.ServerURL == "" { 160 es = append(es, fmt.Errorf("Key not found: server_url")) 161 } 162 if p.ValidationClientName == "" { 163 es = append(es, fmt.Errorf("Key not found: validation_client_name")) 164 } 165 if p.ValidationKeyPath == "" { 166 es = append(es, fmt.Errorf("Key not found: validation_key_path")) 167 } 168 169 return ws, es 170 } 171 172 func (r *ResourceProvisioner) decodeConfig(c *terraform.ResourceConfig) (*Provisioner, error) { 173 p := new(Provisioner) 174 175 decConf := &mapstructure.DecoderConfig{ 176 ErrorUnused: true, 177 WeaklyTypedInput: true, 178 Result: p, 179 } 180 dec, err := mapstructure.NewDecoder(decConf) 181 if err != nil { 182 return nil, err 183 } 184 185 if err := dec.Decode(c.Raw); err != nil { 186 return nil, err 187 } 188 189 if p.Environment == "" { 190 p.Environment = defaultEnv 191 } 192 193 if attrs, ok := c.Raw["attributes"]; ok { 194 p.Attributes, err = rawToJSON(attrs) 195 if err != nil { 196 return nil, fmt.Errorf("Error parsing the attributes: %v", err) 197 } 198 } 199 200 return p, nil 201 } 202 203 func rawToJSON(raw interface{}) (interface{}, error) { 204 switch s := raw.(type) { 205 case []map[string]interface{}: 206 if len(s) != 1 { 207 return nil, errors.New("unexpected input while parsing raw config to JSON") 208 } 209 210 var err error 211 for k, v := range s[0] { 212 s[0][k], err = rawToJSON(v) 213 if err != nil { 214 return nil, err 215 } 216 } 217 218 return s[0], nil 219 default: 220 return raw, nil 221 } 222 } 223 224 // retryFunc is used to retry a function for a given duration 225 func retryFunc(timeout time.Duration, f func() error) error { 226 finish := time.After(timeout) 227 for { 228 err := f() 229 if err == nil { 230 return nil 231 } 232 log.Printf("Retryable error: %v", err) 233 234 select { 235 case <-finish: 236 return err 237 case <-time.After(3 * time.Second): 238 } 239 } 240 } 241 242 func (p *Provisioner) runChefClientFunc( 243 confDir string) func(terraform.UIOutput, communicator.Communicator) error { 244 return func(o terraform.UIOutput, comm communicator.Communicator) error { 245 fb := path.Join(confDir, firstBoot) 246 cmd := fmt.Sprintf("chef-client -j %q -E %q", fb, p.Environment) 247 248 if p.LogToFile { 249 if err := os.MkdirAll(logfileDir, 0755); err != nil { 250 return fmt.Errorf("Error creating logfile directory %s: %v", logfileDir, err) 251 } 252 253 logFile := path.Join(logfileDir, p.NodeName) 254 f, err := os.Create(path.Join(logFile)) 255 if err != nil { 256 return fmt.Errorf("Error creating logfile %s: %v", logFile, err) 257 } 258 f.Close() 259 260 o.Output("Writing Chef Client output to " + logFile) 261 o = p 262 } 263 264 return p.runCommand(o, comm, cmd) 265 } 266 } 267 268 // Output implementation of terraform.UIOutput interface 269 func (p *Provisioner) Output(output string) { 270 logFile := path.Join(logfileDir, p.NodeName) 271 f, err := os.OpenFile(logFile, os.O_APPEND|os.O_WRONLY, 0666) 272 if err != nil { 273 log.Printf("Error creating logfile %s: %v", logFile, err) 274 return 275 } 276 defer f.Close() 277 278 // These steps are needed to remove any ANSI escape codes used to colorize 279 // the output and to make sure we have proper line endings before writing 280 // the string to the logfile. 281 re := regexp.MustCompile(`\x1b\[[0-9;]+m`) 282 output = re.ReplaceAllString(output, "") 283 output = strings.Replace(output, "\r", "\n", -1) 284 285 if _, err := f.WriteString(output); err != nil { 286 log.Printf("Error writing output to logfile %s: %v", logFile, err) 287 } 288 289 if err := f.Sync(); err != nil { 290 log.Printf("Error saving logfile %s to disk: %v", logFile, err) 291 } 292 } 293 294 func (p *Provisioner) deployConfigFiles( 295 o terraform.UIOutput, 296 comm communicator.Communicator, 297 confDir string) error { 298 // Open the validation key file 299 f, err := os.Open(p.ValidationKeyPath) 300 if err != nil { 301 return err 302 } 303 defer f.Close() 304 305 // Copy the validation key to the new instance 306 if err := comm.Upload(path.Join(confDir, validationKey), f); err != nil { 307 return fmt.Errorf("Uploading %s failed: %v", validationKey, err) 308 } 309 310 // Make strings.Join available for use within the template 311 funcMap := template.FuncMap{ 312 "join": strings.Join, 313 } 314 315 // Create a new template and parse the client config into it 316 t := template.Must(template.New(clienrb).Funcs(funcMap).Parse(clientConf)) 317 318 var buf bytes.Buffer 319 err = t.Execute(&buf, p) 320 if err != nil { 321 return fmt.Errorf("Error executing %s template: %s", clienrb, err) 322 } 323 324 // Copy the client config to the new instance 325 if err := comm.Upload(path.Join(confDir, clienrb), &buf); err != nil { 326 return fmt.Errorf("Uploading %s failed: %v", clienrb, err) 327 } 328 329 // Create a map with first boot settings 330 fb := make(map[string]interface{}) 331 if p.Attributes != nil { 332 fb = p.Attributes.(map[string]interface{}) 333 } 334 335 // Check if the run_list was also in the attributes and if so log a warning 336 // that it will be overwritten with the value of the run_list argument. 337 if _, found := fb["run_list"]; found { 338 log.Printf("[WARNING] Found a 'run_list' specified in the configured attributes! " + 339 "This value will be overwritten by the value of the `run_list` argument!") 340 } 341 342 // Add the initial runlist to the first boot settings 343 fb["run_list"] = p.RunList 344 345 // Marshal the first boot settings to JSON 346 d, err := json.Marshal(fb) 347 if err != nil { 348 return fmt.Errorf("Failed to create %s data: %s", firstBoot, err) 349 } 350 351 // Copy the first-boot.json to the new instance 352 if err := comm.Upload(path.Join(confDir, firstBoot), bytes.NewReader(d)); err != nil { 353 return fmt.Errorf("Uploading %s failed: %v", firstBoot, err) 354 } 355 356 return nil 357 } 358 359 // runCommand is used to run already prepared commands 360 func (p *Provisioner) runCommand( 361 o terraform.UIOutput, 362 comm communicator.Communicator, 363 command string) error { 364 var err error 365 366 // Unless prevented, prefix the command with sudo 367 if p.useSudo { 368 command = "sudo " + command 369 } 370 371 outR, outW := io.Pipe() 372 errR, errW := io.Pipe() 373 outDoneCh := make(chan struct{}) 374 errDoneCh := make(chan struct{}) 375 go p.copyOutput(o, outR, outDoneCh) 376 go p.copyOutput(o, errR, errDoneCh) 377 378 cmd := &remote.Cmd{ 379 Command: command, 380 Stdout: outW, 381 Stderr: errW, 382 } 383 384 if err := comm.Start(cmd); err != nil { 385 return fmt.Errorf("Error executing command %q: %v", cmd.Command, err) 386 } 387 388 cmd.Wait() 389 if cmd.ExitStatus != 0 { 390 err = fmt.Errorf( 391 "Command %q exited with non-zero exit status: %d", cmd.Command, cmd.ExitStatus) 392 } 393 394 // Wait for output to clean up 395 outW.Close() 396 errW.Close() 397 <-outDoneCh 398 <-errDoneCh 399 400 // If we have an error, return it out now that we've cleaned up 401 if err != nil { 402 return err 403 } 404 405 return nil 406 } 407 408 func (p *Provisioner) copyOutput(o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) { 409 defer close(doneCh) 410 lr := linereader.New(r) 411 for line := range lr.Ch { 412 o.Output(line) 413 } 414 }