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

501 lines
15 KiB
Go
Raw Normal View History

//go:generate mapstructure-to-hcl2 -type Config
2017-01-13 02:51:36 -05:00
// Package puppetmasterless implements a provisioner for Packer that executes
// Puppet on the remote machine, configured to apply a local manifest
// versus connecting to a Puppet master.
package puppetmasterless
2013-08-01 20:05:23 -04:00
import (
"context"
2013-08-01 20:05:23 -04:00
"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"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/helper/config"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/provisioner"
"github.com/hashicorp/packer/template/interpolate"
2013-08-01 20:05:23 -04:00
)
type Config struct {
common.PackerConfig `mapstructure:",squash"`
2015-05-27 17:50:20 -04:00
ctx interpolate.Context
2013-08-01 20:05:23 -04:00
2018-05-01 16:38:01 -04:00
// If true, staging directory is removed after executing puppet.
CleanStagingDir bool `mapstructure:"clean_staging_directory"`
// The Guest OS Type (unix or windows)
GuestOSType string `mapstructure:"guest_os_type"`
// The command used to execute Puppet.
ExecuteCommand string `mapstructure:"execute_command"`
2013-08-01 20:05:23 -04:00
// Additional arguments to pass when executing Puppet
ExtraArguments []string `mapstructure:"extra_arguments"`
// Additional facts to set when executing Puppet
Facter map[string]string
// Path to a hiera configuration file to upload and use.
HieraConfigPath string `mapstructure:"hiera_config_path"`
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"`
2013-08-01 20:05:23 -04:00
// An array of local paths of modules to upload.
ModulePaths []string `mapstructure:"module_paths"`
2013-08-01 20:05:23 -04:00
// The main manifest file to apply to kick off the entire thing.
2013-08-01 20:05:23 -04:00
ManifestFile string `mapstructure:"manifest_file"`
// A directory of manifest files that will be uploaded to the remote
// machine.
ManifestDir string `mapstructure:"manifest_dir"`
// If true, `sudo` will NOT be used to execute Puppet.
2013-08-01 20:05:23 -04:00
PreventSudo bool `mapstructure:"prevent_sudo"`
2018-05-01 17:16:47 -04:00
// The directory that contains the puppet binary.
2018-05-01 16:38:01 -04:00
// E.g. if it can't be found on the standard path.
PuppetBinDir string `mapstructure:"puppet_bin_dir"`
// The directory where files will be uploaded. Packer requires write
// permissions in this directory.
StagingDir string `mapstructure:"staging_directory"`
// 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
modulePathJoiner 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{
provisioner.UnixOSType: {
tempDir: "/tmp",
stagingDir: "/tmp/packer-puppet-masterless",
executeCommand: "cd {{.WorkingDir}} && " +
`{{if ne .FacterVars ""}}{{.FacterVars}} {{end}}` +
"{{if .Sudo}}sudo -E {{end}}" +
`{{if ne .PuppetBinDir ""}}{{.PuppetBinDir}}/{{end}}` +
"puppet apply --detailed-exitcodes " +
"{{if .Debug}}--debug {{end}}" +
`{{if ne .ModulePath ""}}--modulepath='{{.ModulePath}}' {{end}}` +
`{{if ne .HieraConfigPath ""}}--hiera_config='{{.HieraConfigPath}}' {{end}}` +
`{{if ne .ManifestDir ""}}--manifestdir='{{.ManifestDir}}' {{end}}` +
`{{if ne .ExtraArguments ""}}{{.ExtraArguments}} {{end}}` +
"{{.ManifestFile}}",
facterVarsFmt: "FACTER_%s='%s'",
facterVarsJoiner: " ",
modulePathJoiner: ":",
},
provisioner.WindowsOSType: {
tempDir: filepath.ToSlash(os.Getenv("TEMP")),
stagingDir: filepath.ToSlash(os.Getenv("SYSTEMROOT")) + "/Temp/packer-puppet-masterless",
executeCommand: "cd {{.WorkingDir}} && " +
`{{if ne .FacterVars ""}}{{.FacterVars}} && {{end}}` +
`{{if ne .PuppetBinDir ""}}{{.PuppetBinDir}}/{{end}}` +
"puppet apply --detailed-exitcodes " +
"{{if .Debug}}--debug {{end}}" +
`{{if ne .ModulePath ""}}--modulepath='{{.ModulePath}}' {{end}}` +
`{{if ne .HieraConfigPath ""}}--hiera_config='{{.HieraConfigPath}}' {{end}}` +
`{{if ne .ManifestDir ""}}--manifestdir='{{.ManifestDir}}' {{end}}` +
`{{if ne .ExtraArguments ""}}{{.ExtraArguments}} {{end}}` +
"{{.ManifestFile}}",
facterVarsFmt: `SET "FACTER_%s=%s"`,
facterVarsJoiner: " & ",
modulePathJoiner: ";",
},
2013-08-01 20:05:23 -04:00
}
type Provisioner struct {
config Config
communicator packer.Communicator
guestOSTypeConfig guestOSTypeConfig
guestCommands *provisioner.GuestCommands
generatedData map[string]interface{}
2013-08-01 20:05:23 -04:00
}
type ExecuteTemplate struct {
2018-05-08 12:44:15 -04:00
Debug bool
ExtraArguments string
FacterVars string
HieraConfigPath string
ModulePath string
2018-05-08 12:21:04 -04:00
ModulePathJoiner string
2018-05-08 12:44:15 -04:00
ManifestFile string
ManifestDir string
PuppetBinDir string
Sudo bool
WorkingDir string
2013-08-01 20:05:23 -04:00
}
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() }
2013-08-01 20:05:23 -04:00
func (p *Provisioner) Prepare(raws ...interface{}) error {
2015-05-27 17:50:20 -04:00
err := config.Decode(&p.config, &config.DecodeOpts{
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
}
// Set some defaults
if p.config.GuestOSType == "" {
p.config.GuestOSType = provisioner.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 = provisioner.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
2013-08-01 20:05:23 -04:00
}
if p.config.WorkingDir == "" {
2015-01-10 18:29:01 -05:00
p.config.WorkingDir = p.config.StagingDir
}
if p.config.Facter == nil {
p.config.Facter = make(map[string]string)
2013-08-01 20:05:23 -04:00
}
p.config.Facter["packer_build_name"] = p.config.PackerBuildName
p.config.Facter["packer_builder_type"] = p.config.PackerBuilderType
// Validation
2015-05-27 17:50:20 -04:00
var errs *packer.MultiError
if p.config.HieraConfigPath != "" {
info, err := os.Stat(p.config.HieraConfigPath)
if err != nil {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("hiera_config_path is invalid: %s", err))
} else if info.IsDir() {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("hiera_config_path must point to a file"))
}
}
if p.config.ManifestDir != "" {
info, err := os.Stat(p.config.ManifestDir)
if err != nil {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("manifest_dir is invalid: %s", err))
} else if !info.IsDir() {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("manifest_dir must point to a directory"))
}
}
if p.config.ManifestFile == "" {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("A manifest_file must be specified."))
} else {
2014-12-16 09:49:57 -05:00
_, err := os.Stat(p.config.ManifestFile)
if err != nil {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("manifest_file is invalid: %s", err))
2013-08-01 20:05:23 -04:00
}
}
for i, path := range p.config.ModulePaths {
info, err := os.Stat(path)
if err != nil {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("module_path[%d] is invalid: %s", i, err))
} else if !info.IsDir() {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("module_path[%d] must point to a directory", i))
}
}
if errs != nil && len(errs.Errors) > 0 {
return errs
2013-08-01 20:05:23 -04:00
}
return nil
}
func (p *Provisioner) Provision(ctx context.Context, ui packer.Ui, comm packer.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)
2013-08-01 20:05:23 -04:00
}
// Upload hiera config if set
remoteHieraConfigPath := ""
if p.config.HieraConfigPath != "" {
var err error
remoteHieraConfigPath, err = p.uploadHieraConfig(ui, comm)
if err != nil {
return fmt.Errorf("Error uploading hiera config: %s", err)
}
}
// Upload manifest dir if set
remoteManifestDir := ""
if p.config.ManifestDir != "" {
ui.Message(fmt.Sprintf(
"Uploading manifest directory from: %s", p.config.ManifestDir))
remoteManifestDir = fmt.Sprintf("%s/manifests", p.config.StagingDir)
err := p.uploadDirectory(ui, comm, remoteManifestDir, p.config.ManifestDir)
if err != nil {
return fmt.Errorf("Error uploading manifest dir: %s", err)
}
}
2013-08-01 20:05:23 -04:00
// Upload all modules
modulePaths := make([]string, 0, len(p.config.ModulePaths))
for i, path := range p.config.ModulePaths {
ui.Message(fmt.Sprintf("Uploading local modules from: %s", path))
targetPath := fmt.Sprintf("%s/module-%d", p.config.StagingDir, i)
if err := p.uploadDirectory(ui, comm, targetPath, path); err != nil {
return fmt.Errorf("Error uploading modules: %s", err)
}
modulePaths = append(modulePaths, targetPath)
2013-08-01 20:05:23 -04:00
}
// Upload manifests
remoteManifestFile, err := p.uploadManifests(ui, comm)
2013-08-01 20:05:23 -04:00
if err != nil {
return fmt.Errorf("Error uploading manifests: %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{
2018-05-08 12:44:15 -04:00
ExtraArguments: "",
FacterVars: strings.Join(facterVars, p.guestOSTypeConfig.facterVarsJoiner),
HieraConfigPath: remoteHieraConfigPath,
ManifestDir: remoteManifestDir,
ManifestFile: remoteManifestFile,
ModulePath: strings.Join(modulePaths, p.guestOSTypeConfig.modulePathJoiner),
2018-05-08 12:21:04 -04:00
ModulePathJoiner: p.guestOSTypeConfig.modulePathJoiner,
2018-05-08 12:44:15 -04:00
PuppetBinDir: p.config.PuppetBinDir,
Sudo: !p.config.PreventSudo,
WorkingDir: p.config.WorkingDir,
2015-05-27 17:50:20 -04:00
}
p.config.ctx.Data = &data
_ExtraArguments, err := interpolate.Render(strings.Join(p.config.ExtraArguments, " "), &p.config.ctx)
if err != nil {
return err
}
data.ExtraArguments = _ExtraArguments
2015-05-27 17:50:20 -04:00
command, err := interpolate.Render(p.config.ExecuteCommand, &p.config.ctx)
if err != nil {
return err
}
2013-08-01 20:05:23 -04:00
if p.config.ElevatedUser != "" {
command, err = provisioner.GenerateElevatedRunner(command, p)
if err != nil {
return err
}
}
cmd := &packer.RemoteCmd{
Command: command,
}
2013-08-01 20:05:23 -04:00
ui.Message(fmt.Sprintf("Running Puppet: %s", command))
if err := cmd.RunWithUi(ctx, comm, ui); err != nil {
return fmt.Errorf("Got an error starting command: %s", 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())
2013-08-01 20:05:23 -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)
}
}
2013-08-01 20:05:23 -04:00
return nil
}
func (p *Provisioner) uploadHieraConfig(ui packer.Ui, comm packer.Communicator) (string, error) {
ui.Message("Uploading hiera configuration...")
f, err := os.Open(p.config.HieraConfigPath)
if err != nil {
return "", err
}
defer f.Close()
path := fmt.Sprintf("%s/hiera.yaml", p.config.StagingDir)
if err := comm.Upload(path, f, nil); err != nil {
return "", err
}
return path, nil
}
func (p *Provisioner) uploadManifests(ui packer.Ui, comm packer.Communicator) (string, error) {
// Create the remote manifests directory...
ui.Message("Uploading manifests...")
remoteManifestsPath := fmt.Sprintf("%s/manifests", p.config.StagingDir)
if err := p.createDir(ui, comm, remoteManifestsPath); err != nil {
return "", fmt.Errorf("Error creating manifests directory: %s", err)
2013-08-01 20:05:23 -04:00
}
// NOTE! manifest_file may either be a directory or a file, as puppet apply
// now accepts either one.
fi, err := os.Stat(p.config.ManifestFile)
2013-08-01 20:05:23 -04:00
if err != nil {
return "", fmt.Errorf("Error inspecting manifest file: %s", err)
2013-08-01 20:05:23 -04:00
}
if fi.IsDir() {
// If manifest_file is a directory we'll upload the whole thing
ui.Message(fmt.Sprintf(
"Uploading manifest directory from: %s", p.config.ManifestFile))
remoteManifestDir := fmt.Sprintf("%s/manifests", p.config.StagingDir)
err := p.uploadDirectory(ui, comm, remoteManifestDir, p.config.ManifestFile)
if err != nil {
return "", fmt.Errorf("Error uploading manifest dir: %s", err)
}
return remoteManifestDir, nil
2017-01-13 02:51:36 -05:00
}
// Otherwise manifest_file is a file and we'll upload it
ui.Message(fmt.Sprintf(
"Uploading manifest file from: %s", p.config.ManifestFile))
2017-01-13 02:51:36 -05:00
f, err := os.Open(p.config.ManifestFile)
if err != nil {
return "", err
}
defer f.Close()
2013-08-01 20:05:23 -04:00
2017-01-13 02:51:36 -05:00
manifestFilename := filepath.Base(p.config.ManifestFile)
remoteManifestFile := fmt.Sprintf("%s/%s", remoteManifestsPath, manifestFilename)
if err := comm.Upload(remoteManifestFile, f, nil); err != nil {
return "", err
}
2017-01-13 02:51:36 -05:00
return remoteManifestFile, nil
2013-08-01 20:05:23 -04:00
}
func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error {
ui.Message(fmt.Sprintf("Creating directory: %s", dir))
cmd := &packer.RemoteCmd{Command: p.guestCommands.CreateDir(dir)}
ctx := context.TODO()
2013-08-01 20:05:23 -04:00
if err := cmd.RunWithUi(ctx, comm, ui); err != nil {
return err
}
2013-08-01 20:05:23 -04:00
if cmd.ExitStatus() != 0 {
return fmt.Errorf("Non-zero exit status.")
2013-08-01 20:05:23 -04:00
}
// Chmod the directory to 0777 just so that we can access it as our user
cmd = &packer.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 packer.Ui, comm packer.Communicator, dir string) error {
ctx := context.TODO()
cmd := &packer.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 packer.Ui, comm packer.Communicator, dst string, src string) error {
if err := p.createDir(ui, comm, dst); err != nil {
return err
2013-08-01 20:05:23 -04:00
}
// Make sure there is a trailing "/" so that the directory isn't
// created on the other side.
if src[len(src)-1] != '/' {
src = src + "/"
2013-08-01 20:05:23 -04:00
}
return comm.UploadDir(dst, src, nil)
2013-08-01 20:05:23 -04:00
}
func (p *Provisioner) Communicator() packer.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
}