Wilken Rivera c262467413
Extract Azure plugin components from Packer (#10979)
* Remove Azure plugin components and docs

* Add Azure plugin to vendored_plugins

* Updates Azure to use remote plugin docs

* Revendor packer-plugin-azure at v0.0.2

* Update to new version of the Packer plugin SDK v0.2.1
* Update to the official version of go-getter@v2.0.0
* Update salt provisioner to use new go-getter API

* Update vendored plugins to working versions

This changes fixes an issue with the go.sum for the Azure plugin.
It also revendors the plugins for puppet, chef, and ansible as v0.0.1
of those plugins where not usable.
2021-05-13 16:13:21 -04:00

607 lines
20 KiB
Go

//go:generate packer-sdc mapstructure-to-hcl2 -type Config
// This package implements a provisioner for Packer that executes a
// saltstack state within the remote machine
package saltmasterless
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/hashicorp/go-getter/v2"
"github.com/hashicorp/hcl/v2/hcldec"
"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"`
// If true, run the salt-bootstrap script
SkipBootstrap bool `mapstructure:"skip_bootstrap"`
BootstrapArgs string `mapstructure:"bootstrap_args"`
DisableSudo bool `mapstructure:"disable_sudo"`
// Custom state to run instead of highstate
CustomState string `mapstructure:"custom_state"`
// Local path to the minion config
MinionConfig string `mapstructure:"minion_config"`
// Local path to the minion grains
GrainsFile string `mapstructure:"grains_file"`
// Local path to the salt state tree
LocalStateTree string `mapstructure:"local_state_tree"`
// Local path to the salt pillar roots
LocalPillarRoots string `mapstructure:"local_pillar_roots"`
// Remote path to the salt state tree
RemoteStateTree string `mapstructure:"remote_state_tree"`
// Remote path to the salt pillar roots
RemotePillarRoots string `mapstructure:"remote_pillar_roots"`
// Where files will be copied before moving to the /srv/salt directory
TempConfigDir string `mapstructure:"temp_config_dir"`
// Don't exit packer if salt-call returns an error code
NoExitOnFailure bool `mapstructure:"no_exit_on_failure"`
// Set the logging level for the salt-call run
LogLevel string `mapstructure:"log_level"`
// Arguments to pass to salt-call
SaltCallArgs string `mapstructure:"salt_call_args"`
// Directory containing salt-call
SaltBinDir string `mapstructure:"salt_bin_dir"`
// Command line args passed onto salt-call
CmdArgs string `mapstructure-to-hcl2:",skip"`
// The Guest OS Type (unix or windows)
GuestOSType string `mapstructure:"guest_os_type"`
// An array of private or community git source formulas
Formulas []string `mapstructure:"formulas"`
ctx interpolate.Context
}
type Provisioner struct {
config Config
guestOSTypeConfig guestOSTypeConfig
guestCommands *guestexec.GuestCommands
}
type guestOSTypeConfig struct {
tempDir string
stateRoot string
pillarRoot string
configDir string
bootstrapFetchCmd string
bootstrapRunCmd string
}
var guestOSTypeConfigs = map[string]guestOSTypeConfig{
guestexec.UnixOSType: {
configDir: "/etc/salt",
tempDir: "/tmp/salt",
stateRoot: "/srv/salt",
pillarRoot: "/srv/pillar",
bootstrapFetchCmd: "curl -L https://bootstrap.saltproject.io -o /tmp/install_salt.sh || wget -O /tmp/install_salt.sh https://bootstrap.saltproject.io",
bootstrapRunCmd: "sh /tmp/install_salt.sh",
},
guestexec.WindowsOSType: {
configDir: "C:/salt/conf",
tempDir: "C:/Windows/Temp/salt/",
stateRoot: "C:/salt/state",
pillarRoot: "C:/salt/pillar/",
bootstrapFetchCmd: "powershell -Command \"[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]'Tls,Tls11,Tls12'; Invoke-WebRequest -Uri 'https://winbootstrap.saltproject.io' -OutFile 'C:/Windows/Temp/bootstrap-salt.ps1'\"",
bootstrapRunCmd: "Powershell C:/Windows/Temp/bootstrap-salt.ps1",
},
}
func (p *Provisioner) ConfigSpec() hcldec.ObjectSpec { return p.config.FlatMapstructure().HCL2Spec() }
func (p *Provisioner) Prepare(raws ...interface{}) error {
err := config.Decode(&p.config, &config.DecodeOpts{
PluginType: "salt-masterless",
Interpolate: true,
InterpolateContext: &p.config.ctx,
InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{},
},
}, raws...)
if err != nil {
return err
}
if p.config.GuestOSType == "" {
p.config.GuestOSType = guestexec.DefaultOSType
} else {
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.DisableSudo)
if err != nil {
return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType)
}
if p.config.TempConfigDir == "" {
p.config.TempConfigDir = p.guestOSTypeConfig.tempDir
}
var errs *packersdk.MultiError
// require a salt state tree
err = validateDirConfig(p.config.LocalStateTree, "local_state_tree", true)
if err != nil {
errs = packersdk.MultiErrorAppend(errs, err)
}
if p.config.Formulas != nil && len(p.config.Formulas) > 0 {
validURLs := hasValidFormulaURLs(p.config.Formulas)
if !validURLs {
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("Invalid formula URL. Please verify the git URLs also contain a '//' subdir"))
}
}
err = validateDirConfig(p.config.LocalPillarRoots, "local_pillar_roots", false)
if err != nil {
errs = packersdk.MultiErrorAppend(errs, err)
}
err = validateFileConfig(p.config.MinionConfig, "minion_config", false)
if err != nil {
errs = packersdk.MultiErrorAppend(errs, err)
}
if p.config.MinionConfig != "" && (p.config.RemoteStateTree != "" || p.config.RemotePillarRoots != "") {
errs = packersdk.MultiErrorAppend(errs,
errors.New("remote_state_tree and remote_pillar_roots only apply when minion_config is not used"))
}
err = validateFileConfig(p.config.GrainsFile, "grains_file", false)
if err != nil {
errs = packersdk.MultiErrorAppend(errs, err)
}
// build the command line args to pass onto salt
var cmd_args bytes.Buffer
if p.config.CustomState == "" {
cmd_args.WriteString(" state.highstate")
} else {
cmd_args.WriteString(" state.sls ")
cmd_args.WriteString(p.config.CustomState)
}
if p.config.MinionConfig == "" {
// pass --file-root and --pillar-root if no minion_config is supplied
if p.config.RemoteStateTree != "" {
cmd_args.WriteString(" --file-root=")
cmd_args.WriteString(p.config.RemoteStateTree)
} else {
cmd_args.WriteString(" --file-root=")
cmd_args.WriteString(p.guestOSTypeConfig.stateRoot)
}
if p.config.RemotePillarRoots != "" {
cmd_args.WriteString(" --pillar-root=")
cmd_args.WriteString(p.config.RemotePillarRoots)
} else {
cmd_args.WriteString(" --pillar-root=")
cmd_args.WriteString(p.guestOSTypeConfig.pillarRoot)
}
}
if !p.config.NoExitOnFailure {
cmd_args.WriteString(" --retcode-passthrough")
}
if p.config.LogLevel == "" {
cmd_args.WriteString(" -l info")
} else {
cmd_args.WriteString(" -l ")
cmd_args.WriteString(p.config.LogLevel)
}
if p.config.SaltCallArgs != "" {
cmd_args.WriteString(" ")
cmd_args.WriteString(p.config.SaltCallArgs)
}
p.config.CmdArgs = cmd_args.String()
if errs != nil && len(errs.Errors) > 0 {
return errs
}
return nil
}
func (p *Provisioner) Provision(ctx context.Context, ui packersdk.Ui, comm packersdk.Communicator, _ map[string]interface{}) error {
var err error
var src, dst string
var formulas []string
if p.config.Formulas != nil && len(p.config.Formulas) > 0 {
ui.Say("Downloading Salt formulas...")
client := new(getter.Client)
for _, i := range p.config.Formulas {
req := getter.Request{
Src: i,
}
// Use //subdirectory name when creating in local_state_tree directory
state := strings.Split(i, "//")
last := state[len(state)-1]
path := filepath.Join(p.config.LocalStateTree, last)
formulas = append(formulas, path)
if _, err := os.Stat(path); os.IsNotExist(err) {
ui.Message(fmt.Sprintf("%s => %s", i, path))
if err = os.Mkdir(path, 0755); err != nil {
return fmt.Errorf("Unable to create Salt state directory: %s", err)
}
req.Dst = path
req.GetMode = getter.ModeAny
if _, err := client.Get(ctx, &req); err != nil {
return fmt.Errorf("Unable to download Salt formula from %s: %s", i, err)
}
} else {
ui.Message(fmt.Sprintf("Found existing formula at: %s", path))
}
}
}
ui.Say("Provisioning with Salt...")
if !p.config.SkipBootstrap {
cmd := &packersdk.RemoteCmd{
// Fallback on wget if curl failed for any reason (such as not being installed)
Command: fmt.Sprintf(p.guestOSTypeConfig.bootstrapFetchCmd),
}
ui.Message(fmt.Sprintf("Downloading saltstack bootstrap to /tmp/install_salt.sh"))
if err = cmd.RunWithUi(ctx, comm, ui); err != nil {
return fmt.Errorf("Unable to download Salt: %s", err)
}
cmd = &packersdk.RemoteCmd{
Command: fmt.Sprintf("%s %s", p.sudo(p.guestOSTypeConfig.bootstrapRunCmd), p.config.BootstrapArgs),
}
ui.Message(fmt.Sprintf("Installing Salt with command %s", cmd.Command))
if err = cmd.RunWithUi(ctx, comm, ui); err != nil {
return fmt.Errorf("Unable to install Salt: %s", err)
}
}
ui.Message(fmt.Sprintf("Creating remote temporary directory: %s", p.config.TempConfigDir))
if err := p.createDir(ui, comm, p.config.TempConfigDir); err != nil {
return fmt.Errorf("Error creating remote temporary directory: %s", err)
}
if p.config.MinionConfig != "" {
ui.Message(fmt.Sprintf("Uploading minion config: %s", p.config.MinionConfig))
src = p.config.MinionConfig
dst = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "minion"))
if err = p.uploadFile(ui, comm, dst, src); err != nil {
return fmt.Errorf("Error uploading local minion config file to remote: %s", err)
}
// move minion config into /etc/salt
ui.Message(fmt.Sprintf("Make sure directory %s exists", p.guestOSTypeConfig.configDir))
if err := p.createDir(ui, comm, p.guestOSTypeConfig.configDir); err != nil {
return fmt.Errorf("Error creating remote salt configuration directory: %s", err)
}
src = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "minion"))
dst = filepath.ToSlash(filepath.Join(p.guestOSTypeConfig.configDir, "minion"))
if err = p.moveFile(ui, comm, dst, src); err != nil {
return fmt.Errorf("Unable to move %s/minion to %s/minion: %s", p.config.TempConfigDir, p.guestOSTypeConfig.configDir, err)
}
}
if p.config.GrainsFile != "" {
ui.Message(fmt.Sprintf("Uploading grains file: %s", p.config.GrainsFile))
src = p.config.GrainsFile
dst = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "grains"))
if err = p.uploadFile(ui, comm, dst, src); err != nil {
return fmt.Errorf("Error uploading local grains file to remote: %s", err)
}
// move grains file into /etc/salt
ui.Message(fmt.Sprintf("Make sure directory %s exists", p.guestOSTypeConfig.configDir))
if err := p.createDir(ui, comm, p.guestOSTypeConfig.configDir); err != nil {
return fmt.Errorf("Error creating remote salt configuration directory: %s", err)
}
src = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "grains"))
dst = filepath.ToSlash(filepath.Join(p.guestOSTypeConfig.configDir, "grains"))
if err = p.moveFile(ui, comm, dst, src); err != nil {
return fmt.Errorf("Unable to move %s/grains to %s/grains: %s", p.config.TempConfigDir, p.guestOSTypeConfig.configDir, err)
}
}
ui.Message(fmt.Sprintf("Uploading local state tree: %s", p.config.LocalStateTree))
src = p.config.LocalStateTree
dst = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "states"))
if err = p.uploadDir(ui, comm, dst, src, []string{".git"}); err != nil {
return fmt.Errorf("Error uploading local state tree to remote: %s", err)
}
// move state tree from temporary directory
src = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "states"))
if p.config.RemoteStateTree != "" {
dst = p.config.RemoteStateTree
} else {
dst = p.guestOSTypeConfig.stateRoot
}
if err = p.statPath(ui, comm, dst); err != nil {
if err = p.removeDir(ui, comm, dst); err != nil {
return fmt.Errorf("Unable to clear salt tree: %s", err)
}
}
if err = p.moveFile(ui, comm, dst, src); err != nil {
return fmt.Errorf("Unable to move %s/states to %s: %s", p.config.TempConfigDir, dst, err)
}
// Remove the local Salt formulas if present
if p.config.Formulas != nil {
for _, f := range formulas {
if _, err := os.Stat(f); !os.IsNotExist(err) && f != p.config.LocalStateTree {
ui.Message(fmt.Sprintf("Removing Salt formula: %s", f))
defer os.RemoveAll(f)
}
}
}
if p.config.LocalPillarRoots != "" {
ui.Message(fmt.Sprintf("Uploading local pillar roots: %s", p.config.LocalPillarRoots))
src = p.config.LocalPillarRoots
dst = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "pillar"))
if err = p.uploadDir(ui, comm, dst, src, []string{".git"}); err != nil {
return fmt.Errorf("Error uploading local pillar roots to remote: %s", err)
}
// move pillar root from temporary directory
src = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "pillar"))
if p.config.RemotePillarRoots != "" {
dst = p.config.RemotePillarRoots
} else {
dst = p.guestOSTypeConfig.pillarRoot
}
if err = p.statPath(ui, comm, dst); err != nil {
if err = p.removeDir(ui, comm, dst); err != nil {
return fmt.Errorf("Unable to clear pillar root: %s", err)
}
}
if err = p.moveFile(ui, comm, dst, src); err != nil {
return fmt.Errorf("Unable to move %s/pillar to %s: %s", p.config.TempConfigDir, dst, err)
}
}
if p.config.GuestOSType == guestexec.WindowsOSType {
ui.Message("Downloading Git for Windows")
cmd1 := &packersdk.RemoteCmd{Command: fmt.Sprintf("powershell [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri https://github.com/git-for-windows/git/releases/download/v2.28.0.windows.1/Git-2.28.0-64-bit.exe -OutFile $env:TEMP/Git.exe")}
if err = cmd1.RunWithUi(ctx, comm, ui); (err != nil || cmd1.ExitStatus() != 0) && !p.config.NoExitOnFailure {
if err == nil {
err = fmt.Errorf("Bad exit status: %d", cmd1.ExitStatus())
}
return fmt.Errorf("Unable to Download Git for Windows: %s", err)
}
ui.Message("Installing Git for Windows")
cmd2 := &packersdk.RemoteCmd{Command: fmt.Sprintf("powershell Start-Process -FilePath $env:TEMP/Git.exe /SILENT -Wait")}
if err = cmd2.RunWithUi(ctx, comm, ui); (err != nil || cmd2.ExitStatus() != 0) && !p.config.NoExitOnFailure {
if err == nil {
err = fmt.Errorf("Bad exit status: %d", cmd2.ExitStatus())
}
return fmt.Errorf("Unable to Install Git for Windows: %s", err)
}
ui.Message("Cleaning Up After Git for Windows")
cmd3 := &packersdk.RemoteCmd{Command: fmt.Sprintf("powershell Remove-Item $env:TEMP/Git.exe")}
if err = cmd3.RunWithUi(ctx, comm, ui); (err != nil || cmd3.ExitStatus() != 0) && !p.config.NoExitOnFailure {
if err == nil {
err = fmt.Errorf("Bad exit status: %d", cmd3.ExitStatus())
}
return fmt.Errorf("Unable to Clean-up After Git for Windows: %s", err)
}
ui.Message("Running salt-call --local winrepo.update_git_repos")
cmd4 := &packersdk.RemoteCmd{Command: fmt.Sprintf("%s --local winrepo.update_git_repos", filepath.Join(p.config.SaltBinDir, "salt-call"))}
if err = cmd4.RunWithUi(ctx, comm, ui); (err != nil || cmd4.ExitStatus() != 0) && !p.config.NoExitOnFailure {
if err == nil {
err = fmt.Errorf("Bad exit status: %d", cmd4.ExitStatus())
}
return fmt.Errorf("Error executing salt-call --local winrepo.update_git_repos: %s", err)
}
ui.Message("Running salt-call --local pkg.refresh_db")
cmd5 := &packersdk.RemoteCmd{Command: fmt.Sprintf("%s --local pkg.refresh_db", filepath.Join(p.config.SaltBinDir, "salt-call"))}
if err = cmd5.RunWithUi(ctx, comm, ui); (err != nil || cmd5.ExitStatus() != 0) && !p.config.NoExitOnFailure {
if err == nil {
err = fmt.Errorf("Bad exit status: %d", cmd5.ExitStatus())
}
return fmt.Errorf("Error executing salt-call --local pkg.refresh_db: %s", err)
}
}
ui.Message(fmt.Sprintf("Running: salt-call --local %s", p.config.CmdArgs))
cmd := &packersdk.RemoteCmd{Command: p.sudo(fmt.Sprintf("%s --local %s", filepath.Join(p.config.SaltBinDir, "salt-call"), p.config.CmdArgs))}
if err = cmd.RunWithUi(ctx, comm, ui); (err != nil || cmd.ExitStatus() != 0) && !p.config.NoExitOnFailure {
if err == nil {
err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus())
}
return fmt.Errorf("Error executing salt-call: %s", err)
}
return nil
}
// Prepends sudo to supplied command if config says to
func (p *Provisioner) sudo(cmd string) string {
if p.config.DisableSudo || (p.config.GuestOSType == guestexec.WindowsOSType) {
return cmd
}
return "sudo " + cmd
}
func validateDirConfig(path string, name string, required bool) error {
if required && path == "" {
return fmt.Errorf("%s cannot be empty", name)
} else if required == false && path == "" {
return nil
}
info, err := os.Stat(path)
if err != nil {
return fmt.Errorf("%s: path '%s' is invalid: %s", name, path, err)
} else if !info.IsDir() {
return fmt.Errorf("%s: path '%s' must point to a directory", name, path)
}
return nil
}
func validateFileConfig(path string, name string, required bool) error {
if required == true && path == "" {
return fmt.Errorf("%s cannot be empty", name)
} else if required == false && path == "" {
return nil
}
info, err := os.Stat(path)
if err != nil {
return fmt.Errorf("%s: path '%s' is invalid: %s", name, path, err)
} else if info.IsDir() {
return fmt.Errorf("%s: path '%s' must point to a file", name, path)
}
return nil
}
func hasValidFormulaURLs(s []string) bool {
re := regexp.MustCompile(`^(.*).git\/\/[a-zA-Z0-9-_]+(\?.*)?$`)
for _, u := range s {
if !re.MatchString(u) {
return false
}
}
return true
}
func (p *Provisioner) uploadFile(ui packersdk.Ui, comm packersdk.Communicator, dst, src string) error {
f, err := os.Open(src)
if err != nil {
return fmt.Errorf("Error opening: %s", err)
}
defer f.Close()
_, temp_dst := filepath.Split(dst)
if err = comm.Upload(temp_dst, f, nil); err != nil {
return fmt.Errorf("Error uploading %s: %s", src, err)
}
err = p.moveFile(ui, comm, dst, temp_dst)
if err != nil {
return fmt.Errorf("Error moving file to destination: %s", err)
}
return nil
}
func (p *Provisioner) moveFile(ui packersdk.Ui, comm packersdk.Communicator, dst string, src string) error {
ctx := context.TODO()
ui.Message(fmt.Sprintf("Moving %s to %s", src, dst))
cmd := &packersdk.RemoteCmd{
Command: p.sudo(p.guestCommands.MovePath(src, dst)),
}
if err := cmd.RunWithUi(ctx, comm, ui); err != nil || cmd.ExitStatus() != 0 {
if err == nil {
err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus())
}
return fmt.Errorf("Unable to move %s to %s: %s", src, dst, err)
}
return nil
}
func (p *Provisioner) createDir(ui packersdk.Ui, comm packersdk.Communicator, dir string) error {
ui.Message(fmt.Sprintf("Creating directory: %s", dir))
cmd := &packersdk.RemoteCmd{
Command: p.guestCommands.CreateDir(dir),
}
ctx := context.TODO()
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) statPath(ui packersdk.Ui, comm packersdk.Communicator, path string) error {
ctx := context.TODO()
ui.Message(fmt.Sprintf("Verifying Path: %s", path))
cmd := &packersdk.RemoteCmd{
Command: p.guestCommands.StatPath(path),
}
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) removeDir(ui packersdk.Ui, comm packersdk.Communicator, dir string) error {
ctx := context.TODO()
ui.Message(fmt.Sprintf("Removing directory: %s", dir))
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) uploadDir(ui packersdk.Ui, comm packersdk.Communicator, dst, src string, ignore []string) error {
_, temp_dst := filepath.Split(dst)
if err := comm.UploadDir(temp_dst, src, ignore); err != nil {
return err
}
return p.moveFile(ui, comm, dst, temp_dst)
}