gopkg.in/hashicorp/packer.v1@v1.3.2/provisioner/puppet-server/provisioner.go (about) 1 // Package puppetserver implements a provisioner for Packer that executes 2 // Puppet on the remote machine connecting to a Puppet master. 3 package puppetserver 4 5 import ( 6 "fmt" 7 "os" 8 "path/filepath" 9 "strings" 10 11 "github.com/hashicorp/packer/common" 12 "github.com/hashicorp/packer/helper/config" 13 "github.com/hashicorp/packer/packer" 14 "github.com/hashicorp/packer/provisioner" 15 "github.com/hashicorp/packer/template/interpolate" 16 ) 17 18 type Config struct { 19 common.PackerConfig `mapstructure:",squash"` 20 ctx interpolate.Context 21 22 // If true, staging directory is removed after executing puppet. 23 CleanStagingDir bool `mapstructure:"clean_staging_directory"` 24 25 // A path to the client certificate 26 ClientCertPath string `mapstructure:"client_cert_path"` 27 28 // A path to a directory containing the client private keys 29 ClientPrivateKeyPath string `mapstructure:"client_private_key_path"` 30 31 // The command used to execute Puppet. 32 ExecuteCommand string `mapstructure:"execute_command"` 33 34 // Additional argument to pass when executing Puppet. 35 ExtraArguments []string `mapstructure:"extra_arguments"` 36 37 // Additional facts to set when executing Puppet 38 Facter map[string]string 39 40 // The Guest OS Type (unix or windows) 41 GuestOSType string `mapstructure:"guest_os_type"` 42 43 // If true, packer will ignore all exit-codes from a puppet run 44 IgnoreExitCodes bool `mapstructure:"ignore_exit_codes"` 45 46 // If true, `sudo` will NOT be used to execute Puppet. 47 PreventSudo bool `mapstructure:"prevent_sudo"` 48 49 // The directory that contains the puppet binary. 50 // E.g. if it can't be found on the standard path. 51 PuppetBinDir string `mapstructure:"puppet_bin_dir"` 52 53 // The hostname of the Puppet node. 54 PuppetNode string `mapstructure:"puppet_node"` 55 56 // The hostname of the Puppet server. 57 PuppetServer string `mapstructure:"puppet_server"` 58 59 // The directory where files will be uploaded. Packer requires write 60 // permissions in this directory. 61 StagingDir string `mapstructure:"staging_dir"` 62 63 // The directory from which the command will be executed. 64 // Packer requires the directory to exist when running puppet. 65 WorkingDir string `mapstructure:"working_directory"` 66 } 67 68 type guestOSTypeConfig struct { 69 executeCommand string 70 facterVarsFmt string 71 facterVarsJoiner string 72 stagingDir string 73 tempDir string 74 } 75 76 // FIXME assumes both Packer host and target are same OS 77 var guestOSTypeConfigs = map[string]guestOSTypeConfig{ 78 provisioner.UnixOSType: { 79 tempDir: "/tmp", 80 stagingDir: "/tmp/packer-puppet-server", 81 executeCommand: "cd {{.WorkingDir}} && " + 82 `{{if ne .FacterVars ""}}{{.FacterVars}} {{end}}` + 83 "{{if .Sudo}}sudo -E {{end}}" + 84 `{{if ne .PuppetBinDir ""}}{{.PuppetBinDir}}/{{end}}` + 85 "puppet agent --onetime --no-daemonize --detailed-exitcodes " + 86 "{{if .Debug}}--debug {{end}}" + 87 `{{if ne .PuppetServer ""}}--server='{{.PuppetServer}}' {{end}}` + 88 `{{if ne .PuppetNode ""}}--certname='{{.PuppetNode}}' {{end}}` + 89 `{{if ne .ClientCertPath ""}}--certdir='{{.ClientCertPath}}' {{end}}` + 90 `{{if ne .ClientPrivateKeyPath ""}}--privatekeydir='{{.ClientPrivateKeyPath}}' {{end}}` + 91 `{{if ne .ExtraArguments ""}}{{.ExtraArguments}} {{end}}`, 92 facterVarsFmt: "FACTER_%s='%s'", 93 facterVarsJoiner: " ", 94 }, 95 provisioner.WindowsOSType: { 96 tempDir: filepath.ToSlash(os.Getenv("TEMP")), 97 stagingDir: filepath.ToSlash(os.Getenv("SYSTEMROOT")) + "/Temp/packer-puppet-server", 98 executeCommand: "cd {{.WorkingDir}} && " + 99 `{{if ne .FacterVars ""}}{{.FacterVars}} && {{end}}` + 100 `{{if ne .PuppetBinDir ""}}{{.PuppetBinDir}}/{{end}}` + 101 "puppet agent --onetime --no-daemonize --detailed-exitcodes " + 102 "{{if .Debug}}--debug {{end}}" + 103 `{{if ne .PuppetServer ""}}--server='{{.PuppetServer}}' {{end}}` + 104 `{{if ne .PuppetNode ""}}--certname='{{.PuppetNode}}' {{end}}` + 105 `{{if ne .ClientCertPath ""}}--certdir='{{.ClientCertPath}}' {{end}}` + 106 `{{if ne .ClientPrivateKeyPath ""}}--privatekeydir='{{.ClientPrivateKeyPath}}' {{end}}` + 107 `{{if ne .ExtraArguments ""}}{{.ExtraArguments}} {{end}}`, 108 facterVarsFmt: `SET "FACTER_%s=%s"`, 109 facterVarsJoiner: " & ", 110 }, 111 } 112 113 type Provisioner struct { 114 config Config 115 guestOSTypeConfig guestOSTypeConfig 116 guestCommands *provisioner.GuestCommands 117 } 118 119 type ExecuteTemplate struct { 120 ClientCertPath string 121 ClientPrivateKeyPath string 122 Debug bool 123 ExtraArguments string 124 FacterVars string 125 PuppetNode string 126 PuppetServer string 127 PuppetBinDir string 128 Sudo bool 129 WorkingDir string 130 } 131 132 func (p *Provisioner) Prepare(raws ...interface{}) error { 133 err := config.Decode(&p.config, &config.DecodeOpts{ 134 Interpolate: true, 135 InterpolateContext: &p.config.ctx, 136 InterpolateFilter: &interpolate.RenderFilter{ 137 Exclude: []string{ 138 "execute_command", 139 "extra_arguments", 140 }, 141 }, 142 }, raws...) 143 if err != nil { 144 return err 145 } 146 147 if p.config.GuestOSType == "" { 148 p.config.GuestOSType = provisioner.DefaultOSType 149 } 150 p.config.GuestOSType = strings.ToLower(p.config.GuestOSType) 151 152 var ok bool 153 p.guestOSTypeConfig, ok = guestOSTypeConfigs[p.config.GuestOSType] 154 if !ok { 155 return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType) 156 } 157 158 p.guestCommands, err = provisioner.NewGuestCommands(p.config.GuestOSType, !p.config.PreventSudo) 159 if err != nil { 160 return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType) 161 } 162 163 if p.config.ExecuteCommand == "" { 164 p.config.ExecuteCommand = p.guestOSTypeConfig.executeCommand 165 } 166 167 if p.config.StagingDir == "" { 168 p.config.StagingDir = p.guestOSTypeConfig.stagingDir 169 } 170 171 if p.config.WorkingDir == "" { 172 p.config.WorkingDir = p.config.StagingDir 173 } 174 175 if p.config.Facter == nil { 176 p.config.Facter = make(map[string]string) 177 } 178 p.config.Facter["packer_build_name"] = p.config.PackerBuildName 179 p.config.Facter["packer_builder_type"] = p.config.PackerBuilderType 180 181 var errs *packer.MultiError 182 if p.config.ClientCertPath != "" { 183 info, err := os.Stat(p.config.ClientCertPath) 184 if err != nil { 185 errs = packer.MultiErrorAppend(errs, 186 fmt.Errorf("client_cert_dir is invalid: %s", err)) 187 } else if !info.IsDir() { 188 errs = packer.MultiErrorAppend(errs, 189 fmt.Errorf("client_cert_dir must point to a directory")) 190 } 191 } 192 193 if p.config.ClientPrivateKeyPath != "" { 194 info, err := os.Stat(p.config.ClientPrivateKeyPath) 195 if err != nil { 196 errs = packer.MultiErrorAppend(errs, 197 fmt.Errorf("client_private_key_dir is invalid: %s", err)) 198 } else if !info.IsDir() { 199 errs = packer.MultiErrorAppend(errs, 200 fmt.Errorf("client_private_key_dir must point to a directory")) 201 } 202 } 203 204 if errs != nil && len(errs.Errors) > 0 { 205 return errs 206 } 207 208 return nil 209 } 210 211 func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { 212 ui.Say("Provisioning with Puppet...") 213 ui.Message("Creating Puppet staging directory...") 214 if err := p.createDir(ui, comm, p.config.StagingDir); err != nil { 215 return fmt.Errorf("Error creating staging directory: %s", err) 216 } 217 218 // Upload client cert dir if set 219 remoteClientCertPath := "" 220 if p.config.ClientCertPath != "" { 221 ui.Message(fmt.Sprintf( 222 "Uploading client cert from: %s", p.config.ClientCertPath)) 223 remoteClientCertPath = fmt.Sprintf("%s/certs", p.config.StagingDir) 224 err := p.uploadDirectory(ui, comm, remoteClientCertPath, p.config.ClientCertPath) 225 if err != nil { 226 return fmt.Errorf("Error uploading client cert: %s", err) 227 } 228 } 229 230 // Upload client cert dir if set 231 remoteClientPrivateKeyPath := "" 232 if p.config.ClientPrivateKeyPath != "" { 233 ui.Message(fmt.Sprintf( 234 "Uploading client private keys from: %s", p.config.ClientPrivateKeyPath)) 235 remoteClientPrivateKeyPath = fmt.Sprintf("%s/private_keys", p.config.StagingDir) 236 err := p.uploadDirectory(ui, comm, remoteClientPrivateKeyPath, p.config.ClientPrivateKeyPath) 237 if err != nil { 238 return fmt.Errorf("Error uploading client private keys: %s", err) 239 } 240 } 241 242 // Compile the facter variables 243 facterVars := make([]string, 0, len(p.config.Facter)) 244 for k, v := range p.config.Facter { 245 facterVars = append(facterVars, fmt.Sprintf(p.guestOSTypeConfig.facterVarsFmt, k, v)) 246 } 247 248 data := ExecuteTemplate{ 249 ClientCertPath: remoteClientCertPath, 250 ClientPrivateKeyPath: remoteClientPrivateKeyPath, 251 ExtraArguments: "", 252 FacterVars: strings.Join(facterVars, p.guestOSTypeConfig.facterVarsJoiner), 253 PuppetNode: p.config.PuppetNode, 254 PuppetServer: p.config.PuppetServer, 255 PuppetBinDir: p.config.PuppetBinDir, 256 Sudo: !p.config.PreventSudo, 257 WorkingDir: p.config.WorkingDir, 258 } 259 260 p.config.ctx.Data = &data 261 _ExtraArguments, err := interpolate.Render(strings.Join(p.config.ExtraArguments, " "), &p.config.ctx) 262 if err != nil { 263 return err 264 } 265 data.ExtraArguments = _ExtraArguments 266 267 command, err := interpolate.Render(p.config.ExecuteCommand, &p.config.ctx) 268 if err != nil { 269 return err 270 } 271 272 cmd := &packer.RemoteCmd{ 273 Command: command, 274 } 275 276 ui.Message(fmt.Sprintf("Running Puppet: %s", command)) 277 if err := cmd.StartWithUi(comm, ui); err != nil { 278 return err 279 } 280 281 if cmd.ExitStatus != 0 && cmd.ExitStatus != 2 && !p.config.IgnoreExitCodes { 282 return fmt.Errorf("Puppet exited with a non-zero exit status: %d", cmd.ExitStatus) 283 } 284 285 if p.config.CleanStagingDir { 286 if err := p.removeDir(ui, comm, p.config.StagingDir); err != nil { 287 return fmt.Errorf("Error removing staging directory: %s", err) 288 } 289 } 290 291 return nil 292 } 293 294 func (p *Provisioner) Cancel() { 295 // Just hard quit. It isn't a big deal if what we're doing keeps 296 // running on the other side. 297 os.Exit(0) 298 } 299 300 func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error { 301 ui.Message(fmt.Sprintf("Creating directory: %s", dir)) 302 303 cmd := &packer.RemoteCmd{Command: p.guestCommands.CreateDir(dir)} 304 if err := cmd.StartWithUi(comm, ui); err != nil { 305 return err 306 } 307 if cmd.ExitStatus != 0 { 308 return fmt.Errorf("Non-zero exit status. See output above for more info.") 309 } 310 311 // Chmod the directory to 0777 just so that we can access it as our user 312 cmd = &packer.RemoteCmd{Command: p.guestCommands.Chmod(dir, "0777")} 313 if err := cmd.StartWithUi(comm, ui); err != nil { 314 return err 315 } 316 if cmd.ExitStatus != 0 { 317 return fmt.Errorf("Non-zero exit status. See output above for more info.") 318 } 319 320 return nil 321 } 322 323 func (p *Provisioner) removeDir(ui packer.Ui, comm packer.Communicator, dir string) error { 324 cmd := &packer.RemoteCmd{ 325 Command: fmt.Sprintf("rm -fr '%s'", dir), 326 } 327 328 if err := cmd.StartWithUi(comm, ui); err != nil { 329 return err 330 } 331 332 if cmd.ExitStatus != 0 { 333 return fmt.Errorf("Non-zero exit status.") 334 } 335 336 return nil 337 } 338 339 func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, dst string, src string) error { 340 if err := p.createDir(ui, comm, dst); err != nil { 341 return err 342 } 343 344 // Make sure there is a trailing "/" so that the directory isn't 345 // created on the other side. 346 if src[len(src)-1] != '/' { 347 src = src + "/" 348 } 349 350 return comm.UploadDir(dst, src, nil) 351 }