github.com/ddnomad/packer@v1.3.2/provisioner/puppet-masterless/provisioner.go (about) 1 // Package puppetmasterless implements a provisioner for Packer that executes 2 // Puppet on the remote machine, configured to apply a local manifest 3 // versus connecting to a Puppet master. 4 package puppetmasterless 5 6 import ( 7 "fmt" 8 "os" 9 "path/filepath" 10 "strings" 11 12 "github.com/hashicorp/packer/common" 13 "github.com/hashicorp/packer/helper/config" 14 "github.com/hashicorp/packer/packer" 15 "github.com/hashicorp/packer/provisioner" 16 "github.com/hashicorp/packer/template/interpolate" 17 ) 18 19 type Config struct { 20 common.PackerConfig `mapstructure:",squash"` 21 ctx interpolate.Context 22 23 // If true, staging directory is removed after executing puppet. 24 CleanStagingDir bool `mapstructure:"clean_staging_directory"` 25 26 // The Guest OS Type (unix or windows) 27 GuestOSType string `mapstructure:"guest_os_type"` 28 29 // The command used to execute Puppet. 30 ExecuteCommand string `mapstructure:"execute_command"` 31 32 // Additional arguments to pass when executing Puppet 33 ExtraArguments []string `mapstructure:"extra_arguments"` 34 35 // Additional facts to set when executing Puppet 36 Facter map[string]string 37 38 // Path to a hiera configuration file to upload and use. 39 HieraConfigPath string `mapstructure:"hiera_config_path"` 40 41 // If true, packer will ignore all exit-codes from a puppet run 42 IgnoreExitCodes bool `mapstructure:"ignore_exit_codes"` 43 44 // An array of local paths of modules to upload. 45 ModulePaths []string `mapstructure:"module_paths"` 46 47 // The main manifest file to apply to kick off the entire thing. 48 ManifestFile string `mapstructure:"manifest_file"` 49 50 // A directory of manifest files that will be uploaded to the remote 51 // machine. 52 ManifestDir string `mapstructure:"manifest_dir"` 53 54 // If true, `sudo` will NOT be used to execute Puppet. 55 PreventSudo bool `mapstructure:"prevent_sudo"` 56 57 // The directory that contains the puppet binary. 58 // E.g. if it can't be found on the standard path. 59 PuppetBinDir string `mapstructure:"puppet_bin_dir"` 60 61 // The directory where files will be uploaded. Packer requires write 62 // permissions in this directory. 63 StagingDir string `mapstructure:"staging_directory"` 64 65 // The directory from which the command will be executed. 66 // Packer requires the directory to exist when running puppet. 67 WorkingDir string `mapstructure:"working_directory"` 68 } 69 70 type guestOSTypeConfig struct { 71 executeCommand string 72 facterVarsFmt string 73 facterVarsJoiner string 74 modulePathJoiner string 75 stagingDir string 76 tempDir string 77 } 78 79 // FIXME assumes both Packer host and target are same OS 80 var guestOSTypeConfigs = map[string]guestOSTypeConfig{ 81 provisioner.UnixOSType: { 82 tempDir: "/tmp", 83 stagingDir: "/tmp/packer-puppet-masterless", 84 executeCommand: "cd {{.WorkingDir}} && " + 85 `{{if ne .FacterVars ""}}{{.FacterVars}} {{end}}` + 86 "{{if .Sudo}}sudo -E {{end}}" + 87 `{{if ne .PuppetBinDir ""}}{{.PuppetBinDir}}/{{end}}` + 88 "puppet apply --detailed-exitcodes " + 89 "{{if .Debug}}--debug {{end}}" + 90 `{{if ne .ModulePath ""}}--modulepath='{{.ModulePath}}' {{end}}` + 91 `{{if ne .HieraConfigPath ""}}--hiera_config='{{.HieraConfigPath}}' {{end}}` + 92 `{{if ne .ManifestDir ""}}--manifestdir='{{.ManifestDir}}' {{end}}` + 93 `{{if ne .ExtraArguments ""}}{{.ExtraArguments}} {{end}}` + 94 "{{.ManifestFile}}", 95 facterVarsFmt: "FACTER_%s='%s'", 96 facterVarsJoiner: " ", 97 modulePathJoiner: ":", 98 }, 99 provisioner.WindowsOSType: { 100 tempDir: filepath.ToSlash(os.Getenv("TEMP")), 101 stagingDir: filepath.ToSlash(os.Getenv("SYSTEMROOT")) + "/Temp/packer-puppet-masterless", 102 executeCommand: "cd {{.WorkingDir}} && " + 103 `{{if ne .FacterVars ""}}{{.FacterVars}} && {{end}}` + 104 `{{if ne .PuppetBinDir ""}}{{.PuppetBinDir}}/{{end}}` + 105 "puppet apply --detailed-exitcodes " + 106 "{{if .Debug}}--debug {{end}}" + 107 `{{if ne .ModulePath ""}}--modulepath='{{.ModulePath}}' {{end}}` + 108 `{{if ne .HieraConfigPath ""}}--hiera_config='{{.HieraConfigPath}}' {{end}}` + 109 `{{if ne .ManifestDir ""}}--manifestdir='{{.ManifestDir}}' {{end}}` + 110 `{{if ne .ExtraArguments ""}}{{.ExtraArguments}} {{end}}` + 111 "{{.ManifestFile}}", 112 facterVarsFmt: `SET "FACTER_%s=%s"`, 113 facterVarsJoiner: " & ", 114 modulePathJoiner: ";", 115 }, 116 } 117 118 type Provisioner struct { 119 config Config 120 guestOSTypeConfig guestOSTypeConfig 121 guestCommands *provisioner.GuestCommands 122 } 123 124 type ExecuteTemplate struct { 125 Debug bool 126 ExtraArguments string 127 FacterVars string 128 HieraConfigPath string 129 ModulePath string 130 ModulePathJoiner string 131 ManifestFile string 132 ManifestDir string 133 PuppetBinDir string 134 Sudo bool 135 WorkingDir string 136 } 137 138 func (p *Provisioner) Prepare(raws ...interface{}) error { 139 err := config.Decode(&p.config, &config.DecodeOpts{ 140 Interpolate: true, 141 InterpolateContext: &p.config.ctx, 142 InterpolateFilter: &interpolate.RenderFilter{ 143 Exclude: []string{ 144 "execute_command", 145 "extra_arguments", 146 }, 147 }, 148 }, raws...) 149 if err != nil { 150 return err 151 } 152 153 // Set some defaults 154 if p.config.GuestOSType == "" { 155 p.config.GuestOSType = provisioner.DefaultOSType 156 } 157 p.config.GuestOSType = strings.ToLower(p.config.GuestOSType) 158 159 var ok bool 160 p.guestOSTypeConfig, ok = guestOSTypeConfigs[p.config.GuestOSType] 161 if !ok { 162 return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType) 163 } 164 165 p.guestCommands, err = provisioner.NewGuestCommands(p.config.GuestOSType, !p.config.PreventSudo) 166 if err != nil { 167 return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType) 168 } 169 170 if p.config.ExecuteCommand == "" { 171 p.config.ExecuteCommand = p.guestOSTypeConfig.executeCommand 172 } 173 174 if p.config.StagingDir == "" { 175 p.config.StagingDir = p.guestOSTypeConfig.stagingDir 176 } 177 178 if p.config.WorkingDir == "" { 179 p.config.WorkingDir = p.config.StagingDir 180 } 181 182 if p.config.Facter == nil { 183 p.config.Facter = make(map[string]string) 184 } 185 p.config.Facter["packer_build_name"] = p.config.PackerBuildName 186 p.config.Facter["packer_builder_type"] = p.config.PackerBuilderType 187 188 // Validation 189 var errs *packer.MultiError 190 if p.config.HieraConfigPath != "" { 191 info, err := os.Stat(p.config.HieraConfigPath) 192 if err != nil { 193 errs = packer.MultiErrorAppend(errs, 194 fmt.Errorf("hiera_config_path is invalid: %s", err)) 195 } else if info.IsDir() { 196 errs = packer.MultiErrorAppend(errs, 197 fmt.Errorf("hiera_config_path must point to a file")) 198 } 199 } 200 201 if p.config.ManifestDir != "" { 202 info, err := os.Stat(p.config.ManifestDir) 203 if err != nil { 204 errs = packer.MultiErrorAppend(errs, 205 fmt.Errorf("manifest_dir is invalid: %s", err)) 206 } else if !info.IsDir() { 207 errs = packer.MultiErrorAppend(errs, 208 fmt.Errorf("manifest_dir must point to a directory")) 209 } 210 } 211 212 if p.config.ManifestFile == "" { 213 errs = packer.MultiErrorAppend(errs, 214 fmt.Errorf("A manifest_file must be specified.")) 215 } else { 216 _, err := os.Stat(p.config.ManifestFile) 217 if err != nil { 218 errs = packer.MultiErrorAppend(errs, 219 fmt.Errorf("manifest_file is invalid: %s", err)) 220 } 221 } 222 223 for i, path := range p.config.ModulePaths { 224 info, err := os.Stat(path) 225 if err != nil { 226 errs = packer.MultiErrorAppend(errs, 227 fmt.Errorf("module_path[%d] is invalid: %s", i, err)) 228 } else if !info.IsDir() { 229 errs = packer.MultiErrorAppend(errs, 230 fmt.Errorf("module_path[%d] must point to a directory", i)) 231 } 232 } 233 234 if errs != nil && len(errs.Errors) > 0 { 235 return errs 236 } 237 238 return nil 239 } 240 241 func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { 242 ui.Say("Provisioning with Puppet...") 243 ui.Message("Creating Puppet staging directory...") 244 if err := p.createDir(ui, comm, p.config.StagingDir); err != nil { 245 return fmt.Errorf("Error creating staging directory: %s", err) 246 } 247 248 // Upload hiera config if set 249 remoteHieraConfigPath := "" 250 if p.config.HieraConfigPath != "" { 251 var err error 252 remoteHieraConfigPath, err = p.uploadHieraConfig(ui, comm) 253 if err != nil { 254 return fmt.Errorf("Error uploading hiera config: %s", err) 255 } 256 } 257 258 // Upload manifest dir if set 259 remoteManifestDir := "" 260 if p.config.ManifestDir != "" { 261 ui.Message(fmt.Sprintf( 262 "Uploading manifest directory from: %s", p.config.ManifestDir)) 263 remoteManifestDir = fmt.Sprintf("%s/manifests", p.config.StagingDir) 264 err := p.uploadDirectory(ui, comm, remoteManifestDir, p.config.ManifestDir) 265 if err != nil { 266 return fmt.Errorf("Error uploading manifest dir: %s", err) 267 } 268 } 269 270 // Upload all modules 271 modulePaths := make([]string, 0, len(p.config.ModulePaths)) 272 for i, path := range p.config.ModulePaths { 273 ui.Message(fmt.Sprintf("Uploading local modules from: %s", path)) 274 targetPath := fmt.Sprintf("%s/module-%d", p.config.StagingDir, i) 275 if err := p.uploadDirectory(ui, comm, targetPath, path); err != nil { 276 return fmt.Errorf("Error uploading modules: %s", err) 277 } 278 279 modulePaths = append(modulePaths, targetPath) 280 } 281 282 // Upload manifests 283 remoteManifestFile, err := p.uploadManifests(ui, comm) 284 if err != nil { 285 return fmt.Errorf("Error uploading manifests: %s", err) 286 } 287 288 // Compile the facter variables 289 facterVars := make([]string, 0, len(p.config.Facter)) 290 for k, v := range p.config.Facter { 291 facterVars = append(facterVars, fmt.Sprintf(p.guestOSTypeConfig.facterVarsFmt, k, v)) 292 } 293 294 data := ExecuteTemplate{ 295 ExtraArguments: "", 296 FacterVars: strings.Join(facterVars, p.guestOSTypeConfig.facterVarsJoiner), 297 HieraConfigPath: remoteHieraConfigPath, 298 ManifestDir: remoteManifestDir, 299 ManifestFile: remoteManifestFile, 300 ModulePath: strings.Join(modulePaths, p.guestOSTypeConfig.modulePathJoiner), 301 ModulePathJoiner: p.guestOSTypeConfig.modulePathJoiner, 302 PuppetBinDir: p.config.PuppetBinDir, 303 Sudo: !p.config.PreventSudo, 304 WorkingDir: p.config.WorkingDir, 305 } 306 307 p.config.ctx.Data = &data 308 _ExtraArguments, err := interpolate.Render(strings.Join(p.config.ExtraArguments, " "), &p.config.ctx) 309 if err != nil { 310 return err 311 } 312 data.ExtraArguments = _ExtraArguments 313 314 command, err := interpolate.Render(p.config.ExecuteCommand, &p.config.ctx) 315 if err != nil { 316 return err 317 } 318 319 cmd := &packer.RemoteCmd{ 320 Command: command, 321 } 322 323 ui.Message(fmt.Sprintf("Running Puppet: %s", command)) 324 if err := cmd.StartWithUi(comm, ui); err != nil { 325 return fmt.Errorf("Got an error starting command: %s", err) 326 } 327 328 if cmd.ExitStatus != 0 && cmd.ExitStatus != 2 && !p.config.IgnoreExitCodes { 329 return fmt.Errorf("Puppet exited with a non-zero exit status: %d", cmd.ExitStatus) 330 } 331 332 if p.config.CleanStagingDir { 333 if err := p.removeDir(ui, comm, p.config.StagingDir); err != nil { 334 return fmt.Errorf("Error removing staging directory: %s", err) 335 } 336 } 337 338 return nil 339 } 340 341 func (p *Provisioner) Cancel() { 342 // Just hard quit. It isn't a big deal if what we're doing keeps 343 // running on the other side. 344 os.Exit(0) 345 } 346 347 func (p *Provisioner) uploadHieraConfig(ui packer.Ui, comm packer.Communicator) (string, error) { 348 ui.Message("Uploading hiera configuration...") 349 f, err := os.Open(p.config.HieraConfigPath) 350 if err != nil { 351 return "", err 352 } 353 defer f.Close() 354 355 path := fmt.Sprintf("%s/hiera.yaml", p.config.StagingDir) 356 if err := comm.Upload(path, f, nil); err != nil { 357 return "", err 358 } 359 360 return path, nil 361 } 362 363 func (p *Provisioner) uploadManifests(ui packer.Ui, comm packer.Communicator) (string, error) { 364 // Create the remote manifests directory... 365 ui.Message("Uploading manifests...") 366 remoteManifestsPath := fmt.Sprintf("%s/manifests", p.config.StagingDir) 367 if err := p.createDir(ui, comm, remoteManifestsPath); err != nil { 368 return "", fmt.Errorf("Error creating manifests directory: %s", err) 369 } 370 371 // NOTE! manifest_file may either be a directory or a file, as puppet apply 372 // now accepts either one. 373 374 fi, err := os.Stat(p.config.ManifestFile) 375 if err != nil { 376 return "", fmt.Errorf("Error inspecting manifest file: %s", err) 377 } 378 379 if fi.IsDir() { 380 // If manifest_file is a directory we'll upload the whole thing 381 ui.Message(fmt.Sprintf( 382 "Uploading manifest directory from: %s", p.config.ManifestFile)) 383 384 remoteManifestDir := fmt.Sprintf("%s/manifests", p.config.StagingDir) 385 err := p.uploadDirectory(ui, comm, remoteManifestDir, p.config.ManifestFile) 386 if err != nil { 387 return "", fmt.Errorf("Error uploading manifest dir: %s", err) 388 } 389 return remoteManifestDir, nil 390 } 391 // Otherwise manifest_file is a file and we'll upload it 392 ui.Message(fmt.Sprintf( 393 "Uploading manifest file from: %s", p.config.ManifestFile)) 394 395 f, err := os.Open(p.config.ManifestFile) 396 if err != nil { 397 return "", err 398 } 399 defer f.Close() 400 401 manifestFilename := filepath.Base(p.config.ManifestFile) 402 remoteManifestFile := fmt.Sprintf("%s/%s", remoteManifestsPath, manifestFilename) 403 if err := comm.Upload(remoteManifestFile, f, nil); err != nil { 404 return "", err 405 } 406 return remoteManifestFile, nil 407 } 408 409 func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error { 410 ui.Message(fmt.Sprintf("Creating directory: %s", dir)) 411 412 cmd := &packer.RemoteCmd{Command: p.guestCommands.CreateDir(dir)} 413 414 if err := cmd.StartWithUi(comm, ui); err != nil { 415 return err 416 } 417 418 if cmd.ExitStatus != 0 { 419 return fmt.Errorf("Non-zero exit status.") 420 } 421 422 // Chmod the directory to 0777 just so that we can access it as our user 423 cmd = &packer.RemoteCmd{Command: p.guestCommands.Chmod(dir, "0777")} 424 if err := cmd.StartWithUi(comm, ui); err != nil { 425 return err 426 } 427 if cmd.ExitStatus != 0 { 428 return fmt.Errorf("Non-zero exit status. See output above for more info.") 429 } 430 431 return nil 432 } 433 434 func (p *Provisioner) removeDir(ui packer.Ui, comm packer.Communicator, dir string) error { 435 cmd := &packer.RemoteCmd{ 436 Command: fmt.Sprintf("rm -fr '%s'", dir), 437 } 438 439 if err := cmd.StartWithUi(comm, ui); err != nil { 440 return err 441 } 442 443 if cmd.ExitStatus != 0 { 444 return fmt.Errorf("Non-zero exit status.") 445 } 446 447 return nil 448 } 449 450 func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, dst string, src string) error { 451 if err := p.createDir(ui, comm, dst); err != nil { 452 return err 453 } 454 455 // Make sure there is a trailing "/" so that the directory isn't 456 // created on the other side. 457 if src[len(src)-1] != '/' { 458 src = src + "/" 459 } 460 461 return comm.UploadDir(dst, src, nil) 462 }