github.com/mmcquillan/packer@v1.1.1-0.20171009221028-c85cf0483a5d/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 // An array of local paths of playbook files to upload. 42 PlaybookPaths []string `mapstructure:"playbook_paths"` 43 44 // An array of local paths of roles to upload. 45 RolePaths []string `mapstructure:"role_paths"` 46 47 // The directory where files will be uploaded. Packer requires write 48 // permissions in this directory. 49 StagingDir string `mapstructure:"staging_directory"` 50 51 // The optional inventory file 52 InventoryFile string `mapstructure:"inventory_file"` 53 54 // The optional inventory groups 55 InventoryGroups []string `mapstructure:"inventory_groups"` 56 57 // The optional ansible-galaxy requirements file 58 GalaxyFile string `mapstructure:"galaxy_file"` 59 60 // The command to run ansible-galaxy 61 GalaxyCommand string 62 } 63 64 type Provisioner struct { 65 config Config 66 } 67 68 func (p *Provisioner) Prepare(raws ...interface{}) error { 69 err := config.Decode(&p.config, &config.DecodeOpts{ 70 Interpolate: true, 71 InterpolateContext: &p.config.ctx, 72 InterpolateFilter: &interpolate.RenderFilter{ 73 Exclude: []string{}, 74 }, 75 }, raws...) 76 if err != nil { 77 return err 78 } 79 80 // Defaults 81 if p.config.Command == "" { 82 p.config.Command = "ANSIBLE_FORCE_COLOR=1 PYTHONUNBUFFERED=1 ansible-playbook" 83 } 84 if p.config.GalaxyCommand == "" { 85 p.config.GalaxyCommand = "ansible-galaxy" 86 } 87 88 if p.config.StagingDir == "" { 89 p.config.StagingDir = filepath.ToSlash(filepath.Join(DefaultStagingDir, uuid.TimeOrderedUUID())) 90 } 91 92 // Validation 93 var errs *packer.MultiError 94 err = validateFileConfig(p.config.PlaybookFile, "playbook_file", true) 95 if err != nil { 96 errs = packer.MultiErrorAppend(errs, err) 97 } 98 99 // Check that the inventory file exists, if configured 100 if len(p.config.InventoryFile) > 0 { 101 err = validateFileConfig(p.config.InventoryFile, "inventory_file", true) 102 if err != nil { 103 errs = packer.MultiErrorAppend(errs, err) 104 } 105 } 106 107 // Check that the galaxy file exists, if configured 108 if len(p.config.GalaxyFile) > 0 { 109 err = validateFileConfig(p.config.GalaxyFile, "galaxy_file", true) 110 if err != nil { 111 errs = packer.MultiErrorAppend(errs, err) 112 } 113 } 114 115 // Check that the playbook_dir directory exists, if configured 116 if len(p.config.PlaybookDir) > 0 { 117 if err := validateDirConfig(p.config.PlaybookDir, "playbook_dir"); err != nil { 118 errs = packer.MultiErrorAppend(errs, err) 119 } 120 } 121 122 // Check that the group_vars directory exists, if configured 123 if len(p.config.GroupVars) > 0 { 124 if err := validateDirConfig(p.config.GroupVars, "group_vars"); err != nil { 125 errs = packer.MultiErrorAppend(errs, err) 126 } 127 } 128 129 // Check that the host_vars directory exists, if configured 130 if len(p.config.HostVars) > 0 { 131 if err := validateDirConfig(p.config.HostVars, "host_vars"); err != nil { 132 errs = packer.MultiErrorAppend(errs, err) 133 } 134 } 135 136 for _, path := range p.config.PlaybookPaths { 137 err := validateDirConfig(path, "playbook_paths") 138 if err != nil { 139 errs = packer.MultiErrorAppend(errs, err) 140 } 141 } 142 for _, path := range p.config.RolePaths { 143 if err := validateDirConfig(path, "role_paths"); err != nil { 144 errs = packer.MultiErrorAppend(errs, err) 145 } 146 } 147 148 if errs != nil && len(errs.Errors) > 0 { 149 return errs 150 } 151 return nil 152 } 153 154 func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { 155 ui.Say("Provisioning with Ansible...") 156 157 if len(p.config.PlaybookDir) > 0 { 158 ui.Message("Uploading Playbook directory to Ansible staging directory...") 159 if err := p.uploadDir(ui, comm, p.config.StagingDir, p.config.PlaybookDir); err != nil { 160 return fmt.Errorf("Error uploading playbook_dir directory: %s", err) 161 } 162 } else { 163 ui.Message("Creating Ansible staging directory...") 164 if err := p.createDir(ui, comm, p.config.StagingDir); err != nil { 165 return fmt.Errorf("Error creating staging directory: %s", err) 166 } 167 } 168 169 ui.Message("Uploading main Playbook file...") 170 src := p.config.PlaybookFile 171 dst := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(src))) 172 if err := p.uploadFile(ui, comm, dst, src); err != nil { 173 return fmt.Errorf("Error uploading main playbook: %s", err) 174 } 175 176 if len(p.config.InventoryFile) == 0 { 177 tf, err := ioutil.TempFile("", "packer-provisioner-ansible-local") 178 if err != nil { 179 return fmt.Errorf("Error preparing inventory file: %s", err) 180 } 181 defer os.Remove(tf.Name()) 182 if len(p.config.InventoryGroups) != 0 { 183 content := "" 184 for _, group := range p.config.InventoryGroups { 185 content += fmt.Sprintf("[%s]\n127.0.0.1\n", group) 186 } 187 _, err = tf.Write([]byte(content)) 188 } else { 189 _, err = tf.Write([]byte("127.0.0.1")) 190 } 191 if err != nil { 192 tf.Close() 193 return fmt.Errorf("Error preparing inventory file: %s", err) 194 } 195 tf.Close() 196 p.config.InventoryFile = tf.Name() 197 defer func() { 198 p.config.InventoryFile = "" 199 }() 200 } 201 202 if len(p.config.GalaxyFile) > 0 { 203 ui.Message("Uploading galaxy file...") 204 src = p.config.GalaxyFile 205 dst = filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(src))) 206 if err := p.uploadFile(ui, comm, dst, src); err != nil { 207 return fmt.Errorf("Error uploading galaxy file: %s", err) 208 } 209 } 210 211 ui.Message("Uploading inventory file...") 212 src = p.config.InventoryFile 213 dst = filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(src))) 214 if err := p.uploadFile(ui, comm, dst, src); err != nil { 215 return fmt.Errorf("Error uploading inventory file: %s", err) 216 } 217 218 if len(p.config.GroupVars) > 0 { 219 ui.Message("Uploading group_vars directory...") 220 src := p.config.GroupVars 221 dst := filepath.ToSlash(filepath.Join(p.config.StagingDir, "group_vars")) 222 if err := p.uploadDir(ui, comm, dst, src); err != nil { 223 return fmt.Errorf("Error uploading group_vars directory: %s", err) 224 } 225 } 226 227 if len(p.config.HostVars) > 0 { 228 ui.Message("Uploading host_vars directory...") 229 src := p.config.HostVars 230 dst := filepath.ToSlash(filepath.Join(p.config.StagingDir, "host_vars")) 231 if err := p.uploadDir(ui, comm, dst, src); err != nil { 232 return fmt.Errorf("Error uploading host_vars directory: %s", err) 233 } 234 } 235 236 if len(p.config.RolePaths) > 0 { 237 ui.Message("Uploading role directories...") 238 for _, src := range p.config.RolePaths { 239 dst := filepath.ToSlash(filepath.Join(p.config.StagingDir, "roles", filepath.Base(src))) 240 if err := p.uploadDir(ui, comm, dst, src); err != nil { 241 return fmt.Errorf("Error uploading roles: %s", err) 242 } 243 } 244 } 245 246 if len(p.config.PlaybookPaths) > 0 { 247 ui.Message("Uploading additional Playbooks...") 248 playbookDir := filepath.ToSlash(filepath.Join(p.config.StagingDir, "playbooks")) 249 if err := p.createDir(ui, comm, playbookDir); err != nil { 250 return fmt.Errorf("Error creating playbooks directory: %s", err) 251 } 252 for _, src := range p.config.PlaybookPaths { 253 dst := filepath.ToSlash(filepath.Join(playbookDir, filepath.Base(src))) 254 if err := p.uploadDir(ui, comm, dst, src); err != nil { 255 return fmt.Errorf("Error uploading playbooks: %s", err) 256 } 257 } 258 } 259 260 if err := p.executeAnsible(ui, comm); err != nil { 261 return fmt.Errorf("Error executing Ansible: %s", err) 262 } 263 return nil 264 } 265 266 func (p *Provisioner) Cancel() { 267 // Just hard quit. It isn't a big deal if what we're doing keeps 268 // running on the other side. 269 os.Exit(0) 270 } 271 272 func (p *Provisioner) executeGalaxy(ui packer.Ui, comm packer.Communicator) error { 273 rolesDir := filepath.ToSlash(filepath.Join(p.config.StagingDir, "roles")) 274 galaxyFile := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(p.config.GalaxyFile))) 275 276 // ansible-galaxy install -r requirements.yml -p roles/ 277 command := fmt.Sprintf("cd %s && %s install -r %s -p %s", 278 p.config.StagingDir, p.config.GalaxyCommand, galaxyFile, rolesDir) 279 ui.Message(fmt.Sprintf("Executing Ansible Galaxy: %s", command)) 280 cmd := &packer.RemoteCmd{ 281 Command: command, 282 } 283 if err := cmd.StartWithUi(comm, ui); err != nil { 284 return err 285 } 286 if cmd.ExitStatus != 0 { 287 // ansible-galaxy version 2.0.0.2 doesn't return exit codes on error.. 288 return fmt.Errorf("Non-zero exit status: %d", cmd.ExitStatus) 289 } 290 return nil 291 } 292 293 func (p *Provisioner) executeAnsible(ui packer.Ui, comm packer.Communicator) error { 294 playbook := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(p.config.PlaybookFile))) 295 inventory := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(p.config.InventoryFile))) 296 297 extraArgs := fmt.Sprintf(" --extra-vars \"packer_build_name=%s packer_builder_type=%s packer_http_addr=%s\" ", 298 p.config.PackerBuildName, p.config.PackerBuilderType, common.GetHTTPAddr()) 299 if len(p.config.ExtraArguments) > 0 { 300 extraArgs = extraArgs + strings.Join(p.config.ExtraArguments, " ") 301 } 302 303 // Fetch external dependencies 304 if len(p.config.GalaxyFile) > 0 { 305 if err := p.executeGalaxy(ui, comm); err != nil { 306 return fmt.Errorf("Error executing Ansible Galaxy: %s", err) 307 } 308 } 309 310 command := fmt.Sprintf("cd %s && %s %s%s -c local -i %s", 311 p.config.StagingDir, p.config.Command, playbook, extraArgs, inventory) 312 ui.Message(fmt.Sprintf("Executing Ansible: %s", command)) 313 cmd := &packer.RemoteCmd{ 314 Command: command, 315 } 316 if err := cmd.StartWithUi(comm, ui); err != nil { 317 return err 318 } 319 if cmd.ExitStatus != 0 { 320 if cmd.ExitStatus == 127 { 321 return fmt.Errorf("%s could not be found. Verify that it is available on the\n"+ 322 "PATH after connecting to the machine.", 323 p.config.Command) 324 } 325 326 return fmt.Errorf("Non-zero exit status: %d", cmd.ExitStatus) 327 } 328 return nil 329 } 330 331 func validateDirConfig(path string, config string) error { 332 info, err := os.Stat(path) 333 if err != nil { 334 return fmt.Errorf("%s: %s is invalid: %s", config, path, err) 335 } else if !info.IsDir() { 336 return fmt.Errorf("%s: %s must point to a directory", config, path) 337 } 338 return nil 339 } 340 341 func validateFileConfig(name string, config string, req bool) error { 342 if req { 343 if name == "" { 344 return fmt.Errorf("%s must be specified.", config) 345 } 346 } 347 info, err := os.Stat(name) 348 if err != nil { 349 return fmt.Errorf("%s: %s is invalid: %s", config, name, err) 350 } else if info.IsDir() { 351 return fmt.Errorf("%s: %s must point to a file", config, name) 352 } 353 return nil 354 } 355 356 func (p *Provisioner) uploadFile(ui packer.Ui, comm packer.Communicator, dst, src string) error { 357 f, err := os.Open(src) 358 if err != nil { 359 return fmt.Errorf("Error opening: %s", err) 360 } 361 defer f.Close() 362 363 if err = comm.Upload(dst, f, nil); err != nil { 364 return fmt.Errorf("Error uploading %s: %s", src, err) 365 } 366 return nil 367 } 368 369 func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error { 370 ui.Message(fmt.Sprintf("Creating directory: %s", dir)) 371 cmd := &packer.RemoteCmd{ 372 Command: fmt.Sprintf("mkdir -p '%s'", dir), 373 } 374 if err := cmd.StartWithUi(comm, ui); err != nil { 375 return err 376 } 377 if cmd.ExitStatus != 0 { 378 return fmt.Errorf("Non-zero exit status.") 379 } 380 return nil 381 } 382 383 func (p *Provisioner) uploadDir(ui packer.Ui, comm packer.Communicator, dst, src string) error { 384 if err := p.createDir(ui, comm, dst); err != nil { 385 return err 386 } 387 388 // Make sure there is a trailing "/" so that the directory isn't 389 // created on the other side. 390 if src[len(src)-1] != '/' { 391 src = src + "/" 392 } 393 return comm.UploadDir(dst, src, nil) 394 }