packer-cn/provisioner/puppet-server/provisioner.go

391 lines
12 KiB
Go
Raw Normal View History

//go:generate mapstructure-to-hcl2 -type Config
// Package puppetserver implements a provisioner for Packer that executes
// Puppet on the remote machine connecting to a Puppet master.
package puppetserver
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
2015-05-27 17:50:20 -04:00
build using HCL2 (#8423) This follows #8232 which added the code to generate the code required to parse HCL files for each packer component. All old config files of packer will keep on working the same. Packer takes one argument. When a directory is passed, all files in the folder with a name ending with “.pkr.hcl” or “.pkr.json” will be parsed using the HCL2 format. When a file ending with “.pkr.hcl” or “.pkr.json” is passed it will be parsed using the HCL2 format. For every other case; the old packer style will be used. ## 1. the hcl2template pkg can create a packer.Build from a set of HCL (v2) files I had to make the packer.coreBuild (which is our one and only packer.Build ) a public struct with public fields ## 2. Components interfaces get a new ConfigSpec Method to read a file from an HCL file. This is a breaking change for packer plugins. a packer component can be a: builder/provisioner/post-processor each component interface now gets a `ConfigSpec() hcldec.ObjectSpec` which allows packer to tell what is the layout of the hcl2 config meant to configure that specific component. This ObjectSpec is sent through the wire (RPC) and a cty.Value is now sent through the already existing configuration entrypoints: Provisioner.Prepare(raws ...interface{}) error Builder.Prepare(raws ...interface{}) ([]string, error) PostProcessor.Configure(raws ...interface{}) error close #1768 Example hcl files: ```hcl // file amazon-ebs-kms-key/run.pkr.hcl build { sources = [ "source.amazon-ebs.first", ] provisioner "shell" { inline = [ "sleep 5" ] } post-processor "shell-local" { inline = [ "sleep 5" ] } } // amazon-ebs-kms-key/source.pkr.hcl source "amazon-ebs" "first" { ami_name = "hcl2-test" region = "us-east-1" instance_type = "t2.micro" kms_key_id = "c729958f-c6ba-44cd-ab39-35ab68ce0a6c" encrypt_boot = true source_ami_filter { filters { virtualization-type = "hvm" name = "amzn-ami-hvm-????.??.?.????????-x86_64-gp2" root-device-type = "ebs" } most_recent = true owners = ["amazon"] } launch_block_device_mappings { device_name = "/dev/xvda" volume_size = 20 volume_type = "gp2" delete_on_termination = "true" } launch_block_device_mappings { device_name = "/dev/xvdf" volume_size = 500 volume_type = "gp2" delete_on_termination = true encrypted = true } ami_regions = ["eu-central-1"] run_tags { Name = "packer-solr-something" stack-name = "DevOps Tools" } communicator = "ssh" ssh_pty = true ssh_username = "ec2-user" associate_public_ip_address = true } ```
2019-12-17 05:25:56 -05:00
"github.com/hashicorp/hcl/v2/hcldec"
2020-12-17 16:29:25 -05:00
"github.com/hashicorp/packer-plugin-sdk/common"
"github.com/hashicorp/packer-plugin-sdk/guestexec"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer-plugin-sdk/template/config"
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
)
type Config struct {
common.PackerConfig `mapstructure:",squash"`
2015-05-27 17:50:20 -04:00
ctx interpolate.Context
2018-05-01 16:38:01 -04:00
// If true, staging directory is removed after executing puppet.
CleanStagingDir bool `mapstructure:"clean_staging_directory"`
// A path to the client certificate
ClientCertPath string `mapstructure:"client_cert_path"`
// A path to a directory containing the client private keys
ClientPrivateKeyPath string `mapstructure:"client_private_key_path"`
// The command used to execute Puppet.
ExecuteCommand string `mapstructure:"execute_command"`
// Additional argument to pass when executing Puppet.
ExtraArguments []string `mapstructure:"extra_arguments"`
2018-05-01 16:38:01 -04:00
// Additional facts to set when executing Puppet
Facter map[string]string
// The Guest OS Type (unix or windows)
GuestOSType string `mapstructure:"guest_os_type"`
2018-05-01 16:38:01 -04:00
// If true, packer will ignore all exit-codes from a puppet run
IgnoreExitCodes bool `mapstructure:"ignore_exit_codes"`
2018-05-01 16:38:01 -04:00
// If true, `sudo` will NOT be used to execute Puppet.
PreventSudo bool `mapstructure:"prevent_sudo"`
2018-05-01 16:38:01 -04:00
// The directory that contains the puppet binary.
// E.g. if it can't be found on the standard path.
PuppetBinDir string `mapstructure:"puppet_bin_dir"`
// The hostname of the Puppet node.
PuppetNode string `mapstructure:"puppet_node"`
// The hostname of the Puppet server.
PuppetServer string `mapstructure:"puppet_server"`
// The directory where files will be uploaded. Packer requires write
// permissions in this directory.
StagingDir string `mapstructure:"staging_dir"`
// The directory from which the command will be executed.
// Packer requires the directory to exist when running puppet.
WorkingDir string `mapstructure:"working_directory"`
// Instructs the communicator to run the remote script as a Windows
// scheduled task, effectively elevating the remote user by impersonating
// a logged-in user
ElevatedUser string `mapstructure:"elevated_user"`
ElevatedPassword string `mapstructure:"elevated_password"`
}
type guestOSTypeConfig struct {
executeCommand string
facterVarsFmt string
facterVarsJoiner string
2018-05-01 16:38:01 -04:00
stagingDir string
tempDir string
}
// FIXME assumes both Packer host and target are same OS
var guestOSTypeConfigs = map[string]guestOSTypeConfig{
guestexec.UnixOSType: {
tempDir: "/tmp",
stagingDir: "/tmp/packer-puppet-server",
executeCommand: "cd {{.WorkingDir}} && " +
`{{if ne .FacterVars ""}}{{.FacterVars}} {{end}}` +
"{{if .Sudo}}sudo -E {{end}}" +
`{{if ne .PuppetBinDir ""}}{{.PuppetBinDir}}/{{end}}` +
"puppet agent --onetime --no-daemonize --detailed-exitcodes " +
"{{if .Debug}}--debug {{end}}" +
`{{if ne .PuppetServer ""}}--server='{{.PuppetServer}}' {{end}}` +
2018-05-08 09:53:18 -04:00
`{{if ne .PuppetNode ""}}--certname='{{.PuppetNode}}' {{end}}` +
`{{if ne .ClientCertPath ""}}--certdir='{{.ClientCertPath}}' {{end}}` +
`{{if ne .ClientPrivateKeyPath ""}}--privatekeydir='{{.ClientPrivateKeyPath}}' {{end}}` +
`{{if ne .ExtraArguments ""}}{{.ExtraArguments}} {{end}}`,
facterVarsFmt: "FACTER_%s='%s'",
facterVarsJoiner: " ",
},
guestexec.WindowsOSType: {
tempDir: filepath.ToSlash(os.Getenv("TEMP")),
stagingDir: filepath.ToSlash(os.Getenv("SYSTEMROOT")) + "/Temp/packer-puppet-server",
executeCommand: "cd {{.WorkingDir}} && " +
`{{if ne .FacterVars ""}}{{.FacterVars}} && {{end}}` +
`{{if ne .PuppetBinDir ""}}{{.PuppetBinDir}}/{{end}}` +
"puppet agent --onetime --no-daemonize --detailed-exitcodes " +
"{{if .Debug}}--debug {{end}}" +
`{{if ne .PuppetServer ""}}--server='{{.PuppetServer}}' {{end}}` +
2018-05-08 09:53:18 -04:00
`{{if ne .PuppetNode ""}}--certname='{{.PuppetNode}}' {{end}}` +
`{{if ne .ClientCertPath ""}}--certdir='{{.ClientCertPath}}' {{end}}` +
`{{if ne .ClientPrivateKeyPath ""}}--privatekeydir='{{.ClientPrivateKeyPath}}' {{end}}` +
`{{if ne .ExtraArguments ""}}{{.ExtraArguments}} {{end}}`,
facterVarsFmt: `SET "FACTER_%s=%s"`,
facterVarsJoiner: " & ",
},
}
type Provisioner struct {
config Config
communicator packersdk.Communicator
guestOSTypeConfig guestOSTypeConfig
guestCommands *guestexec.GuestCommands
generatedData map[string]interface{}
}
type ExecuteTemplate struct {
ClientCertPath string
ClientPrivateKeyPath string
2018-05-01 16:38:01 -04:00
Debug bool
ExtraArguments string
FacterVars string
PuppetNode string
PuppetServer string
PuppetBinDir string
Sudo bool
WorkingDir string
}
type EnvVarsTemplate struct {
WinRMPassword string
}
build using HCL2 (#8423) This follows #8232 which added the code to generate the code required to parse HCL files for each packer component. All old config files of packer will keep on working the same. Packer takes one argument. When a directory is passed, all files in the folder with a name ending with “.pkr.hcl” or “.pkr.json” will be parsed using the HCL2 format. When a file ending with “.pkr.hcl” or “.pkr.json” is passed it will be parsed using the HCL2 format. For every other case; the old packer style will be used. ## 1. the hcl2template pkg can create a packer.Build from a set of HCL (v2) files I had to make the packer.coreBuild (which is our one and only packer.Build ) a public struct with public fields ## 2. Components interfaces get a new ConfigSpec Method to read a file from an HCL file. This is a breaking change for packer plugins. a packer component can be a: builder/provisioner/post-processor each component interface now gets a `ConfigSpec() hcldec.ObjectSpec` which allows packer to tell what is the layout of the hcl2 config meant to configure that specific component. This ObjectSpec is sent through the wire (RPC) and a cty.Value is now sent through the already existing configuration entrypoints: Provisioner.Prepare(raws ...interface{}) error Builder.Prepare(raws ...interface{}) ([]string, error) PostProcessor.Configure(raws ...interface{}) error close #1768 Example hcl files: ```hcl // file amazon-ebs-kms-key/run.pkr.hcl build { sources = [ "source.amazon-ebs.first", ] provisioner "shell" { inline = [ "sleep 5" ] } post-processor "shell-local" { inline = [ "sleep 5" ] } } // amazon-ebs-kms-key/source.pkr.hcl source "amazon-ebs" "first" { ami_name = "hcl2-test" region = "us-east-1" instance_type = "t2.micro" kms_key_id = "c729958f-c6ba-44cd-ab39-35ab68ce0a6c" encrypt_boot = true source_ami_filter { filters { virtualization-type = "hvm" name = "amzn-ami-hvm-????.??.?.????????-x86_64-gp2" root-device-type = "ebs" } most_recent = true owners = ["amazon"] } launch_block_device_mappings { device_name = "/dev/xvda" volume_size = 20 volume_type = "gp2" delete_on_termination = "true" } launch_block_device_mappings { device_name = "/dev/xvdf" volume_size = 500 volume_type = "gp2" delete_on_termination = true encrypted = true } ami_regions = ["eu-central-1"] run_tags { Name = "packer-solr-something" stack-name = "DevOps Tools" } communicator = "ssh" ssh_pty = true ssh_username = "ec2-user" associate_public_ip_address = true } ```
2019-12-17 05:25:56 -05:00
func (p *Provisioner) ConfigSpec() hcldec.ObjectSpec { return p.config.FlatMapstructure().HCL2Spec() }
func (p *Provisioner) Prepare(raws ...interface{}) error {
2015-05-27 17:50:20 -04:00
err := config.Decode(&p.config, &config.DecodeOpts{
PluginType: "puppet-server",
Interpolate: true,
InterpolateContext: &p.config.ctx,
2015-05-27 17:50:20 -04:00
InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{
"execute_command",
"extra_arguments",
},
2015-05-27 17:50:20 -04:00
},
}, raws...)
if err != nil {
return err
}
if p.config.GuestOSType == "" {
p.config.GuestOSType = guestexec.DefaultOSType
}
p.config.GuestOSType = strings.ToLower(p.config.GuestOSType)
var ok bool
p.guestOSTypeConfig, ok = guestOSTypeConfigs[p.config.GuestOSType]
if !ok {
return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType)
}
p.guestCommands, err = guestexec.NewGuestCommands(p.config.GuestOSType, !p.config.PreventSudo)
if err != nil {
return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType)
}
if p.config.ExecuteCommand == "" {
p.config.ExecuteCommand = p.guestOSTypeConfig.executeCommand
}
if p.config.StagingDir == "" {
p.config.StagingDir = p.guestOSTypeConfig.stagingDir
}
if p.config.WorkingDir == "" {
p.config.WorkingDir = p.config.StagingDir
}
if p.config.Facter == nil {
p.config.Facter = make(map[string]string)
}
p.config.Facter["packer_build_name"] = p.config.PackerBuildName
p.config.Facter["packer_builder_type"] = p.config.PackerBuilderType
var errs *packersdk.MultiError
if p.config.ClientCertPath != "" {
info, err := os.Stat(p.config.ClientCertPath)
if err != nil {
errs = packersdk.MultiErrorAppend(errs,
fmt.Errorf("client_cert_dir is invalid: %s", err))
} else if !info.IsDir() {
errs = packersdk.MultiErrorAppend(errs,
fmt.Errorf("client_cert_dir must point to a directory"))
}
}
if p.config.ClientPrivateKeyPath != "" {
info, err := os.Stat(p.config.ClientPrivateKeyPath)
if err != nil {
errs = packersdk.MultiErrorAppend(errs,
fmt.Errorf("client_private_key_dir is invalid: %s", err))
} else if !info.IsDir() {
errs = packersdk.MultiErrorAppend(errs,
fmt.Errorf("client_private_key_dir must point to a directory"))
}
}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
return nil
}
func (p *Provisioner) Provision(ctx context.Context, ui packersdk.Ui, comm packersdk.Communicator, generatedData map[string]interface{}) error {
ui.Say("Provisioning with Puppet...")
p.communicator = comm
p.generatedData = generatedData
ui.Message("Creating Puppet staging directory...")
if err := p.createDir(ui, comm, p.config.StagingDir); err != nil {
return fmt.Errorf("Error creating staging directory: %s", err)
}
// Upload client cert dir if set
remoteClientCertPath := ""
if p.config.ClientCertPath != "" {
ui.Message(fmt.Sprintf(
"Uploading client cert from: %s", p.config.ClientCertPath))
remoteClientCertPath = fmt.Sprintf("%s/certs", p.config.StagingDir)
err := p.uploadDirectory(ui, comm, remoteClientCertPath, p.config.ClientCertPath)
if err != nil {
return fmt.Errorf("Error uploading client cert: %s", err)
}
}
// Upload client cert dir if set
remoteClientPrivateKeyPath := ""
if p.config.ClientPrivateKeyPath != "" {
ui.Message(fmt.Sprintf(
"Uploading client private keys from: %s", p.config.ClientPrivateKeyPath))
remoteClientPrivateKeyPath = fmt.Sprintf("%s/private_keys", p.config.StagingDir)
err := p.uploadDirectory(ui, comm, remoteClientPrivateKeyPath, p.config.ClientPrivateKeyPath)
if err != nil {
return fmt.Errorf("Error uploading client private keys: %s", err)
}
}
// Compile the facter variables
facterVars := make([]string, 0, len(p.config.Facter))
for k, v := range p.config.Facter {
facterVars = append(facterVars, fmt.Sprintf(p.guestOSTypeConfig.facterVarsFmt, k, v))
}
data := ExecuteTemplate{
ClientCertPath: remoteClientCertPath,
ClientPrivateKeyPath: remoteClientPrivateKeyPath,
2018-05-01 16:38:01 -04:00
ExtraArguments: "",
FacterVars: strings.Join(facterVars, p.guestOSTypeConfig.facterVarsJoiner),
PuppetNode: p.config.PuppetNode,
PuppetServer: p.config.PuppetServer,
PuppetBinDir: p.config.PuppetBinDir,
Sudo: !p.config.PreventSudo,
WorkingDir: p.config.WorkingDir,
}
p.config.ctx.Data = &data
_ExtraArguments, err := interpolate.Render(strings.Join(p.config.ExtraArguments, " "), &p.config.ctx)
if err != nil {
return err
2015-05-27 17:50:20 -04:00
}
data.ExtraArguments = _ExtraArguments
command, err := interpolate.Render(p.config.ExecuteCommand, &p.config.ctx)
if err != nil {
return err
}
if p.config.ElevatedUser != "" {
command, err = guestexec.GenerateElevatedRunner(command, p)
if err != nil {
return err
}
}
cmd := &packersdk.RemoteCmd{
Command: command,
}
ui.Message(fmt.Sprintf("Running Puppet: %s", command))
if err := cmd.RunWithUi(ctx, comm, ui); err != nil {
return err
}
if cmd.ExitStatus() != 0 && cmd.ExitStatus() != 2 && !p.config.IgnoreExitCodes {
return fmt.Errorf("Puppet exited with a non-zero exit status: %d", cmd.ExitStatus())
}
2018-05-01 16:38:01 -04:00
if p.config.CleanStagingDir {
if err := p.removeDir(ui, comm, p.config.StagingDir); err != nil {
return fmt.Errorf("Error removing staging directory: %s", err)
}
}
return nil
}
func (p *Provisioner) createDir(ui packersdk.Ui, comm packersdk.Communicator, dir string) error {
ui.Message(fmt.Sprintf("Creating directory: %s", dir))
ctx := context.TODO()
cmd := &packersdk.RemoteCmd{Command: p.guestCommands.CreateDir(dir)}
if err := cmd.RunWithUi(ctx, comm, ui); err != nil {
return err
}
if cmd.ExitStatus() != 0 {
return fmt.Errorf("Non-zero exit status. See output above for more info.")
}
// Chmod the directory to 0777 just so that we can access it as our user
cmd = &packersdk.RemoteCmd{Command: p.guestCommands.Chmod(dir, "0777")}
if err := cmd.RunWithUi(ctx, comm, ui); err != nil {
return err
}
if cmd.ExitStatus() != 0 {
return fmt.Errorf("Non-zero exit status. See output above for more info.")
}
return nil
}
func (p *Provisioner) removeDir(ui packersdk.Ui, comm packersdk.Communicator, dir string) error {
ctx := context.TODO()
cmd := &packersdk.RemoteCmd{Command: p.guestCommands.RemoveDir(dir)}
if err := cmd.RunWithUi(ctx, comm, ui); err != nil {
return err
}
if cmd.ExitStatus() != 0 {
return fmt.Errorf("Non-zero exit status.")
}
return nil
}
func (p *Provisioner) uploadDirectory(ui packersdk.Ui, comm packersdk.Communicator, dst string, src string) error {
if err := p.createDir(ui, comm, dst); err != nil {
return err
}
// Make sure there is a trailing "/" so that the directory isn't
// created on the other side.
if src[len(src)-1] != '/' {
src = src + "/"
}
return comm.UploadDir(dst, src, nil)
}
func (p *Provisioner) Communicator() packersdk.Communicator {
return p.communicator
}
func (p *Provisioner) ElevatedUser() string {
return p.config.ElevatedUser
}
func (p *Provisioner) ElevatedPassword() string {
// Replace ElevatedPassword for winrm users who used this feature
p.config.ctx.Data = p.generatedData
elevatedPassword, _ := interpolate.Render(p.config.ElevatedPassword, &p.config.ctx)
return elevatedPassword
}