github.com/ddnomad/packer@v1.3.2/provisioner/ansible/provisioner.go (about) 1 package ansible 2 3 import ( 4 "bufio" 5 "bytes" 6 "crypto/rand" 7 "crypto/rsa" 8 "crypto/x509" 9 "encoding/pem" 10 "errors" 11 "fmt" 12 "io" 13 "io/ioutil" 14 "log" 15 "net" 16 "os" 17 "os/exec" 18 "os/user" 19 "path/filepath" 20 "regexp" 21 "strconv" 22 "strings" 23 "sync" 24 "unicode" 25 26 "golang.org/x/crypto/ssh" 27 28 "github.com/hashicorp/packer/common" 29 commonhelper "github.com/hashicorp/packer/helper/common" 30 "github.com/hashicorp/packer/helper/config" 31 "github.com/hashicorp/packer/packer" 32 "github.com/hashicorp/packer/template/interpolate" 33 ) 34 35 type Config struct { 36 common.PackerConfig `mapstructure:",squash"` 37 ctx interpolate.Context 38 39 // The command to run ansible 40 Command string 41 42 // Extra options to pass to the ansible command 43 ExtraArguments []string `mapstructure:"extra_arguments"` 44 45 AnsibleEnvVars []string `mapstructure:"ansible_env_vars"` 46 47 // The main playbook file to execute. 48 PlaybookFile string `mapstructure:"playbook_file"` 49 Groups []string `mapstructure:"groups"` 50 EmptyGroups []string `mapstructure:"empty_groups"` 51 HostAlias string `mapstructure:"host_alias"` 52 User string `mapstructure:"user"` 53 LocalPort string `mapstructure:"local_port"` 54 SSHHostKeyFile string `mapstructure:"ssh_host_key_file"` 55 SSHAuthorizedKeyFile string `mapstructure:"ssh_authorized_key_file"` 56 SFTPCmd string `mapstructure:"sftp_command"` 57 SkipVersionCheck bool `mapstructure:"skip_version_check"` 58 UseSFTP bool `mapstructure:"use_sftp"` 59 InventoryDirectory string `mapstructure:"inventory_directory"` 60 InventoryFile string `mapstructure:"inventory_file"` 61 } 62 63 type Provisioner struct { 64 config Config 65 adapter *adapter 66 done chan struct{} 67 ansibleVersion string 68 ansibleMajVersion uint 69 } 70 71 type PassthroughTemplate struct { 72 WinRMPassword string 73 } 74 75 func (p *Provisioner) Prepare(raws ...interface{}) error { 76 p.done = make(chan struct{}) 77 78 // Create passthrough for winrm password so we can fill it in once we know 79 // it 80 p.config.ctx.Data = &PassthroughTemplate{ 81 WinRMPassword: `{{.WinRMPassword}}`, 82 } 83 84 err := config.Decode(&p.config, &config.DecodeOpts{ 85 Interpolate: true, 86 InterpolateContext: &p.config.ctx, 87 InterpolateFilter: &interpolate.RenderFilter{ 88 Exclude: []string{}, 89 }, 90 }, raws...) 91 if err != nil { 92 return err 93 } 94 95 // Defaults 96 if p.config.Command == "" { 97 p.config.Command = "ansible-playbook" 98 } 99 100 if p.config.HostAlias == "" { 101 p.config.HostAlias = "default" 102 } 103 104 var errs *packer.MultiError 105 err = validateFileConfig(p.config.PlaybookFile, "playbook_file", true) 106 if err != nil { 107 errs = packer.MultiErrorAppend(errs, err) 108 } 109 110 // Check that the authorized key file exists 111 if len(p.config.SSHAuthorizedKeyFile) > 0 { 112 err = validateFileConfig(p.config.SSHAuthorizedKeyFile, "ssh_authorized_key_file", true) 113 if err != nil { 114 log.Println(p.config.SSHAuthorizedKeyFile, "does not exist") 115 errs = packer.MultiErrorAppend(errs, err) 116 } 117 } 118 if len(p.config.SSHHostKeyFile) > 0 { 119 err = validateFileConfig(p.config.SSHHostKeyFile, "ssh_host_key_file", true) 120 if err != nil { 121 log.Println(p.config.SSHHostKeyFile, "does not exist") 122 errs = packer.MultiErrorAppend(errs, err) 123 } 124 } else { 125 p.config.AnsibleEnvVars = append(p.config.AnsibleEnvVars, "ANSIBLE_HOST_KEY_CHECKING=False") 126 } 127 128 if !p.config.UseSFTP { 129 p.config.AnsibleEnvVars = append(p.config.AnsibleEnvVars, "ANSIBLE_SCP_IF_SSH=True") 130 } 131 132 if len(p.config.LocalPort) > 0 { 133 if _, err := strconv.ParseUint(p.config.LocalPort, 10, 16); err != nil { 134 errs = packer.MultiErrorAppend(errs, fmt.Errorf("local_port: %s must be a valid port", p.config.LocalPort)) 135 } 136 } else { 137 p.config.LocalPort = "0" 138 } 139 140 if len(p.config.InventoryDirectory) > 0 { 141 err = validateInventoryDirectoryConfig(p.config.InventoryDirectory) 142 if err != nil { 143 log.Println(p.config.InventoryDirectory, "does not exist") 144 errs = packer.MultiErrorAppend(errs, err) 145 } 146 } 147 148 if !p.config.SkipVersionCheck { 149 err = p.getVersion() 150 if err != nil { 151 errs = packer.MultiErrorAppend(errs, err) 152 } 153 } 154 155 if p.config.User == "" { 156 usr, err := user.Current() 157 if err != nil { 158 errs = packer.MultiErrorAppend(errs, err) 159 } else { 160 p.config.User = usr.Username 161 } 162 } 163 if p.config.User == "" { 164 errs = packer.MultiErrorAppend(errs, fmt.Errorf("user: could not determine current user from environment.")) 165 } 166 167 if errs != nil && len(errs.Errors) > 0 { 168 return errs 169 } 170 return nil 171 } 172 173 func (p *Provisioner) getVersion() error { 174 out, err := exec.Command(p.config.Command, "--version").Output() 175 if err != nil { 176 return fmt.Errorf( 177 "Error running \"%s --version\": %s", p.config.Command, err.Error()) 178 } 179 180 versionRe := regexp.MustCompile(`\w (\d+\.\d+[.\d+]*)`) 181 matches := versionRe.FindStringSubmatch(string(out)) 182 if matches == nil { 183 return fmt.Errorf( 184 "Could not find %s version in output:\n%s", p.config.Command, string(out)) 185 } 186 187 version := matches[1] 188 log.Printf("%s version: %s", p.config.Command, version) 189 p.ansibleVersion = version 190 191 majVer, err := strconv.ParseUint(strings.Split(version, ".")[0], 10, 0) 192 if err != nil { 193 return fmt.Errorf("Could not parse major version from \"%s\".", version) 194 } 195 p.ansibleMajVersion = uint(majVer) 196 197 return nil 198 } 199 200 func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { 201 ui.Say("Provisioning with Ansible...") 202 // Interpolate env vars to check for .WinRMPassword 203 p.config.ctx.Data = &PassthroughTemplate{ 204 WinRMPassword: getWinRMPassword(p.config.PackerBuildName), 205 } 206 for i, envVar := range p.config.AnsibleEnvVars { 207 envVar, err := interpolate.Render(envVar, &p.config.ctx) 208 if err != nil { 209 return fmt.Errorf("Could not interpolate ansible env vars: %s", err) 210 } 211 p.config.AnsibleEnvVars[i] = envVar 212 } 213 // Interpolate extra vars to check for .WinRMPassword 214 for i, arg := range p.config.ExtraArguments { 215 arg, err := interpolate.Render(arg, &p.config.ctx) 216 if err != nil { 217 return fmt.Errorf("Could not interpolate ansible env vars: %s", err) 218 } 219 p.config.ExtraArguments[i] = arg 220 } 221 222 k, err := newUserKey(p.config.SSHAuthorizedKeyFile) 223 if err != nil { 224 return err 225 } 226 227 hostSigner, err := newSigner(p.config.SSHHostKeyFile) 228 // Remove the private key file 229 if len(k.privKeyFile) > 0 { 230 defer os.Remove(k.privKeyFile) 231 } 232 233 keyChecker := ssh.CertChecker{ 234 UserKeyFallback: func(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) { 235 if user := conn.User(); user != p.config.User { 236 return nil, errors.New(fmt.Sprintf("authentication failed: %s is not a valid user", user)) 237 } 238 239 if !bytes.Equal(k.Marshal(), pubKey.Marshal()) { 240 return nil, errors.New("authentication failed: unauthorized key") 241 } 242 243 return nil, nil 244 }, 245 } 246 247 config := &ssh.ServerConfig{ 248 AuthLogCallback: func(conn ssh.ConnMetadata, method string, err error) { 249 log.Printf("authentication attempt from %s to %s as %s using %s", conn.RemoteAddr(), conn.LocalAddr(), conn.User(), method) 250 }, 251 PublicKeyCallback: keyChecker.Authenticate, 252 //NoClientAuth: true, 253 } 254 255 config.AddHostKey(hostSigner) 256 257 localListener, err := func() (net.Listener, error) { 258 port, err := strconv.ParseUint(p.config.LocalPort, 10, 16) 259 if err != nil { 260 return nil, err 261 } 262 263 tries := 1 264 if port != 0 { 265 tries = 10 266 } 267 for i := 0; i < tries; i++ { 268 l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port)) 269 port++ 270 if err != nil { 271 ui.Say(err.Error()) 272 continue 273 } 274 _, p.config.LocalPort, err = net.SplitHostPort(l.Addr().String()) 275 if err != nil { 276 ui.Say(err.Error()) 277 continue 278 } 279 return l, nil 280 } 281 return nil, errors.New("Error setting up SSH proxy connection") 282 }() 283 284 if err != nil { 285 return err 286 } 287 288 ui = newUi(ui) 289 p.adapter = newAdapter(p.done, localListener, config, p.config.SFTPCmd, ui, comm) 290 291 defer func() { 292 log.Print("shutting down the SSH proxy") 293 close(p.done) 294 p.adapter.Shutdown() 295 }() 296 297 go p.adapter.Serve() 298 299 if len(p.config.InventoryFile) == 0 { 300 tf, err := ioutil.TempFile(p.config.InventoryDirectory, "packer-provisioner-ansible") 301 if err != nil { 302 return fmt.Errorf("Error preparing inventory file: %s", err) 303 } 304 defer os.Remove(tf.Name()) 305 306 host := fmt.Sprintf("%s ansible_host=127.0.0.1 ansible_user=%s ansible_port=%s\n", 307 p.config.HostAlias, p.config.User, p.config.LocalPort) 308 if p.ansibleMajVersion < 2 { 309 host = fmt.Sprintf("%s ansible_ssh_host=127.0.0.1 ansible_ssh_user=%s ansible_ssh_port=%s\n", 310 p.config.HostAlias, p.config.User, p.config.LocalPort) 311 } 312 313 w := bufio.NewWriter(tf) 314 w.WriteString(host) 315 for _, group := range p.config.Groups { 316 fmt.Fprintf(w, "[%s]\n%s", group, host) 317 } 318 319 for _, group := range p.config.EmptyGroups { 320 fmt.Fprintf(w, "[%s]\n", group) 321 } 322 323 if err := w.Flush(); err != nil { 324 tf.Close() 325 return fmt.Errorf("Error preparing inventory file: %s", err) 326 } 327 tf.Close() 328 p.config.InventoryFile = tf.Name() 329 defer func() { 330 p.config.InventoryFile = "" 331 }() 332 } 333 334 if err := p.executeAnsible(ui, comm, k.privKeyFile); err != nil { 335 return fmt.Errorf("Error executing Ansible: %s", err) 336 } 337 338 return nil 339 } 340 341 func (p *Provisioner) Cancel() { 342 if p.done != nil { 343 close(p.done) 344 } 345 if p.adapter != nil { 346 p.adapter.Shutdown() 347 } 348 os.Exit(0) 349 } 350 351 func (p *Provisioner) executeAnsible(ui packer.Ui, comm packer.Communicator, privKeyFile string) error { 352 playbook, _ := filepath.Abs(p.config.PlaybookFile) 353 inventory := p.config.InventoryFile 354 if len(p.config.InventoryDirectory) > 0 { 355 inventory = p.config.InventoryDirectory 356 } 357 var envvars []string 358 359 args := []string{"--extra-vars", fmt.Sprintf("packer_build_name=%s packer_builder_type=%s", 360 p.config.PackerBuildName, p.config.PackerBuilderType), 361 "-i", inventory, playbook} 362 if len(privKeyFile) > 0 { 363 // Changed this from using --private-key to supplying -e ansible_ssh_private_key_file as the latter 364 // is treated as a highest priority variable, and thus prevents overriding by dynamic variables 365 // as seen in #5852 366 // args = append(args, "--private-key", privKeyFile) 367 args = append(args, "-e", fmt.Sprintf("ansible_ssh_private_key_file=%s", privKeyFile)) 368 } 369 370 // expose packer_http_addr extra variable 371 httpAddr := common.GetHTTPAddr() 372 if httpAddr != "" { 373 args = append(args, "--extra-vars", fmt.Sprintf("packer_http_addr=%s", httpAddr)) 374 } 375 376 args = append(args, p.config.ExtraArguments...) 377 if len(p.config.AnsibleEnvVars) > 0 { 378 envvars = append(envvars, p.config.AnsibleEnvVars...) 379 } 380 381 cmd := exec.Command(p.config.Command, args...) 382 383 cmd.Env = os.Environ() 384 if len(envvars) > 0 { 385 cmd.Env = append(cmd.Env, envvars...) 386 } 387 388 stdout, err := cmd.StdoutPipe() 389 if err != nil { 390 return err 391 } 392 stderr, err := cmd.StderrPipe() 393 if err != nil { 394 return err 395 } 396 397 wg := sync.WaitGroup{} 398 repeat := func(r io.ReadCloser) { 399 reader := bufio.NewReader(r) 400 for { 401 line, err := reader.ReadString('\n') 402 if line != "" { 403 line = strings.TrimRightFunc(line, unicode.IsSpace) 404 ui.Message(line) 405 } 406 if err != nil { 407 if err == io.EOF { 408 break 409 } else { 410 ui.Error(err.Error()) 411 break 412 } 413 } 414 } 415 wg.Done() 416 } 417 wg.Add(2) 418 go repeat(stdout) 419 go repeat(stderr) 420 421 // remove winrm password from command, if it's been added 422 flattenedCmd := strings.Join(cmd.Args, " ") 423 sanitized := flattenedCmd 424 if len(getWinRMPassword(p.config.PackerBuildName)) > 0 { 425 sanitized = strings.Replace(sanitized, 426 getWinRMPassword(p.config.PackerBuildName), "*****", -1) 427 } 428 ui.Say(fmt.Sprintf("Executing Ansible: %s", sanitized)) 429 430 if err := cmd.Start(); err != nil { 431 return err 432 } 433 wg.Wait() 434 err = cmd.Wait() 435 if err != nil { 436 return fmt.Errorf("Non-zero exit status: %s", err) 437 } 438 439 return nil 440 } 441 442 func validateFileConfig(name string, config string, req bool) error { 443 if req { 444 if name == "" { 445 return fmt.Errorf("%s must be specified.", config) 446 } 447 } 448 info, err := os.Stat(name) 449 if err != nil { 450 return fmt.Errorf("%s: %s is invalid: %s", config, name, err) 451 } else if info.IsDir() { 452 return fmt.Errorf("%s: %s must point to a file", config, name) 453 } 454 return nil 455 } 456 457 func validateInventoryDirectoryConfig(name string) error { 458 info, err := os.Stat(name) 459 if err != nil { 460 return fmt.Errorf("inventory_directory: %s is invalid: %s", name, err) 461 } else if !info.IsDir() { 462 return fmt.Errorf("inventory_directory: %s must point to a directory", name) 463 } 464 return nil 465 } 466 467 type userKey struct { 468 ssh.PublicKey 469 privKeyFile string 470 } 471 472 func newUserKey(pubKeyFile string) (*userKey, error) { 473 userKey := new(userKey) 474 if len(pubKeyFile) > 0 { 475 pubKeyBytes, err := ioutil.ReadFile(pubKeyFile) 476 if err != nil { 477 return nil, errors.New("Failed to read public key") 478 } 479 userKey.PublicKey, _, _, _, err = ssh.ParseAuthorizedKey(pubKeyBytes) 480 if err != nil { 481 return nil, errors.New("Failed to parse authorized key") 482 } 483 484 return userKey, nil 485 } 486 487 key, err := rsa.GenerateKey(rand.Reader, 2048) 488 if err != nil { 489 return nil, errors.New("Failed to generate key pair") 490 } 491 userKey.PublicKey, err = ssh.NewPublicKey(key.Public()) 492 if err != nil { 493 return nil, errors.New("Failed to extract public key from generated key pair") 494 } 495 496 // To support Ansible calling back to us we need to write 497 // this file down 498 privateKeyDer := x509.MarshalPKCS1PrivateKey(key) 499 privateKeyBlock := pem.Block{ 500 Type: "RSA PRIVATE KEY", 501 Headers: nil, 502 Bytes: privateKeyDer, 503 } 504 tf, err := ioutil.TempFile("", "ansible-key") 505 if err != nil { 506 return nil, errors.New("failed to create temp file for generated key") 507 } 508 _, err = tf.Write(pem.EncodeToMemory(&privateKeyBlock)) 509 if err != nil { 510 return nil, errors.New("failed to write private key to temp file") 511 } 512 513 err = tf.Close() 514 if err != nil { 515 return nil, errors.New("failed to close private key temp file") 516 } 517 userKey.privKeyFile = tf.Name() 518 519 return userKey, nil 520 } 521 522 type signer struct { 523 ssh.Signer 524 } 525 526 func newSigner(privKeyFile string) (*signer, error) { 527 signer := new(signer) 528 529 if len(privKeyFile) > 0 { 530 privateBytes, err := ioutil.ReadFile(privKeyFile) 531 if err != nil { 532 return nil, errors.New("Failed to load private host key") 533 } 534 535 signer.Signer, err = ssh.ParsePrivateKey(privateBytes) 536 if err != nil { 537 return nil, errors.New("Failed to parse private host key") 538 } 539 540 return signer, nil 541 } 542 543 key, err := rsa.GenerateKey(rand.Reader, 2048) 544 if err != nil { 545 return nil, errors.New("Failed to generate server key pair") 546 } 547 548 signer.Signer, err = ssh.NewSignerFromKey(key) 549 if err != nil { 550 return nil, errors.New("Failed to extract private key from generated key pair") 551 } 552 553 return signer, nil 554 } 555 556 func getWinRMPassword(buildName string) string { 557 winRMPass, _ := commonhelper.RetrieveSharedState("winrm_password", buildName) 558 packer.LogSecretFilter.Set(winRMPass) 559 return winRMPass 560 } 561 562 // Ui provides concurrency-safe access to packer.Ui. 563 type Ui struct { 564 sem chan int 565 ui packer.Ui 566 } 567 568 func newUi(ui packer.Ui) packer.Ui { 569 return &Ui{sem: make(chan int, 1), ui: ui} 570 } 571 572 func (ui *Ui) Ask(s string) (string, error) { 573 ui.sem <- 1 574 ret, err := ui.ui.Ask(s) 575 <-ui.sem 576 577 return ret, err 578 } 579 580 func (ui *Ui) Say(s string) { 581 ui.sem <- 1 582 ui.ui.Say(s) 583 <-ui.sem 584 } 585 586 func (ui *Ui) Message(s string) { 587 ui.sem <- 1 588 ui.ui.Message(s) 589 <-ui.sem 590 } 591 592 func (ui *Ui) Error(s string) { 593 ui.sem <- 1 594 ui.ui.Error(s) 595 <-ui.sem 596 } 597 598 func (ui *Ui) Machine(t string, args ...string) { 599 ui.sem <- 1 600 ui.ui.Machine(t, args...) 601 <-ui.sem 602 } 603 604 func (ui *Ui) ProgressBar() packer.ProgressBar { 605 return new(packer.NoopProgressBar) 606 }