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