added inspec.io provisioner
This commit is contained in:
parent
65b3e67951
commit
f5b13e3cb5
2
Makefile
2
Makefile
|
@ -11,7 +11,7 @@ GOPATH=$(shell go env GOPATH)
|
||||||
# gofmt
|
# gofmt
|
||||||
UNFORMATTED_FILES=$(shell find . -not -path "./vendor/*" -name "*.go" | xargs gofmt -s -l)
|
UNFORMATTED_FILES=$(shell find . -not -path "./vendor/*" -name "*.go" | xargs gofmt -s -l)
|
||||||
|
|
||||||
EXECUTABLE_FILES=$(shell find . -type f -executable | egrep -v '^\./(website/[vendor|tmp]|vendor/|\.git|bin/|scripts/|pkg/)' | egrep -v '.*(\.sh|\.bats|\.git)' | egrep -v './provisioner/ansible/test-fixtures/exit1')
|
EXECUTABLE_FILES=$(shell find . -type f -executable | egrep -v '^\./(website/[vendor|tmp]|vendor/|\.git|bin/|scripts/|pkg/)' | egrep -v '.*(\.sh|\.bats|\.git)' | egrep -v './provisioner/(ansible|inspec)/test-fixtures/exit1')
|
||||||
|
|
||||||
# Get the git commit
|
# Get the git commit
|
||||||
GIT_DIRTY=$(shell test -n "`git status --porcelain`" && echo "+CHANGES" || true)
|
GIT_DIRTY=$(shell test -n "`git status --porcelain`" && echo "+CHANGES" || true)
|
||||||
|
|
|
@ -71,6 +71,7 @@ import (
|
||||||
chefsoloprovisioner "github.com/hashicorp/packer/provisioner/chef-solo"
|
chefsoloprovisioner "github.com/hashicorp/packer/provisioner/chef-solo"
|
||||||
convergeprovisioner "github.com/hashicorp/packer/provisioner/converge"
|
convergeprovisioner "github.com/hashicorp/packer/provisioner/converge"
|
||||||
fileprovisioner "github.com/hashicorp/packer/provisioner/file"
|
fileprovisioner "github.com/hashicorp/packer/provisioner/file"
|
||||||
|
inspecprovisioner "github.com/hashicorp/packer/provisioner/inspec"
|
||||||
powershellprovisioner "github.com/hashicorp/packer/provisioner/powershell"
|
powershellprovisioner "github.com/hashicorp/packer/provisioner/powershell"
|
||||||
puppetmasterlessprovisioner "github.com/hashicorp/packer/provisioner/puppet-masterless"
|
puppetmasterlessprovisioner "github.com/hashicorp/packer/provisioner/puppet-masterless"
|
||||||
puppetserverprovisioner "github.com/hashicorp/packer/provisioner/puppet-server"
|
puppetserverprovisioner "github.com/hashicorp/packer/provisioner/puppet-server"
|
||||||
|
@ -130,6 +131,7 @@ var Provisioners = map[string]packer.Provisioner{
|
||||||
"chef-solo": new(chefsoloprovisioner.Provisioner),
|
"chef-solo": new(chefsoloprovisioner.Provisioner),
|
||||||
"converge": new(convergeprovisioner.Provisioner),
|
"converge": new(convergeprovisioner.Provisioner),
|
||||||
"file": new(fileprovisioner.Provisioner),
|
"file": new(fileprovisioner.Provisioner),
|
||||||
|
"inspec": new(inspecprovisioner.Provisioner),
|
||||||
"powershell": new(powershellprovisioner.Provisioner),
|
"powershell": new(powershellprovisioner.Provisioner),
|
||||||
"puppet-masterless": new(puppetmasterlessprovisioner.Provisioner),
|
"puppet-masterless": new(puppetmasterlessprovisioner.Provisioner),
|
||||||
"puppet-server": new(puppetserverprovisioner.Provisioner),
|
"puppet-server": new(puppetserverprovisioner.Provisioner),
|
||||||
|
|
|
@ -0,0 +1,285 @@
|
||||||
|
package inspec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer/packer"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
// An adapter satisfies SSH requests (from an Inspec client) by delegating SSH
|
||||||
|
// exec and subsystem commands to a packer.Communicator.
|
||||||
|
type adapter struct {
|
||||||
|
done <-chan struct{}
|
||||||
|
l net.Listener
|
||||||
|
config *ssh.ServerConfig
|
||||||
|
ui packer.Ui
|
||||||
|
comm packer.Communicator
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAdapter(done <-chan struct{}, l net.Listener, config *ssh.ServerConfig, ui packer.Ui, comm packer.Communicator) *adapter {
|
||||||
|
return &adapter{
|
||||||
|
done: done,
|
||||||
|
l: l,
|
||||||
|
config: config,
|
||||||
|
ui: ui,
|
||||||
|
comm: comm,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *adapter) Serve() {
|
||||||
|
log.Printf("SSH proxy: serving on %s", c.l.Addr())
|
||||||
|
|
||||||
|
for {
|
||||||
|
// Accept will return if either the underlying connection is closed or if a connection is made.
|
||||||
|
// after returning, check to see if c.done can be received. If so, then Accept() returned because
|
||||||
|
// the connection has been closed.
|
||||||
|
conn, err := c.l.Accept()
|
||||||
|
select {
|
||||||
|
case <-c.done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
if err != nil {
|
||||||
|
c.ui.Error(fmt.Sprintf("listen.Accept failed: %v", err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
go func(conn net.Conn) {
|
||||||
|
if err := c.Handle(conn, c.ui); err != nil {
|
||||||
|
c.ui.Error(err.Error())
|
||||||
|
}
|
||||||
|
}(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *adapter) Handle(conn net.Conn, ui packer.Ui) error {
|
||||||
|
log.Print("SSH proxy: accepted connection")
|
||||||
|
_, chans, reqs, err := ssh.NewServerConn(conn, c.config)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to handshake")
|
||||||
|
}
|
||||||
|
|
||||||
|
// discard all global requests
|
||||||
|
go ssh.DiscardRequests(reqs)
|
||||||
|
|
||||||
|
// Service the incoming NewChannels
|
||||||
|
for newChannel := range chans {
|
||||||
|
if newChannel.ChannelType() != "session" {
|
||||||
|
newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
go func(ch ssh.NewChannel) {
|
||||||
|
if err := c.handleSession(ch); err != nil {
|
||||||
|
c.ui.Error(err.Error())
|
||||||
|
}
|
||||||
|
}(newChannel)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *adapter) handleSession(newChannel ssh.NewChannel) error {
|
||||||
|
channel, requests, err := newChannel.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer channel.Close()
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
|
||||||
|
// Sessions have requests such as "pty-req", "shell", "env", and "exec".
|
||||||
|
// see RFC 4254, section 6
|
||||||
|
go func(in <-chan *ssh.Request) {
|
||||||
|
env := make([]envRequestPayload, 4)
|
||||||
|
for req := range in {
|
||||||
|
switch req.Type {
|
||||||
|
case "pty-req":
|
||||||
|
log.Println("inspec provisioner pty-req request")
|
||||||
|
// accept pty-req requests, but don't actually do anything. Necessary for OpenSSH and sudo.
|
||||||
|
req.Reply(true, nil)
|
||||||
|
|
||||||
|
case "env":
|
||||||
|
req, err := newEnvRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
c.ui.Error(err.Error())
|
||||||
|
req.Reply(false, nil)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
env = append(env, req.Payload)
|
||||||
|
log.Printf("new env request: %s", req.Payload)
|
||||||
|
req.Reply(true, nil)
|
||||||
|
case "exec":
|
||||||
|
req, err := newExecRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
c.ui.Error(err.Error())
|
||||||
|
req.Reply(false, nil)
|
||||||
|
close(done)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("new exec request: %s", req.Payload)
|
||||||
|
|
||||||
|
if len(req.Payload) == 0 {
|
||||||
|
req.Reply(false, nil)
|
||||||
|
close(done)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func(channel ssh.Channel) {
|
||||||
|
exit := c.exec(string(req.Payload), channel, channel, channel.Stderr())
|
||||||
|
|
||||||
|
exitStatus := make([]byte, 4)
|
||||||
|
binary.BigEndian.PutUint32(exitStatus, uint32(exit))
|
||||||
|
channel.SendRequest("exit-status", false, exitStatus)
|
||||||
|
close(done)
|
||||||
|
}(channel)
|
||||||
|
req.Reply(true, nil)
|
||||||
|
case "subsystem":
|
||||||
|
req, err := newSubsystemRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
c.ui.Error(err.Error())
|
||||||
|
req.Reply(false, nil)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("new subsystem request: %s", req.Payload)
|
||||||
|
|
||||||
|
c.ui.Error(fmt.Sprintf("unsupported subsystem requested: %s", req.Payload))
|
||||||
|
req.Reply(false, nil)
|
||||||
|
default:
|
||||||
|
log.Printf("rejecting %s request", req.Type)
|
||||||
|
req.Reply(false, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(requests)
|
||||||
|
|
||||||
|
<-done
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *adapter) Shutdown() {
|
||||||
|
c.l.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *adapter) exec(command string, in io.Reader, out io.Writer, err io.Writer) int {
|
||||||
|
var exitStatus int
|
||||||
|
exitStatus = c.remoteExec(command, in, out, err)
|
||||||
|
return exitStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *adapter) remoteExec(command string, in io.Reader, out io.Writer, err io.Writer) int {
|
||||||
|
cmd := &packer.RemoteCmd{
|
||||||
|
Stdin: in,
|
||||||
|
Stdout: out,
|
||||||
|
Stderr: err,
|
||||||
|
Command: command,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.comm.Start(cmd); err != nil {
|
||||||
|
c.ui.Error(err.Error())
|
||||||
|
return cmd.ExitStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Wait()
|
||||||
|
|
||||||
|
return cmd.ExitStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
type envRequest struct {
|
||||||
|
*ssh.Request
|
||||||
|
Payload envRequestPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
type envRequestPayload struct {
|
||||||
|
Name string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p envRequestPayload) String() string {
|
||||||
|
return fmt.Sprintf("%s=%s", p.Name, p.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEnvRequest(raw *ssh.Request) (*envRequest, error) {
|
||||||
|
r := new(envRequest)
|
||||||
|
r.Request = raw
|
||||||
|
|
||||||
|
if err := ssh.Unmarshal(raw.Payload, &r.Payload); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sshString(buf io.Reader) (string, error) {
|
||||||
|
var size uint32
|
||||||
|
err := binary.Read(buf, binary.BigEndian, &size)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
b := make([]byte, size)
|
||||||
|
err = binary.Read(buf, binary.BigEndian, b)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type execRequest struct {
|
||||||
|
*ssh.Request
|
||||||
|
Payload execRequestPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
type execRequestPayload string
|
||||||
|
|
||||||
|
func (p execRequestPayload) String() string {
|
||||||
|
return string(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newExecRequest(raw *ssh.Request) (*execRequest, error) {
|
||||||
|
r := new(execRequest)
|
||||||
|
r.Request = raw
|
||||||
|
buf := bytes.NewReader(r.Request.Payload)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var payload string
|
||||||
|
if payload, err = sshString(buf); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Payload = execRequestPayload(payload)
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type subsystemRequest struct {
|
||||||
|
*ssh.Request
|
||||||
|
Payload subsystemRequestPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
type subsystemRequestPayload string
|
||||||
|
|
||||||
|
func (p subsystemRequestPayload) String() string {
|
||||||
|
return string(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSubsystemRequest(raw *ssh.Request) (*subsystemRequest, error) {
|
||||||
|
r := new(subsystemRequest)
|
||||||
|
r.Request = raw
|
||||||
|
buf := bytes.NewReader(r.Request.Payload)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var payload string
|
||||||
|
if payload, err = sshString(buf); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Payload = subsystemRequestPayload(payload)
|
||||||
|
return r, nil
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
package inspec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer/packer"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAdapter_Serve(t *testing.T) {
|
||||||
|
|
||||||
|
// done signals the adapter that the provisioner is done
|
||||||
|
done := make(chan struct{})
|
||||||
|
|
||||||
|
acceptC := make(chan struct{})
|
||||||
|
l := listener{done: make(chan struct{}), acceptC: acceptC}
|
||||||
|
|
||||||
|
config := &ssh.ServerConfig{}
|
||||||
|
|
||||||
|
ui := new(packer.NoopUi)
|
||||||
|
|
||||||
|
sut := newAdapter(done, &l, config, newUi(ui), communicator{})
|
||||||
|
go func() {
|
||||||
|
i := 0
|
||||||
|
for range acceptC {
|
||||||
|
i++
|
||||||
|
if i == 4 {
|
||||||
|
close(done)
|
||||||
|
l.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
sut.Serve()
|
||||||
|
}
|
||||||
|
|
||||||
|
type listener struct {
|
||||||
|
done chan struct{}
|
||||||
|
acceptC chan<- struct{}
|
||||||
|
i int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *listener) Accept() (net.Conn, error) {
|
||||||
|
log.Println("Accept() called")
|
||||||
|
l.acceptC <- struct{}{}
|
||||||
|
select {
|
||||||
|
case <-l.done:
|
||||||
|
log.Println("done, serving an error")
|
||||||
|
return nil, errors.New("listener is closed")
|
||||||
|
|
||||||
|
case <-time.After(10 * time.Millisecond):
|
||||||
|
l.i++
|
||||||
|
|
||||||
|
if l.i%2 == 0 {
|
||||||
|
c1, c2 := net.Pipe()
|
||||||
|
|
||||||
|
go func(c net.Conn) {
|
||||||
|
<-time.After(100 * time.Millisecond)
|
||||||
|
log.Println("closing c")
|
||||||
|
c.Close()
|
||||||
|
}(c1)
|
||||||
|
|
||||||
|
return c2, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("accept error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *listener) Close() error {
|
||||||
|
close(l.done)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *listener) Addr() net.Addr {
|
||||||
|
return addr{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type addr struct{}
|
||||||
|
|
||||||
|
func (a addr) Network() string {
|
||||||
|
return a.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a addr) String() string {
|
||||||
|
return "test"
|
||||||
|
}
|
||||||
|
|
||||||
|
type communicator struct{}
|
||||||
|
|
||||||
|
func (c communicator) Start(*packer.RemoteCmd) error {
|
||||||
|
return errors.New("communicator not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c communicator) Upload(string, io.Reader, *os.FileInfo) error {
|
||||||
|
return errors.New("communicator not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c communicator) UploadDir(dst string, src string, exclude []string) error {
|
||||||
|
return errors.New("communicator not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c communicator) Download(string, io.Writer) error {
|
||||||
|
return errors.New("communicator not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c communicator) DownloadDir(src string, dst string, exclude []string) error {
|
||||||
|
return errors.New("communicator not supported")
|
||||||
|
}
|
|
@ -0,0 +1,563 @@
|
||||||
|
package inspec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/user"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer/common"
|
||||||
|
"github.com/hashicorp/packer/helper/config"
|
||||||
|
"github.com/hashicorp/packer/packer"
|
||||||
|
"github.com/hashicorp/packer/template/interpolate"
|
||||||
|
)
|
||||||
|
|
||||||
|
var SupportedBackends = map[string]bool{"docker": true, "local": true, "ssh": true, "winrm": true}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
common.PackerConfig `mapstructure:",squash"`
|
||||||
|
ctx interpolate.Context
|
||||||
|
|
||||||
|
// The command to run inspec
|
||||||
|
Command string
|
||||||
|
SubCommand string
|
||||||
|
|
||||||
|
// Extra options to pass to the inspec command
|
||||||
|
ExtraArguments []string `mapstructure:"extra_arguments"`
|
||||||
|
InspecEnvVars []string `mapstructure:"inspec_env_vars"`
|
||||||
|
|
||||||
|
// The profile to execute.
|
||||||
|
Profile string `mapstructure:"profile"`
|
||||||
|
AttributesDirectory string `mapstructure:"attributes_directory"`
|
||||||
|
AttributesFiles []string `mapstructure:"attributes"`
|
||||||
|
Backend string `mapstructure:"backend"`
|
||||||
|
User string `mapstructure:"user"`
|
||||||
|
Host string `mapstructure:"host"`
|
||||||
|
LocalPort string `mapstructure:"local_port"`
|
||||||
|
SSHHostKeyFile string `mapstructure:"ssh_host_key_file"`
|
||||||
|
SSHAuthorizedKeyFile string `mapstructure:"ssh_authorized_key_file"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Provisioner struct {
|
||||||
|
config Config
|
||||||
|
adapter *adapter
|
||||||
|
done chan struct{}
|
||||||
|
inspecVersion string
|
||||||
|
inspecMajVersion uint
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provisioner) Prepare(raws ...interface{}) error {
|
||||||
|
p.done = make(chan struct{})
|
||||||
|
|
||||||
|
err := config.Decode(&p.config, &config.DecodeOpts{
|
||||||
|
Interpolate: true,
|
||||||
|
InterpolateContext: &p.config.ctx,
|
||||||
|
InterpolateFilter: &interpolate.RenderFilter{
|
||||||
|
Exclude: []string{},
|
||||||
|
},
|
||||||
|
}, raws...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defaults
|
||||||
|
if p.config.Command == "" {
|
||||||
|
p.config.Command = "inspec"
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.config.SubCommand == "" {
|
||||||
|
p.config.SubCommand = "exec"
|
||||||
|
}
|
||||||
|
|
||||||
|
var errs *packer.MultiError
|
||||||
|
err = validateProfileConfig(p.config.Profile)
|
||||||
|
if err != nil {
|
||||||
|
errs = packer.MultiErrorAppend(errs, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the authorized key file exists
|
||||||
|
if len(p.config.SSHAuthorizedKeyFile) > 0 {
|
||||||
|
err = validateFileConfig(p.config.SSHAuthorizedKeyFile, "ssh_authorized_key_file", true)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(p.config.SSHAuthorizedKeyFile, "does not exist")
|
||||||
|
errs = packer.MultiErrorAppend(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(p.config.SSHHostKeyFile) > 0 {
|
||||||
|
err = validateFileConfig(p.config.SSHHostKeyFile, "ssh_host_key_file", true)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(p.config.SSHHostKeyFile, "does not exist")
|
||||||
|
errs = packer.MultiErrorAppend(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := SupportedBackends[p.config.Backend]; !ok {
|
||||||
|
errs = packer.MultiErrorAppend(errs, fmt.Errorf("backend: %s must be a valid backend", p.config.Backend))
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.config.Backend == "" {
|
||||||
|
p.config.Backend = "ssh"
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.config.Host == "" {
|
||||||
|
p.config.Host = "127.0.0.1"
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(p.config.LocalPort) > 0 {
|
||||||
|
if _, err := strconv.ParseUint(p.config.LocalPort, 10, 16); err != nil {
|
||||||
|
errs = packer.MultiErrorAppend(errs, fmt.Errorf("local_port: %s must be a valid port", p.config.LocalPort))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p.config.LocalPort = "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(p.config.AttributesDirectory) > 0 {
|
||||||
|
err = validateDirectoryConfig(p.config.AttributesDirectory, "attrs")
|
||||||
|
if err != nil {
|
||||||
|
log.Println(p.config.AttributesDirectory, "does not exist")
|
||||||
|
errs = packer.MultiErrorAppend(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.config.User == "" {
|
||||||
|
usr, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
errs = packer.MultiErrorAppend(errs, err)
|
||||||
|
} else {
|
||||||
|
p.config.User = usr.Username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p.config.User == "" {
|
||||||
|
errs = packer.MultiErrorAppend(errs, fmt.Errorf("user: could not determine current user from environment."))
|
||||||
|
}
|
||||||
|
|
||||||
|
if errs != nil && len(errs.Errors) > 0 {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provisioner) getVersion() error {
|
||||||
|
out, err := exec.Command(p.config.Command, "version").Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"Error running \"%s version\": %s", p.config.Command, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
versionRe := regexp.MustCompile(`\w (\d+\.\d+[.\d+]*)`)
|
||||||
|
matches := versionRe.FindStringSubmatch(string(out))
|
||||||
|
if matches == nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"Could not find %s version in output:\n%s", p.config.Command, string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
version := matches[1]
|
||||||
|
log.Printf("%s version: %s", p.config.Command, version)
|
||||||
|
p.inspecVersion = version
|
||||||
|
|
||||||
|
majVer, err := strconv.ParseUint(strings.Split(version, ".")[0], 10, 0)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Could not parse major version from \"%s\".", version)
|
||||||
|
}
|
||||||
|
p.inspecMajVersion = uint(majVer)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
|
||||||
|
ui.Say("Provisioning with Inspec...")
|
||||||
|
|
||||||
|
for i, envVar := range p.config.InspecEnvVars {
|
||||||
|
envVar, err := interpolate.Render(envVar, &p.config.ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Could not interpolate inspec env vars: %s", err)
|
||||||
|
}
|
||||||
|
p.config.InspecEnvVars[i] = envVar
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, arg := range p.config.ExtraArguments {
|
||||||
|
arg, err := interpolate.Render(arg, &p.config.ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Could not interpolate inspec extra arguments: %s", err)
|
||||||
|
}
|
||||||
|
p.config.ExtraArguments[i] = arg
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, arg := range p.config.AttributesFiles {
|
||||||
|
arg, err := interpolate.Render(arg, &p.config.ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Could not interpolate inspec attributes: %s", err)
|
||||||
|
}
|
||||||
|
p.config.AttributesFiles[i] = arg
|
||||||
|
}
|
||||||
|
|
||||||
|
k, err := newUserKey(p.config.SSHAuthorizedKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hostSigner, err := newSigner(p.config.SSHHostKeyFile)
|
||||||
|
// Remove the private key file
|
||||||
|
if len(k.privKeyFile) > 0 {
|
||||||
|
defer os.Remove(k.privKeyFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
keyChecker := ssh.CertChecker{
|
||||||
|
UserKeyFallback: func(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
|
||||||
|
if user := conn.User(); user != p.config.User {
|
||||||
|
return nil, errors.New(fmt.Sprintf("authentication failed: %s is not a valid user", user))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(k.Marshal(), pubKey.Marshal()) {
|
||||||
|
return nil, errors.New("authentication failed: unauthorized key")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &ssh.ServerConfig{
|
||||||
|
AuthLogCallback: func(conn ssh.ConnMetadata, method string, err error) {
|
||||||
|
log.Printf("authentication attempt from %s to %s as %s using %s", conn.RemoteAddr(), conn.LocalAddr(), conn.User(), method)
|
||||||
|
},
|
||||||
|
PublicKeyCallback: keyChecker.Authenticate,
|
||||||
|
//NoClientAuth: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
config.AddHostKey(hostSigner)
|
||||||
|
|
||||||
|
localListener, err := func() (net.Listener, error) {
|
||||||
|
port, err := strconv.ParseUint(p.config.LocalPort, 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tries := 1
|
||||||
|
if port != 0 {
|
||||||
|
tries = 10
|
||||||
|
}
|
||||||
|
for i := 0; i < tries; i++ {
|
||||||
|
l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port))
|
||||||
|
port++
|
||||||
|
if err != nil {
|
||||||
|
ui.Say(err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, p.config.LocalPort, err = net.SplitHostPort(l.Addr().String())
|
||||||
|
if err != nil {
|
||||||
|
ui.Say(err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return l, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("Error setting up SSH proxy connection")
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ui = newUi(ui)
|
||||||
|
p.adapter = newAdapter(p.done, localListener, config, ui, comm)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
log.Print("shutting down the SSH proxy")
|
||||||
|
close(p.done)
|
||||||
|
p.adapter.Shutdown()
|
||||||
|
}()
|
||||||
|
|
||||||
|
go p.adapter.Serve()
|
||||||
|
|
||||||
|
tf, err := ioutil.TempFile(p.config.AttributesDirectory, "packer-provisioner-inspec.*.yml")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error preparing packer attributes file: %s", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tf.Name())
|
||||||
|
|
||||||
|
w := bufio.NewWriter(tf)
|
||||||
|
w.WriteString(fmt.Sprintf("packer_build_name: %s\n", p.config.PackerBuildName))
|
||||||
|
w.WriteString(fmt.Sprintf("packer_builder_type: %s\n", p.config.PackerBuilderType))
|
||||||
|
|
||||||
|
if err := w.Flush(); err != nil {
|
||||||
|
tf.Close()
|
||||||
|
return fmt.Errorf("Error preparing packer attributes file: %s", err)
|
||||||
|
}
|
||||||
|
tf.Close()
|
||||||
|
p.config.AttributesFiles = append(p.config.AttributesFiles, tf.Name())
|
||||||
|
|
||||||
|
if err := p.executeInspec(ui, comm, k.privKeyFile); err != nil {
|
||||||
|
return fmt.Errorf("Error executing Inspec: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (p *Provisioner) Cancel() {
|
||||||
|
if p.done != nil {
|
||||||
|
close(p.done)
|
||||||
|
}
|
||||||
|
if p.adapter != nil {
|
||||||
|
p.adapter.Shutdown()
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provisioner) executeInspec(ui packer.Ui, comm packer.Communicator, privKeyFile string) error {
|
||||||
|
var envvars []string
|
||||||
|
|
||||||
|
args := []string{p.config.SubCommand, p.config.Profile}
|
||||||
|
args = append(args, "--backend", p.config.Backend)
|
||||||
|
args = append(args, "--host", p.config.Host)
|
||||||
|
|
||||||
|
if p.config.Backend == "ssh" {
|
||||||
|
if len(privKeyFile) > 0 {
|
||||||
|
args = append(args, "--key-files", privKeyFile)
|
||||||
|
}
|
||||||
|
args = append(args, "--user", p.config.User)
|
||||||
|
args = append(args, "--port", p.config.LocalPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, "--attrs")
|
||||||
|
args = append(args, p.config.AttributesFiles...)
|
||||||
|
args = append(args, p.config.ExtraArguments...)
|
||||||
|
|
||||||
|
if len(p.config.InspecEnvVars) > 0 {
|
||||||
|
envvars = append(envvars, p.config.InspecEnvVars...)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(p.config.Command, args...)
|
||||||
|
|
||||||
|
cmd.Env = os.Environ()
|
||||||
|
if len(envvars) > 0 {
|
||||||
|
cmd.Env = append(cmd.Env, envvars...)
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stderr, err := cmd.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
repeat := func(r io.ReadCloser) {
|
||||||
|
reader := bufio.NewReader(r)
|
||||||
|
for {
|
||||||
|
line, err := reader.ReadString('\n')
|
||||||
|
if line != "" {
|
||||||
|
line = strings.TrimRightFunc(line, unicode.IsSpace)
|
||||||
|
ui.Message(line)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
ui.Error(err.Error())
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
wg.Add(2)
|
||||||
|
go repeat(stdout)
|
||||||
|
go repeat(stderr)
|
||||||
|
|
||||||
|
ui.Say(fmt.Sprintf("Executing Inspec: %s", strings.Join(cmd.Args, " ")))
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
err = cmd.Wait()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Non-zero exit status: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateFileConfig(name string, config string, req bool) error {
|
||||||
|
if req {
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("%s must be specified.", config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info, err := os.Stat(name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %s is invalid: %s", config, name, err)
|
||||||
|
} else if info.IsDir() {
|
||||||
|
return fmt.Errorf("%s: %s must point to a file", config, name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateProfileConfig(name string) error {
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("profile must be specified.")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateDirectoryConfig(name string, config string) error {
|
||||||
|
info, err := os.Stat(name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %s is invalid: %s", config, name, err)
|
||||||
|
} else if !info.IsDir() {
|
||||||
|
return fmt.Errorf("%s: %s must point to a directory", config, name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type userKey struct {
|
||||||
|
ssh.PublicKey
|
||||||
|
privKeyFile string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUserKey(pubKeyFile string) (*userKey, error) {
|
||||||
|
userKey := new(userKey)
|
||||||
|
if len(pubKeyFile) > 0 {
|
||||||
|
pubKeyBytes, err := ioutil.ReadFile(pubKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("Failed to read public key")
|
||||||
|
}
|
||||||
|
userKey.PublicKey, _, _, _, err = ssh.ParseAuthorizedKey(pubKeyBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("Failed to parse authorized key")
|
||||||
|
}
|
||||||
|
|
||||||
|
return userKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("Failed to generate key pair")
|
||||||
|
}
|
||||||
|
userKey.PublicKey, err = ssh.NewPublicKey(key.Public())
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("Failed to extract public key from generated key pair")
|
||||||
|
}
|
||||||
|
|
||||||
|
// To support Inspec calling back to us we need to write
|
||||||
|
// this file down
|
||||||
|
privateKeyDer := x509.MarshalPKCS1PrivateKey(key)
|
||||||
|
privateKeyBlock := pem.Block{
|
||||||
|
Type: "RSA PRIVATE KEY",
|
||||||
|
Headers: nil,
|
||||||
|
Bytes: privateKeyDer,
|
||||||
|
}
|
||||||
|
tf, err := ioutil.TempFile("", "packer-provisioner-inspec.*.key")
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("failed to create temp file for generated key")
|
||||||
|
}
|
||||||
|
_, err = tf.Write(pem.EncodeToMemory(&privateKeyBlock))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("failed to write private key to temp file")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tf.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("failed to close private key temp file")
|
||||||
|
}
|
||||||
|
userKey.privKeyFile = tf.Name()
|
||||||
|
|
||||||
|
return userKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type signer struct {
|
||||||
|
ssh.Signer
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSigner(privKeyFile string) (*signer, error) {
|
||||||
|
signer := new(signer)
|
||||||
|
|
||||||
|
if len(privKeyFile) > 0 {
|
||||||
|
privateBytes, err := ioutil.ReadFile(privKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("Failed to load private host key")
|
||||||
|
}
|
||||||
|
|
||||||
|
signer.Signer, err = ssh.ParsePrivateKey(privateBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("Failed to parse private host key")
|
||||||
|
}
|
||||||
|
|
||||||
|
return signer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("Failed to generate server key pair")
|
||||||
|
}
|
||||||
|
|
||||||
|
signer.Signer, err = ssh.NewSignerFromKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("Failed to extract private key from generated key pair")
|
||||||
|
}
|
||||||
|
|
||||||
|
return signer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ui provides concurrency-safe access to packer.Ui.
|
||||||
|
type Ui struct {
|
||||||
|
sem chan int
|
||||||
|
ui packer.Ui
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUi(ui packer.Ui) packer.Ui {
|
||||||
|
return &Ui{sem: make(chan int, 1), ui: ui}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *Ui) Ask(s string) (string, error) {
|
||||||
|
ui.sem <- 1
|
||||||
|
ret, err := ui.ui.Ask(s)
|
||||||
|
<-ui.sem
|
||||||
|
|
||||||
|
return ret, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *Ui) Say(s string) {
|
||||||
|
ui.sem <- 1
|
||||||
|
ui.ui.Say(s)
|
||||||
|
<-ui.sem
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *Ui) Message(s string) {
|
||||||
|
ui.sem <- 1
|
||||||
|
ui.ui.Message(s)
|
||||||
|
<-ui.sem
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *Ui) Error(s string) {
|
||||||
|
ui.sem <- 1
|
||||||
|
ui.ui.Error(s)
|
||||||
|
<-ui.sem
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *Ui) Machine(t string, args ...string) {
|
||||||
|
ui.sem <- 1
|
||||||
|
ui.ui.Machine(t, args...)
|
||||||
|
<-ui.sem
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *Ui) ProgressBar() packer.ProgressBar {
|
||||||
|
return new(packer.NoopProgressBar)
|
||||||
|
}
|
|
@ -0,0 +1,293 @@
|
||||||
|
package inspec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Be sure to remove the InSpec stub file in each test with:
|
||||||
|
// defer os.Remove(config["command"].(string))
|
||||||
|
func testConfig(t *testing.T) map[string]interface{} {
|
||||||
|
m := make(map[string]interface{})
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
inspec_stub := path.Join(wd, "packer-inspec-stub.sh")
|
||||||
|
|
||||||
|
err = ioutil.WriteFile(inspec_stub, []byte("#!/usr/bin/env bash\necho 2.2.16"), 0777)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
m["command"] = inspec_stub
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvisioner_Impl(t *testing.T) {
|
||||||
|
var raw interface{}
|
||||||
|
raw = &Provisioner{}
|
||||||
|
if _, ok := raw.(packer.Provisioner); !ok {
|
||||||
|
t.Fatalf("must be a Provisioner")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvisionerPrepare_Defaults(t *testing.T) {
|
||||||
|
var p Provisioner
|
||||||
|
config := testConfig(t)
|
||||||
|
defer os.Remove(config["command"].(string))
|
||||||
|
|
||||||
|
err := p.Prepare(config)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("should have error")
|
||||||
|
}
|
||||||
|
|
||||||
|
hostkey_file, err := ioutil.TempFile("", "hostkey")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(hostkey_file.Name())
|
||||||
|
|
||||||
|
publickey_file, err := ioutil.TempFile("", "publickey")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(publickey_file.Name())
|
||||||
|
|
||||||
|
profile_file, err := ioutil.TempFile("", "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(profile_file.Name())
|
||||||
|
|
||||||
|
config["ssh_host_key_file"] = hostkey_file.Name()
|
||||||
|
config["ssh_authorized_key_file"] = publickey_file.Name()
|
||||||
|
config["profile"] = profile_file.Name()
|
||||||
|
err = p.Prepare(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(profile_file.Name())
|
||||||
|
|
||||||
|
err = os.Unsetenv("USER")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
err = p.Prepare(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvisionerPrepare_ProfileFile(t *testing.T) {
|
||||||
|
var p Provisioner
|
||||||
|
config := testConfig(t)
|
||||||
|
defer os.Remove(config["command"].(string))
|
||||||
|
|
||||||
|
hostkey_file, err := ioutil.TempFile("", "hostkey")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(hostkey_file.Name())
|
||||||
|
|
||||||
|
publickey_file, err := ioutil.TempFile("", "publickey")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(publickey_file.Name())
|
||||||
|
|
||||||
|
config["ssh_host_key_file"] = hostkey_file.Name()
|
||||||
|
config["ssh_authorized_key_file"] = publickey_file.Name()
|
||||||
|
|
||||||
|
err = p.Prepare(config)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("should have error")
|
||||||
|
}
|
||||||
|
|
||||||
|
profile_file, err := ioutil.TempFile("", "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(profile_file.Name())
|
||||||
|
|
||||||
|
config["profile"] = profile_file.Name()
|
||||||
|
err = p.Prepare(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
test_dir, err := ioutil.TempDir("", "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(test_dir)
|
||||||
|
|
||||||
|
config["profile"] = test_dir
|
||||||
|
err = p.Prepare(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvisionerPrepare_HostKeyFile(t *testing.T) {
|
||||||
|
var p Provisioner
|
||||||
|
config := testConfig(t)
|
||||||
|
defer os.Remove(config["command"].(string))
|
||||||
|
|
||||||
|
publickey_file, err := ioutil.TempFile("", "publickey")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(publickey_file.Name())
|
||||||
|
|
||||||
|
profile_file, err := ioutil.TempFile("", "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(profile_file.Name())
|
||||||
|
|
||||||
|
filename := make([]byte, 10)
|
||||||
|
n, err := io.ReadFull(rand.Reader, filename)
|
||||||
|
if n != len(filename) || err != nil {
|
||||||
|
t.Fatal("could not create random file name")
|
||||||
|
}
|
||||||
|
|
||||||
|
config["ssh_host_key_file"] = fmt.Sprintf("%x", filename)
|
||||||
|
config["ssh_authorized_key_file"] = publickey_file.Name()
|
||||||
|
config["profile"] = profile_file.Name()
|
||||||
|
|
||||||
|
err = p.Prepare(config)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("should error if ssh_host_key_file does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
hostkey_file, err := ioutil.TempFile("", "hostkey")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(hostkey_file.Name())
|
||||||
|
|
||||||
|
config["ssh_host_key_file"] = hostkey_file.Name()
|
||||||
|
err = p.Prepare(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvisionerPrepare_AuthorizedKeyFiles(t *testing.T) {
|
||||||
|
var p Provisioner
|
||||||
|
config := testConfig(t)
|
||||||
|
defer os.Remove(config["command"].(string))
|
||||||
|
|
||||||
|
hostkey_file, err := ioutil.TempFile("", "hostkey")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(hostkey_file.Name())
|
||||||
|
|
||||||
|
profile_file, err := ioutil.TempFile("", "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(profile_file.Name())
|
||||||
|
|
||||||
|
filename := make([]byte, 10)
|
||||||
|
n, err := io.ReadFull(rand.Reader, filename)
|
||||||
|
if n != len(filename) || err != nil {
|
||||||
|
t.Fatal("could not create random file name")
|
||||||
|
}
|
||||||
|
|
||||||
|
config["ssh_host_key_file"] = hostkey_file.Name()
|
||||||
|
config["profile"] = profile_file.Name()
|
||||||
|
config["ssh_authorized_key_file"] = fmt.Sprintf("%x", filename)
|
||||||
|
|
||||||
|
err = p.Prepare(config)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("should error if ssh_authorized_key_file does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
publickey_file, err := ioutil.TempFile("", "publickey")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(publickey_file.Name())
|
||||||
|
|
||||||
|
config["ssh_authorized_key_file"] = publickey_file.Name()
|
||||||
|
err = p.Prepare(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvisionerPrepare_LocalPort(t *testing.T) {
|
||||||
|
var p Provisioner
|
||||||
|
config := testConfig(t)
|
||||||
|
defer os.Remove(config["command"].(string))
|
||||||
|
|
||||||
|
hostkey_file, err := ioutil.TempFile("", "hostkey")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(hostkey_file.Name())
|
||||||
|
|
||||||
|
publickey_file, err := ioutil.TempFile("", "publickey")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(publickey_file.Name())
|
||||||
|
|
||||||
|
profile_file, err := ioutil.TempFile("", "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(profile_file.Name())
|
||||||
|
|
||||||
|
config["ssh_host_key_file"] = hostkey_file.Name()
|
||||||
|
config["ssh_authorized_key_file"] = publickey_file.Name()
|
||||||
|
config["profile"] = profile_file.Name()
|
||||||
|
|
||||||
|
config["local_port"] = "65537"
|
||||||
|
err = p.Prepare(config)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("should have error")
|
||||||
|
}
|
||||||
|
|
||||||
|
config["local_port"] = "22222"
|
||||||
|
err = p.Prepare(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInspecGetVersion(t *testing.T) {
|
||||||
|
if os.Getenv("PACKER_ACC") == "" {
|
||||||
|
t.Skip("This test is only run with PACKER_ACC=1 and it requires InSpec to be installed")
|
||||||
|
}
|
||||||
|
|
||||||
|
var p Provisioner
|
||||||
|
p.config.Command = "inspec exec"
|
||||||
|
err := p.getVersion()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInspecGetVersionError(t *testing.T) {
|
||||||
|
var p Provisioner
|
||||||
|
p.config.Command = "./test-fixtures/exit1"
|
||||||
|
err := p.getVersion()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Should return error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "./test-fixtures/exit1 version") {
|
||||||
|
t.Fatal("Error message should include command name")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
exit 1
|
|
@ -25,6 +25,7 @@ still distributed with Packer.
|
||||||
## Provisioners
|
## Provisioners
|
||||||
|
|
||||||
- File
|
- File
|
||||||
|
- InSpec
|
||||||
- PowerShell
|
- PowerShell
|
||||||
- Shell
|
- Shell
|
||||||
- Windows Restart
|
- Windows Restart
|
||||||
|
|
|
@ -0,0 +1,135 @@
|
||||||
|
---
|
||||||
|
description: |
|
||||||
|
The inspec Packer provisioner allows inspec profiles to be run to test the
|
||||||
|
machine.
|
||||||
|
layout: docs
|
||||||
|
page_title: 'InSpec - Provisioners'
|
||||||
|
sidebar_current: 'docs-provisioners-inspec'
|
||||||
|
---
|
||||||
|
|
||||||
|
# InSpec Provisioner
|
||||||
|
|
||||||
|
Type: `inspec`
|
||||||
|
|
||||||
|
The `inspec` Packer provisioner runs InSpec profiles. It dynamically creates a
|
||||||
|
target configured to use SSH, runs an SSH server, executes `inspec exec`, and
|
||||||
|
marshals InSpec tests through the SSH server to the machine being provisioned
|
||||||
|
by Packer.
|
||||||
|
|
||||||
|
## Basic Example
|
||||||
|
|
||||||
|
This is a fully functional template that will test an image on DigitalOcean.
|
||||||
|
Replace the mock `api_token` value with your own.
|
||||||
|
|
||||||
|
``` json
|
||||||
|
{
|
||||||
|
"provisioners": [
|
||||||
|
{
|
||||||
|
"type": "inspec",
|
||||||
|
"profile": "https://github.com/dev-sec/linux-baseline"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"builders": [
|
||||||
|
{
|
||||||
|
"type": "digitalocean",
|
||||||
|
"api_token": "<digital ocean api token>",
|
||||||
|
"image": "ubuntu-14-04-x64",
|
||||||
|
"region": "sfo1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Reference
|
||||||
|
|
||||||
|
Required Parameters:
|
||||||
|
|
||||||
|
- `profile` - The profile to be executed by InSpec.
|
||||||
|
|
||||||
|
Optional Parameters:
|
||||||
|
|
||||||
|
- `inspec_env_vars` (array of strings) - Environment variables to set before
|
||||||
|
running InSpec. Usage example:
|
||||||
|
|
||||||
|
``` json
|
||||||
|
{
|
||||||
|
"inspec_env_vars": [ "FOO=bar" ]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `command` (string) - The command to invoke InSpec. Defaults to `inspec`.
|
||||||
|
|
||||||
|
- `extra_arguments` (array of strings) - Extra arguments to pass to InSpec.
|
||||||
|
These arguments *will not* be passed through a shell and arguments should
|
||||||
|
not be quoted. Usage example:
|
||||||
|
|
||||||
|
``` json
|
||||||
|
{
|
||||||
|
"extra_arguments": [ "--sudo", "--reporter", "json" ]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `attributes` (array of strings) - Attribute Files used by InSpec which will
|
||||||
|
be passed to the `--attrs` argument of the `inspec` command when this
|
||||||
|
provisioner runs InSpec. Specify this if you want a different location.
|
||||||
|
Note using also `"--attrs"` in `extra_arguments` will override this
|
||||||
|
setting.
|
||||||
|
|
||||||
|
- `attributes_directory` (string) - The directory in which to place the
|
||||||
|
temporary generated InSpec Attributes file. By default, this is the
|
||||||
|
system-specific temporary file location. The fully-qualified name of this
|
||||||
|
temporary file will be passed to the `--attrs` argument of the `inspec`
|
||||||
|
command when this provisioner runs InSpec. Specify this if you want a
|
||||||
|
different location.
|
||||||
|
|
||||||
|
- `backend` (string) - Backend used by InSpec for connection. Defaults to
|
||||||
|
SSH.
|
||||||
|
|
||||||
|
- `host` (string) - Host used for by InSpec for connection. Defaults to
|
||||||
|
localhost.
|
||||||
|
|
||||||
|
- `local_port` (string) - The port on which to attempt to listen for SSH
|
||||||
|
connections. This value is a starting point. The provisioner will attempt to
|
||||||
|
listen for SSH connections on the first available of ten ports, starting at
|
||||||
|
`local_port`. A system-chosen port is used when `local_port` is missing or
|
||||||
|
empty.
|
||||||
|
|
||||||
|
- `ssh_host_key_file` (string) - The SSH key that will be used to run the SSH
|
||||||
|
server on the host machine to forward commands to the target machine.
|
||||||
|
InSpec connects to this server and will validate the identity of the server
|
||||||
|
using the system known\_hosts. The default behavior is to generate and use
|
||||||
|
a onetime key.
|
||||||
|
|
||||||
|
- `ssh_authorized_key_file` (string) - The SSH public key of the InSpec
|
||||||
|
`ssh_user`. The default behavior is to generate and use a onetime key. If
|
||||||
|
this key is generated, the corresponding private key is passed to `inspec`
|
||||||
|
command with the `-i inspec_ssh_private_key_file` option.
|
||||||
|
|
||||||
|
- `user` (string) - The `--user` to use. Defaults to the user running Packer.
|
||||||
|
|
||||||
|
## Default Extra Variables
|
||||||
|
|
||||||
|
In addition to being able to specify extra arguments using the
|
||||||
|
`extra_arguments` configuration, the provisioner automatically defines certain
|
||||||
|
commonly useful InSpec Attributes:
|
||||||
|
|
||||||
|
- `packer_build_name` is set to the name of the build that Packer is running.
|
||||||
|
This is most useful when Packer is making multiple builds and you want to
|
||||||
|
distinguish them slightly when using a common profile.
|
||||||
|
|
||||||
|
- `packer_builder_type` is the type of the builder that was used to create
|
||||||
|
the machine that the script is running on. This is useful if you want to
|
||||||
|
run only certain parts of the profile on systems built with certain
|
||||||
|
builders.
|
||||||
|
|
||||||
|
## Debugging
|
||||||
|
|
||||||
|
To debug underlying issues with InSpec, add `"-l"` to `"extra_arguments"` to
|
||||||
|
enable verbose logging.
|
||||||
|
|
||||||
|
``` json
|
||||||
|
{
|
||||||
|
"extra_arguments": [ "-l", "debug" ]
|
||||||
|
}
|
||||||
|
```
|
|
@ -216,6 +216,9 @@
|
||||||
<li<%= sidebar_current("docs-provisioners-file")%>>
|
<li<%= sidebar_current("docs-provisioners-file")%>>
|
||||||
<a href="/docs/provisioners/file.html">File</a>
|
<a href="/docs/provisioners/file.html">File</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li<%= sidebar_current("docs-provisioners-inspec")%>>
|
||||||
|
<a href="/docs/provisioners/inspec.html">InSpec</a>
|
||||||
|
</li>
|
||||||
<li<%= sidebar_current("docs-provisioners-powershell")%>>
|
<li<%= sidebar_current("docs-provisioners-powershell")%>>
|
||||||
<a href="/docs/provisioners/powershell.html">PowerShell</a>
|
<a href="/docs/provisioners/powershell.html">PowerShell</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
Loading…
Reference in New Issue