From f5b13e3cb54ab2735e0a842ae1a059f628d42fb1 Mon Sep 17 00:00:00 2001 From: xinau Date: Sun, 20 Jan 2019 15:43:47 +0000 Subject: [PATCH 1/5] added inspec.io provisioner --- Makefile | 2 +- command/plugin.go | 2 + provisioner/inspec/adapter.go | 285 +++++++++ provisioner/inspec/adapter_test.go | 116 ++++ provisioner/inspec/provisioner.go | 563 ++++++++++++++++++ provisioner/inspec/provisioner_test.go | 293 +++++++++ provisioner/inspec/test-fixtures/exit1 | 3 + website/source/community-plugins.html.md | 1 + .../source/docs/provisioners/inspec.html.md | 135 +++++ website/source/layouts/docs.erb | 3 + 10 files changed, 1402 insertions(+), 1 deletion(-) create mode 100644 provisioner/inspec/adapter.go create mode 100644 provisioner/inspec/adapter_test.go create mode 100644 provisioner/inspec/provisioner.go create mode 100644 provisioner/inspec/provisioner_test.go create mode 100755 provisioner/inspec/test-fixtures/exit1 create mode 100644 website/source/docs/provisioners/inspec.html.md diff --git a/Makefile b/Makefile index b65b6f500..042c46f2f 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ GOPATH=$(shell go env GOPATH) # gofmt 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 GIT_DIRTY=$(shell test -n "`git status --porcelain`" && echo "+CHANGES" || true) diff --git a/command/plugin.go b/command/plugin.go index 825f06683..3bbe8965e 100644 --- a/command/plugin.go +++ b/command/plugin.go @@ -71,6 +71,7 @@ import ( chefsoloprovisioner "github.com/hashicorp/packer/provisioner/chef-solo" convergeprovisioner "github.com/hashicorp/packer/provisioner/converge" fileprovisioner "github.com/hashicorp/packer/provisioner/file" + inspecprovisioner "github.com/hashicorp/packer/provisioner/inspec" powershellprovisioner "github.com/hashicorp/packer/provisioner/powershell" puppetmasterlessprovisioner "github.com/hashicorp/packer/provisioner/puppet-masterless" puppetserverprovisioner "github.com/hashicorp/packer/provisioner/puppet-server" @@ -130,6 +131,7 @@ var Provisioners = map[string]packer.Provisioner{ "chef-solo": new(chefsoloprovisioner.Provisioner), "converge": new(convergeprovisioner.Provisioner), "file": new(fileprovisioner.Provisioner), + "inspec": new(inspecprovisioner.Provisioner), "powershell": new(powershellprovisioner.Provisioner), "puppet-masterless": new(puppetmasterlessprovisioner.Provisioner), "puppet-server": new(puppetserverprovisioner.Provisioner), diff --git a/provisioner/inspec/adapter.go b/provisioner/inspec/adapter.go new file mode 100644 index 000000000..ebd52d0a3 --- /dev/null +++ b/provisioner/inspec/adapter.go @@ -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 +} diff --git a/provisioner/inspec/adapter_test.go b/provisioner/inspec/adapter_test.go new file mode 100644 index 000000000..638cead58 --- /dev/null +++ b/provisioner/inspec/adapter_test.go @@ -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") +} diff --git a/provisioner/inspec/provisioner.go b/provisioner/inspec/provisioner.go new file mode 100644 index 000000000..b520c1a66 --- /dev/null +++ b/provisioner/inspec/provisioner.go @@ -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) +} diff --git a/provisioner/inspec/provisioner_test.go b/provisioner/inspec/provisioner_test.go new file mode 100644 index 000000000..d38d1f0c6 --- /dev/null +++ b/provisioner/inspec/provisioner_test.go @@ -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") + } +} diff --git a/provisioner/inspec/test-fixtures/exit1 b/provisioner/inspec/test-fixtures/exit1 new file mode 100755 index 000000000..2bb8d868b --- /dev/null +++ b/provisioner/inspec/test-fixtures/exit1 @@ -0,0 +1,3 @@ +#!/bin/sh + +exit 1 diff --git a/website/source/community-plugins.html.md b/website/source/community-plugins.html.md index bb0cb2490..537cdc3ca 100644 --- a/website/source/community-plugins.html.md +++ b/website/source/community-plugins.html.md @@ -25,6 +25,7 @@ still distributed with Packer. ## Provisioners - File +- InSpec - PowerShell - Shell - Windows Restart diff --git a/website/source/docs/provisioners/inspec.html.md b/website/source/docs/provisioners/inspec.html.md new file mode 100644 index 000000000..809b33b5e --- /dev/null +++ b/website/source/docs/provisioners/inspec.html.md @@ -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": "", + "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" ] +} +``` diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 5a9f4436d..9c6496594 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -216,6 +216,9 @@ > File + > + InSpec + > PowerShell From 8526244285f3fe1119f981fcd125592355752d71 Mon Sep 17 00:00:00 2001 From: xinau Date: Tue, 12 Feb 2019 06:32:19 +0000 Subject: [PATCH 2/5] changed local_port to uint representation --- provisioner/inspec/provisioner.go | 33 +++++++++---------- provisioner/inspec/provisioner_test.go | 4 +-- .../source/docs/provisioners/inspec.html.md | 8 ++--- 3 files changed, 20 insertions(+), 25 deletions(-) diff --git a/provisioner/inspec/provisioner.go b/provisioner/inspec/provisioner.go index b520c1a66..c5e2f747f 100644 --- a/provisioner/inspec/provisioner.go +++ b/provisioner/inspec/provisioner.go @@ -51,7 +51,7 @@ type Config struct { Backend string `mapstructure:"backend"` User string `mapstructure:"user"` Host string `mapstructure:"host"` - LocalPort string `mapstructure:"local_port"` + LocalPort uint `mapstructure:"local_port"` SSHHostKeyFile string `mapstructure:"ssh_host_key_file"` SSHAuthorizedKeyFile string `mapstructure:"ssh_authorized_key_file"` } @@ -109,24 +109,20 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { } } - 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 _, 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.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 p.config.LocalPort > 65535 { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("local_port: %d must be a valid port", p.config.LocalPort)) } if len(p.config.AttributesDirectory) > 0 { @@ -245,11 +241,8 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { config.AddHostKey(hostSigner) localListener, err := func() (net.Listener, error) { - port, err := strconv.ParseUint(p.config.LocalPort, 10, 16) - if err != nil { - return nil, err - } + port := p.config.LocalPort tries := 1 if port != 0 { tries = 10 @@ -261,11 +254,17 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { ui.Say(err.Error()) continue } - _, p.config.LocalPort, err = net.SplitHostPort(l.Addr().String()) + _, portStr, err := net.SplitHostPort(l.Addr().String()) if err != nil { ui.Say(err.Error()) continue } + portUint64, err := strconv.ParseUint(portStr, 10, 0) + if err != nil { + ui.Say(err.Error()) + continue + } + p.config.LocalPort = uint(portUint64) return l, nil } return nil, errors.New("Error setting up SSH proxy connection") @@ -331,7 +330,7 @@ func (p *Provisioner) executeInspec(ui packer.Ui, comm packer.Communicator, priv args = append(args, "--key-files", privKeyFile) } args = append(args, "--user", p.config.User) - args = append(args, "--port", p.config.LocalPort) + args = append(args, "--port", strconv.FormatUint(uint64(p.config.LocalPort), 10)) } args = append(args, "--attrs") diff --git a/provisioner/inspec/provisioner_test.go b/provisioner/inspec/provisioner_test.go index d38d1f0c6..6857f7004 100644 --- a/provisioner/inspec/provisioner_test.go +++ b/provisioner/inspec/provisioner_test.go @@ -254,13 +254,13 @@ func TestProvisionerPrepare_LocalPort(t *testing.T) { config["ssh_authorized_key_file"] = publickey_file.Name() config["profile"] = profile_file.Name() - config["local_port"] = "65537" + config["local_port"] = uint(65537) err = p.Prepare(config) if err == nil { t.Fatal("should have error") } - config["local_port"] = "22222" + config["local_port"] = uint(22222) err = p.Prepare(config) if err != nil { t.Fatalf("err: %s", err) diff --git a/website/source/docs/provisioners/inspec.html.md b/website/source/docs/provisioners/inspec.html.md index 809b33b5e..56809050d 100644 --- a/website/source/docs/provisioners/inspec.html.md +++ b/website/source/docs/provisioners/inspec.html.md @@ -45,7 +45,7 @@ Replace the mock `api_token` value with your own. Required Parameters: -- `profile` - The profile to be executed by InSpec. +- `profile` (string) - The profile to be executed by InSpec. Optional Parameters: @@ -53,9 +53,7 @@ Optional Parameters: running InSpec. Usage example: ``` json - { "inspec_env_vars": [ "FOO=bar" ] - } ``` - `command` (string) - The command to invoke InSpec. Defaults to `inspec`. @@ -65,9 +63,7 @@ Optional Parameters: not be quoted. Usage example: ``` json - { "extra_arguments": [ "--sudo", "--reporter", "json" ] - } ``` - `attributes` (array of strings) - Attribute Files used by InSpec which will @@ -89,7 +85,7 @@ Optional Parameters: - `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 +- `local_port` (uint) - 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 From 36c1e8d838d3d0cd94e738ee50fe40064e7c30ba Mon Sep 17 00:00:00 2001 From: xinau Date: Tue, 12 Feb 2019 07:10:57 +0000 Subject: [PATCH 3/5] moved adapter to common package --- .../ansible => common/adapter}/adapter.go | 22 +- .../inspec => common/adapter}/adapter_test.go | 4 +- .../ansible => common/adapter}/scp.go | 2 +- provisioner/ansible/adapter_test.go | 116 ------- provisioner/ansible/provisioner.go | 5 +- provisioner/inspec/adapter.go | 285 ------------------ provisioner/inspec/provisioner.go | 5 +- 7 files changed, 20 insertions(+), 419 deletions(-) rename {provisioner/ansible => common/adapter}/adapter.go (93%) rename {provisioner/inspec => common/adapter}/adapter_test.go (96%) rename {provisioner/ansible => common/adapter}/scp.go (99%) delete mode 100644 provisioner/ansible/adapter_test.go delete mode 100644 provisioner/inspec/adapter.go diff --git a/provisioner/ansible/adapter.go b/common/adapter/adapter.go similarity index 93% rename from provisioner/ansible/adapter.go rename to common/adapter/adapter.go index c3dfd3495..510ae40bb 100644 --- a/provisioner/ansible/adapter.go +++ b/common/adapter/adapter.go @@ -1,4 +1,4 @@ -package ansible +package adapter import ( "bytes" @@ -17,7 +17,7 @@ import ( // An adapter satisfies SSH requests (from an Ansible client) by delegating SSH // exec and subsystem commands to a packer.Communicator. -type adapter struct { +type Adapter struct { done <-chan struct{} l net.Listener config *ssh.ServerConfig @@ -26,8 +26,8 @@ type adapter struct { comm packer.Communicator } -func newAdapter(done <-chan struct{}, l net.Listener, config *ssh.ServerConfig, sftpCmd string, ui packer.Ui, comm packer.Communicator) *adapter { - return &adapter{ +func NewAdapter(done <-chan struct{}, l net.Listener, config *ssh.ServerConfig, sftpCmd string, ui packer.Ui, comm packer.Communicator) *Adapter { + return &Adapter{ done: done, l: l, config: config, @@ -37,7 +37,7 @@ func newAdapter(done <-chan struct{}, l net.Listener, config *ssh.ServerConfig, } } -func (c *adapter) Serve() { +func (c *Adapter) Serve() { log.Printf("SSH proxy: serving on %s", c.l.Addr()) for { @@ -62,7 +62,7 @@ func (c *adapter) Serve() { } } -func (c *adapter) Handle(conn net.Conn, ui packer.Ui) error { +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 { @@ -89,7 +89,7 @@ func (c *adapter) Handle(conn net.Conn, ui packer.Ui) error { return nil } -func (c *adapter) handleSession(newChannel ssh.NewChannel) error { +func (c *Adapter) handleSession(newChannel ssh.NewChannel) error { channel, requests, err := newChannel.Accept() if err != nil { return err @@ -182,11 +182,11 @@ func (c *adapter) handleSession(newChannel ssh.NewChannel) error { return nil } -func (c *adapter) Shutdown() { +func (c *Adapter) Shutdown() { c.l.Close() } -func (c *adapter) exec(command string, in io.Reader, out io.Writer, err io.Writer) int { +func (c *Adapter) exec(command string, in io.Reader, out io.Writer, err io.Writer) int { var exitStatus int switch { case strings.HasPrefix(command, "scp ") && serveSCP(command[4:]): @@ -206,7 +206,7 @@ func serveSCP(args string) bool { return bytes.IndexAny(opts, "tf") >= 0 } -func (c *adapter) scpExec(args string, in io.Reader, out io.Writer) error { +func (c *Adapter) scpExec(args string, in io.Reader, out io.Writer) error { opts, rest := scpOptions(args) // remove the quoting that ansible added to rest for shell safety. @@ -226,7 +226,7 @@ func (c *adapter) scpExec(args string, in io.Reader, out io.Writer) error { return errors.New("no scp mode specified") } -func (c *adapter) remoteExec(command string, in io.Reader, out io.Writer, err io.Writer) int { +func (c *Adapter) remoteExec(command string, in io.Reader, out io.Writer, err io.Writer) int { cmd := &packer.RemoteCmd{ Stdin: in, Stdout: out, diff --git a/provisioner/inspec/adapter_test.go b/common/adapter/adapter_test.go similarity index 96% rename from provisioner/inspec/adapter_test.go rename to common/adapter/adapter_test.go index 638cead58..a43b3bedc 100644 --- a/provisioner/inspec/adapter_test.go +++ b/common/adapter/adapter_test.go @@ -1,4 +1,4 @@ -package inspec +package adapter import ( "errors" @@ -26,7 +26,7 @@ func TestAdapter_Serve(t *testing.T) { ui := new(packer.NoopUi) - sut := newAdapter(done, &l, config, newUi(ui), communicator{}) + sut := NewAdapter(done, &l, config, "", ui, communicator{}) go func() { i := 0 for range acceptC { diff --git a/provisioner/ansible/scp.go b/common/adapter/scp.go similarity index 99% rename from provisioner/ansible/scp.go rename to common/adapter/scp.go index ca029605d..06043ff20 100644 --- a/provisioner/ansible/scp.go +++ b/common/adapter/scp.go @@ -1,4 +1,4 @@ -package ansible +package adapter import ( "bufio" diff --git a/provisioner/ansible/adapter_test.go b/provisioner/ansible/adapter_test.go deleted file mode 100644 index 29667cbe2..000000000 --- a/provisioner/ansible/adapter_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package ansible - -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") -} diff --git a/provisioner/ansible/provisioner.go b/provisioner/ansible/provisioner.go index 9979fbd2a..7ea29f584 100644 --- a/provisioner/ansible/provisioner.go +++ b/provisioner/ansible/provisioner.go @@ -26,6 +26,7 @@ import ( "golang.org/x/crypto/ssh" "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/common/adapter" commonhelper "github.com/hashicorp/packer/helper/common" "github.com/hashicorp/packer/helper/config" "github.com/hashicorp/packer/packer" @@ -63,7 +64,7 @@ type Config struct { type Provisioner struct { config Config - adapter *adapter + adapter *adapter.Adapter done chan struct{} ansibleVersion string ansibleMajVersion uint @@ -286,7 +287,7 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { } ui = newUi(ui) - p.adapter = newAdapter(p.done, localListener, config, p.config.SFTPCmd, ui, comm) + p.adapter = adapter.NewAdapter(p.done, localListener, config, p.config.SFTPCmd, ui, comm) defer func() { log.Print("shutting down the SSH proxy") diff --git a/provisioner/inspec/adapter.go b/provisioner/inspec/adapter.go deleted file mode 100644 index ebd52d0a3..000000000 --- a/provisioner/inspec/adapter.go +++ /dev/null @@ -1,285 +0,0 @@ -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 -} diff --git a/provisioner/inspec/provisioner.go b/provisioner/inspec/provisioner.go index c5e2f747f..f2505351d 100644 --- a/provisioner/inspec/provisioner.go +++ b/provisioner/inspec/provisioner.go @@ -25,6 +25,7 @@ import ( "golang.org/x/crypto/ssh" "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/common/adapter" "github.com/hashicorp/packer/helper/config" "github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/template/interpolate" @@ -58,7 +59,7 @@ type Config struct { type Provisioner struct { config Config - adapter *adapter + adapter *adapter.Adapter done chan struct{} inspecVersion string inspecMajVersion uint @@ -275,7 +276,7 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { } ui = newUi(ui) - p.adapter = newAdapter(p.done, localListener, config, ui, comm) + p.adapter = adapter.NewAdapter(p.done, localListener, config, "", ui, comm) defer func() { log.Print("shutting down the SSH proxy") From ae59b81f447e7aa2c4daa36c3500f060c30c5cd0 Mon Sep 17 00:00:00 2001 From: xinau Date: Tue, 12 Feb 2019 20:07:13 +0000 Subject: [PATCH 4/5] added check for docker backend --- provisioner/inspec/provisioner.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/provisioner/inspec/provisioner.go b/provisioner/inspec/provisioner.go index f2505351d..7647b4286 100644 --- a/provisioner/inspec/provisioner.go +++ b/provisioner/inspec/provisioner.go @@ -118,6 +118,10 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { errs = packer.MultiErrorAppend(errs, fmt.Errorf("backend: %s must be a valid backend", p.config.Backend)) } + if p.config.Backend == "docker" && p.config.Host == "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("backend: host must be specified for docker backend")) + } + if p.config.Host == "" { p.config.Host = "127.0.0.1" } From b449af84ee334d94e305b432db732259cfe2fb20 Mon Sep 17 00:00:00 2001 From: xinau Date: Wed, 13 Feb 2019 21:01:50 +0000 Subject: [PATCH 5/5] moved concurrency-safe ui code to packer/ui.go --- packer/ui.go | 47 ++++++++++++++++++++++++++- provisioner/ansible/provisioner.go | 51 +++--------------------------- provisioner/inspec/provisioner.go | 51 +++--------------------------- 3 files changed, 54 insertions(+), 95 deletions(-) diff --git a/packer/ui.go b/packer/ui.go index 2a0f5e275..e42e08139 100644 --- a/packer/ui.go +++ b/packer/ui.go @@ -343,7 +343,7 @@ func (u *MachineReadableUi) ProgressBar() ProgressBar { return new(NoopProgressBar) } -// TimestampedUi is a UI that wraps another UI implementation and prefixes +// TimestampedUi is a UI that wraps another UI implementation and // prefixes each message with an RFC3339 timestamp type TimestampedUi struct { Ui Ui @@ -376,3 +376,48 @@ func (u *TimestampedUi) ProgressBar() ProgressBar { return u.Ui.ProgressBar() } func (u *TimestampedUi) timestampLine(string string) string { return fmt.Sprintf("%v: %v", time.Now().Format(time.RFC3339), string) } + +// Safe is a UI that wraps another UI implementation and +// provides concurrency-safe access +type SafeUi struct { + Sem chan int + Ui Ui +} + +var _ Ui = new(SafeUi) + +func (u *SafeUi) Ask(s string) (string, error) { + u.Sem <- 1 + ret, err := u.Ui.Ask(s) + <-u.Sem + + return ret, err +} + +func (u *SafeUi) Say(s string) { + u.Sem <- 1 + u.Ui.Say(s) + <-u.Sem +} + +func (u *SafeUi) Message(s string) { + u.Sem <- 1 + u.Ui.Message(s) + <-u.Sem +} + +func (u *SafeUi) Error(s string) { + u.Sem <- 1 + u.Ui.Error(s) + <-u.Sem +} + +func (u *SafeUi) Machine(t string, args ...string) { + u.Sem <- 1 + u.Ui.Machine(t, args...) + <-u.Sem +} + +func (u *SafeUi) ProgressBar() ProgressBar { + return new(NoopProgressBar) +} diff --git a/provisioner/ansible/provisioner.go b/provisioner/ansible/provisioner.go index 7ea29f584..e836eb894 100644 --- a/provisioner/ansible/provisioner.go +++ b/provisioner/ansible/provisioner.go @@ -286,7 +286,10 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { return err } - ui = newUi(ui) + ui = &packer.SafeUi{ + Sem: make(chan int, 1), + Ui: ui, + } p.adapter = adapter.NewAdapter(p.done, localListener, config, p.config.SFTPCmd, ui, comm) defer func() { @@ -557,49 +560,3 @@ func getWinRMPassword(buildName string) string { packer.LogSecretFilter.Set(winRMPass) return winRMPass } - -// 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) -} diff --git a/provisioner/inspec/provisioner.go b/provisioner/inspec/provisioner.go index 7647b4286..fab92ee49 100644 --- a/provisioner/inspec/provisioner.go +++ b/provisioner/inspec/provisioner.go @@ -279,7 +279,10 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { return err } - ui = newUi(ui) + ui = &packer.SafeUi{ + Sem: make(chan int, 1), + Ui: ui, + } p.adapter = adapter.NewAdapter(p.done, localListener, config, "", ui, comm) defer func() { @@ -519,49 +522,3 @@ func newSigner(privKeyFile string) (*signer, error) { 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) -}