github.com/ddnomad/packer@v1.3.2/provisioner/ansible-local/provisioner.go (about) 1 package ansiblelocal 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "os" 7 "path/filepath" 8 "strings" 9 10 "github.com/hashicorp/packer/common" 11 "github.com/hashicorp/packer/common/uuid" 12 "github.com/hashicorp/packer/helper/config" 13 "github.com/hashicorp/packer/packer" 14 "github.com/hashicorp/packer/template/interpolate" 15 ) 16 17 const DefaultStagingDir = "/tmp/packer-provisioner-ansible-local" 18 19 type Config struct { 20 common.PackerConfig `mapstructure:",squash"` 21 ctx interpolate.Context 22 23 // The command to run ansible 24 Command string 25 26 // Extra options to pass to the ansible command 27 ExtraArguments []string `mapstructure:"extra_arguments"` 28 29 // Path to group_vars directory 30 GroupVars string `mapstructure:"group_vars"` 31 32 // Path to host_vars directory 33 HostVars string `mapstructure:"host_vars"` 34 35 // The playbook dir to upload. 36 PlaybookDir string `mapstructure:"playbook_dir"` 37 38 // The main playbook file to execute. 39 PlaybookFile string `mapstructure:"playbook_file"` 40 41 // The playbook files to execute. 42 PlaybookFiles []string `mapstructure:"playbook_files"` 43 44 // An array of local paths of playbook files to upload. 45 PlaybookPaths []string `mapstructure:"playbook_paths"` 46 47 // An array of local paths of roles to upload. 48 RolePaths []string `mapstructure:"role_paths"` 49 50 // The directory where files will be uploaded. Packer requires write 51 // permissions in this directory. 52 StagingDir string `mapstructure:"staging_directory"` 53 54 // If true, staging directory is removed after executing ansible. 55 CleanStagingDir bool `mapstructure:"clean_staging_directory"` 56 57 // The optional inventory file 58 InventoryFile string `mapstructure:"inventory_file"` 59 60 // The optional inventory groups 61 InventoryGroups []string `mapstructure:"inventory_groups"` 62 63 // The optional ansible-galaxy requirements file 64 GalaxyFile string `mapstructure:"galaxy_file"` 65 66 // The command to run ansible-galaxy 67 GalaxyCommand string 68 } 69 70 type Provisioner struct { 71 config Config 72 73 playbookFiles []string 74 } 75 76 func (p *Provisioner) Prepare(raws ...interface{}) error { 77 err := config.Decode(&p.config, &config.DecodeOpts{ 78 Interpolate: true, 79 InterpolateContext: &p.config.ctx, 80 InterpolateFilter: &interpolate.RenderFilter{ 81 Exclude: []string{}, 82 }, 83 }, raws...) 84 if err != nil { 85 return err 86 } 87 88 // Reset the state. 89 p.playbookFiles = make([]string, 0, len(p.config.PlaybookFiles)) 90 91 // Defaults 92 if p.config.Command == "" { 93 p.config.Command = "ANSIBLE_FORCE_COLOR=1 PYTHONUNBUFFERED=1 ansible-playbook" 94 } 95 if p.config.GalaxyCommand == "" { 96 p.config.GalaxyCommand = "ansible-galaxy" 97 } 98 99 if p.config.StagingDir == "" { 100 p.config.StagingDir = filepath.ToSlash(filepath.Join(DefaultStagingDir, uuid.TimeOrderedUUID())) 101 } 102 103 // Validation 104 var errs *packer.MultiError 105 106 // Check that either playbook_file or playbook_files is specified 107 if len(p.config.PlaybookFiles) != 0 && p.config.PlaybookFile != "" { 108 errs = packer.MultiErrorAppend(errs, fmt.Errorf("Either playbook_file or playbook_files can be specified, not both")) 109 } 110 if len(p.config.PlaybookFiles) == 0 && p.config.PlaybookFile == "" { 111 errs = packer.MultiErrorAppend(errs, fmt.Errorf("Either playbook_file or playbook_files must be specified")) 112 } 113 if p.config.PlaybookFile != "" { 114 err = validateFileConfig(p.config.PlaybookFile, "playbook_file", true) 115 if err != nil { 116 errs = packer.MultiErrorAppend(errs, err) 117 } 118 } 119 120 for _, playbookFile := range p.config.PlaybookFiles { 121 if err := validateFileConfig(playbookFile, "playbook_files", true); err != nil { 122 errs = packer.MultiErrorAppend(errs, err) 123 } else { 124 playbookFile, err := filepath.Abs(playbookFile) 125 if err != nil { 126 errs = packer.MultiErrorAppend(errs, err) 127 } else { 128 p.playbookFiles = append(p.playbookFiles, playbookFile) 129 } 130 } 131 } 132 133 // Check that the inventory file exists, if configured 134 if len(p.config.InventoryFile) > 0 { 135 err = validateFileConfig(p.config.InventoryFile, "inventory_file", true) 136 if err != nil { 137 errs = packer.MultiErrorAppend(errs, err) 138 } 139 } 140 141 // Check that the galaxy file exists, if configured 142 if len(p.config.GalaxyFile) > 0 { 143 err = validateFileConfig(p.config.GalaxyFile, "galaxy_file", true) 144 if err != nil { 145 errs = packer.MultiErrorAppend(errs, err) 146 } 147 } 148 149 // Check that the playbook_dir directory exists, if configured 150 if len(p.config.PlaybookDir) > 0 { 151 if err := validateDirConfig(p.config.PlaybookDir, "playbook_dir"); err != nil { 152 errs = packer.MultiErrorAppend(errs, err) 153 } 154 } 155 156 // Check that the group_vars directory exists, if configured 157 if len(p.config.GroupVars) > 0 { 158 if err := validateDirConfig(p.config.GroupVars, "group_vars"); err != nil { 159 errs = packer.MultiErrorAppend(errs, err) 160 } 161 } 162 163 // Check that the host_vars directory exists, if configured 164 if len(p.config.HostVars) > 0 { 165 if err := validateDirConfig(p.config.HostVars, "host_vars"); err != nil { 166 errs = packer.MultiErrorAppend(errs, err) 167 } 168 } 169 170 for _, path := range p.config.PlaybookPaths { 171 err := validateDirConfig(path, "playbook_paths") 172 if err != nil { 173 errs = packer.MultiErrorAppend(errs, err) 174 } 175 } 176 for _, path := range p.config.RolePaths { 177 if err := validateDirConfig(path, "role_paths"); err != nil { 178 errs = packer.MultiErrorAppend(errs, err) 179 } 180 } 181 182 if errs != nil && len(errs.Errors) > 0 { 183 return errs 184 } 185 return nil 186 } 187 188 func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { 189 ui.Say("Provisioning with Ansible...") 190 191 if len(p.config.PlaybookDir) > 0 { 192 ui.Message("Uploading Playbook directory to Ansible staging directory...") 193 if err := p.uploadDir(ui, comm, p.config.StagingDir, p.config.PlaybookDir); err != nil { 194 return fmt.Errorf("Error uploading playbook_dir directory: %s", err) 195 } 196 } else { 197 ui.Message("Creating Ansible staging directory...") 198 if err := p.createDir(ui, comm, p.config.StagingDir); err != nil { 199 return fmt.Errorf("Error creating staging directory: %s", err) 200 } 201 } 202 203 if p.config.PlaybookFile != "" { 204 ui.Message("Uploading main Playbook file...") 205 src := p.config.PlaybookFile 206 dst := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(src))) 207 if err := p.uploadFile(ui, comm, dst, src); err != nil { 208 return fmt.Errorf("Error uploading main playbook: %s", err) 209 } 210 } else if err := p.provisionPlaybookFiles(ui, comm); err != nil { 211 return err 212 } 213 214 if len(p.config.InventoryFile) == 0 { 215 tf, err := ioutil.TempFile("", "packer-provisioner-ansible-local") 216 if err != nil { 217 return fmt.Errorf("Error preparing inventory file: %s", err) 218 } 219 defer os.Remove(tf.Name()) 220 if len(p.config.InventoryGroups) != 0 { 221 content := "" 222 for _, group := range p.config.InventoryGroups { 223 content += fmt.Sprintf("[%s]\n127.0.0.1\n", group) 224 } 225 _, err = tf.Write([]byte(content)) 226 } else { 227 _, err = tf.Write([]byte("127.0.0.1")) 228 } 229 if err != nil { 230 tf.Close() 231 return fmt.Errorf("Error preparing inventory file: %s", err) 232 } 233 tf.Close() 234 p.config.InventoryFile = tf.Name() 235 defer func() { 236 p.config.InventoryFile = "" 237 }() 238 } 239 240 if len(p.config.GalaxyFile) > 0 { 241 ui.Message("Uploading galaxy file...") 242 src := p.config.GalaxyFile 243 dst := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(src))) 244 if err := p.uploadFile(ui, comm, dst, src); err != nil { 245 return fmt.Errorf("Error uploading galaxy file: %s", err) 246 } 247 } 248 249 ui.Message("Uploading inventory file...") 250 src := p.config.InventoryFile 251 dst := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(src))) 252 if err := p.uploadFile(ui, comm, dst, src); err != nil { 253 return fmt.Errorf("Error uploading inventory file: %s", err) 254 } 255 256 if len(p.config.GroupVars) > 0 { 257 ui.Message("Uploading group_vars directory...") 258 src := p.config.GroupVars 259 dst := filepath.ToSlash(filepath.Join(p.config.StagingDir, "group_vars")) 260 if err := p.uploadDir(ui, comm, dst, src); err != nil { 261 return fmt.Errorf("Error uploading group_vars directory: %s", err) 262 } 263 } 264 265 if len(p.config.HostVars) > 0 { 266 ui.Message("Uploading host_vars directory...") 267 src := p.config.HostVars 268 dst := filepath.ToSlash(filepath.Join(p.config.StagingDir, "host_vars")) 269 if err := p.uploadDir(ui, comm, dst, src); err != nil { 270 return fmt.Errorf("Error uploading host_vars directory: %s", err) 271 } 272 } 273 274 if len(p.config.RolePaths) > 0 { 275 ui.Message("Uploading role directories...") 276 for _, src := range p.config.RolePaths { 277 dst := filepath.ToSlash(filepath.Join(p.config.StagingDir, "roles", filepath.Base(src))) 278 if err := p.uploadDir(ui, comm, dst, src); err != nil { 279 return fmt.Errorf("Error uploading roles: %s", err) 280 } 281 } 282 } 283 284 if len(p.config.PlaybookPaths) > 0 { 285 ui.Message("Uploading additional Playbooks...") 286 playbookDir := filepath.ToSlash(filepath.Join(p.config.StagingDir, "playbooks")) 287 if err := p.createDir(ui, comm, playbookDir); err != nil { 288 return fmt.Errorf("Error creating playbooks directory: %s", err) 289 } 290 for _, src := range p.config.PlaybookPaths { 291 dst := filepath.ToSlash(filepath.Join(playbookDir, filepath.Base(src))) 292 if err := p.uploadDir(ui, comm, dst, src); err != nil { 293 return fmt.Errorf("Error uploading playbooks: %s", err) 294 } 295 } 296 } 297 298 if err := p.executeAnsible(ui, comm); err != nil { 299 return fmt.Errorf("Error executing Ansible: %s", err) 300 } 301 302 if p.config.CleanStagingDir { 303 ui.Message("Removing staging directory...") 304 if err := p.removeDir(ui, comm, p.config.StagingDir); err != nil { 305 return fmt.Errorf("Error removing staging directory: %s", err) 306 } 307 } 308 return nil 309 } 310 311 func (p *Provisioner) Cancel() { 312 // Just hard quit. It isn't a big deal if what we're doing keeps 313 // running on the other side. 314 os.Exit(0) 315 } 316 317 func (p *Provisioner) provisionPlaybookFiles(ui packer.Ui, comm packer.Communicator) error { 318 var playbookDir string 319 if p.config.PlaybookDir != "" { 320 var err error 321 playbookDir, err = filepath.Abs(p.config.PlaybookDir) 322 if err != nil { 323 return err 324 } 325 } 326 for index, playbookFile := range p.playbookFiles { 327 if playbookDir != "" && strings.HasPrefix(playbookFile, playbookDir) { 328 p.playbookFiles[index] = strings.TrimPrefix(playbookFile, playbookDir) 329 continue 330 } 331 if err := p.provisionPlaybookFile(ui, comm, playbookFile); err != nil { 332 return err 333 } 334 } 335 return nil 336 } 337 338 func (p *Provisioner) provisionPlaybookFile(ui packer.Ui, comm packer.Communicator, playbookFile string) error { 339 ui.Message(fmt.Sprintf("Uploading playbook file: %s", playbookFile)) 340 341 remoteDir := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Dir(playbookFile))) 342 remotePlaybookFile := filepath.ToSlash(filepath.Join(p.config.StagingDir, playbookFile)) 343 344 if err := p.createDir(ui, comm, remoteDir); err != nil { 345 return fmt.Errorf("Error uploading playbook file: %s [%s]", playbookFile, err) 346 } 347 348 if err := p.uploadFile(ui, comm, remotePlaybookFile, playbookFile); err != nil { 349 return fmt.Errorf("Error uploading playbook: %s [%s]", playbookFile, err) 350 } 351 352 return nil 353 } 354 355 func (p *Provisioner) executeGalaxy(ui packer.Ui, comm packer.Communicator) error { 356 rolesDir := filepath.ToSlash(filepath.Join(p.config.StagingDir, "roles")) 357 galaxyFile := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(p.config.GalaxyFile))) 358 359 // ansible-galaxy install -r requirements.yml -p roles/ 360 command := fmt.Sprintf("cd %s && %s install -r %s -p %s", 361 p.config.StagingDir, p.config.GalaxyCommand, galaxyFile, rolesDir) 362 ui.Message(fmt.Sprintf("Executing Ansible Galaxy: %s", command)) 363 cmd := &packer.RemoteCmd{ 364 Command: command, 365 } 366 if err := cmd.StartWithUi(comm, ui); err != nil { 367 return err 368 } 369 if cmd.ExitStatus != 0 { 370 // ansible-galaxy version 2.0.0.2 doesn't return exit codes on error.. 371 return fmt.Errorf("Non-zero exit status: %d", cmd.ExitStatus) 372 } 373 return nil 374 } 375 376 func (p *Provisioner) executeAnsible(ui packer.Ui, comm packer.Communicator) error { 377 inventory := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(p.config.InventoryFile))) 378 379 extraArgs := fmt.Sprintf(" --extra-vars \"packer_build_name=%s packer_builder_type=%s packer_http_addr=%s\" ", 380 p.config.PackerBuildName, p.config.PackerBuilderType, common.GetHTTPAddr()) 381 if len(p.config.ExtraArguments) > 0 { 382 extraArgs = extraArgs + strings.Join(p.config.ExtraArguments, " ") 383 } 384 385 // Fetch external dependencies 386 if len(p.config.GalaxyFile) > 0 { 387 if err := p.executeGalaxy(ui, comm); err != nil { 388 return fmt.Errorf("Error executing Ansible Galaxy: %s", err) 389 } 390 } 391 392 if p.config.PlaybookFile != "" { 393 playbookFile := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(p.config.PlaybookFile))) 394 if err := p.executeAnsiblePlaybook(ui, comm, playbookFile, extraArgs, inventory); err != nil { 395 return err 396 } 397 } 398 399 for _, playbookFile := range p.playbookFiles { 400 playbookFile = filepath.ToSlash(filepath.Join(p.config.StagingDir, playbookFile)) 401 if err := p.executeAnsiblePlaybook(ui, comm, playbookFile, extraArgs, inventory); err != nil { 402 return err 403 } 404 } 405 return nil 406 } 407 408 func (p *Provisioner) executeAnsiblePlaybook( 409 ui packer.Ui, comm packer.Communicator, playbookFile, extraArgs, inventory string, 410 ) error { 411 command := fmt.Sprintf("cd %s && %s %s%s -c local -i %s", 412 p.config.StagingDir, p.config.Command, playbookFile, extraArgs, inventory, 413 ) 414 ui.Message(fmt.Sprintf("Executing Ansible: %s", command)) 415 cmd := &packer.RemoteCmd{ 416 Command: command, 417 } 418 if err := cmd.StartWithUi(comm, ui); err != nil { 419 return err 420 } 421 if cmd.ExitStatus != 0 { 422 if cmd.ExitStatus == 127 { 423 return fmt.Errorf("%s could not be found. Verify that it is available on the\n"+ 424 "PATH after connecting to the machine.", 425 p.config.Command) 426 } 427 428 return fmt.Errorf("Non-zero exit status: %d", cmd.ExitStatus) 429 } 430 return nil 431 } 432 433 func validateDirConfig(path string, config string) error { 434 info, err := os.Stat(path) 435 if err != nil { 436 return fmt.Errorf("%s: %s is invalid: %s", config, path, err) 437 } else if !info.IsDir() { 438 return fmt.Errorf("%s: %s must point to a directory", config, path) 439 } 440 return nil 441 } 442 443 func validateFileConfig(name string, config string, req bool) error { 444 if req { 445 if name == "" { 446 return fmt.Errorf("%s must be specified.", config) 447 } 448 } 449 info, err := os.Stat(name) 450 if err != nil { 451 return fmt.Errorf("%s: %s is invalid: %s", config, name, err) 452 } else if info.IsDir() { 453 return fmt.Errorf("%s: %s must point to a file", config, name) 454 } 455 return nil 456 } 457 458 func (p *Provisioner) uploadFile(ui packer.Ui, comm packer.Communicator, dst, src string) error { 459 f, err := os.Open(src) 460 if err != nil { 461 return fmt.Errorf("Error opening: %s", err) 462 } 463 defer f.Close() 464 465 if err = comm.Upload(dst, f, nil); err != nil { 466 return fmt.Errorf("Error uploading %s: %s", src, err) 467 } 468 return nil 469 } 470 471 func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error { 472 cmd := &packer.RemoteCmd{ 473 Command: fmt.Sprintf("mkdir -p '%s'", dir), 474 } 475 476 ui.Message(fmt.Sprintf("Creating directory: %s", dir)) 477 if err := cmd.StartWithUi(comm, ui); err != nil { 478 return err 479 } 480 481 if cmd.ExitStatus != 0 { 482 return fmt.Errorf("Non-zero exit status. See output above for more information.") 483 } 484 return nil 485 } 486 487 func (p *Provisioner) removeDir(ui packer.Ui, comm packer.Communicator, dir string) error { 488 cmd := &packer.RemoteCmd{ 489 Command: fmt.Sprintf("rm -rf '%s'", dir), 490 } 491 492 ui.Message(fmt.Sprintf("Removing directory: %s", dir)) 493 if err := cmd.StartWithUi(comm, ui); err != nil { 494 return err 495 } 496 497 if cmd.ExitStatus != 0 { 498 return fmt.Errorf("Non-zero exit status. See output above for more information.") 499 } 500 return nil 501 } 502 503 func (p *Provisioner) uploadDir(ui packer.Ui, comm packer.Communicator, dst, src string) error { 504 if err := p.createDir(ui, comm, dst); err != nil { 505 return err 506 } 507 508 // Make sure there is a trailing "/" so that the directory isn't 509 // created on the other side. 510 if src[len(src)-1] != '/' { 511 src = src + "/" 512 } 513 return comm.UploadDir(dst, src, nil) 514 }