Merge pull request #970 from fnoeding/nullbuilder

implemented null buider
This commit is contained in:
Ross Smith II 2014-04-21 08:12:33 -07:00
commit a33aee13d5
11 changed files with 420 additions and 1 deletions

View File

@ -0,0 +1,29 @@
package null
import (
"fmt"
)
// dummy Artifact implementation - does nothing
type NullArtifact struct {
}
func (*NullArtifact) BuilderId() string {
return BuilderId
}
func (a *NullArtifact) Files() []string {
return []string{}
}
func (*NullArtifact) Id() string {
return "Null"
}
func (a *NullArtifact) String() string {
return fmt.Sprintf("Did not export anything. This is the null builder")
}
func (a *NullArtifact) Destroy() error {
return nil
}

View File

@ -0,0 +1,10 @@
package null
import (
"github.com/mitchellh/packer/packer"
"testing"
)
func TestNullArtifact(t *testing.T) {
var _ packer.Artifact = new(NullArtifact)
}

71
builder/null/builder.go Normal file
View File

@ -0,0 +1,71 @@
package null
import (
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"log"
"time"
)
const BuilderId = "fnoeding.null"
type Builder struct {
config *Config
runner multistep.Runner
}
func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
c, warnings, errs := NewConfig(raws...)
if errs != nil {
return warnings, errs
}
b.config = c
return warnings, nil
}
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
steps := []multistep.Step{
&common.StepConnectSSH{
SSHAddress: SSHAddress(b.config.Host, b.config.Port),
SSHConfig: SSHConfig(b.config.SSHUsername, b.config.SSHPassword, b.config.SSHPrivateKeyFile),
SSHWaitTimeout: 1 * time.Minute,
},
&common.StepProvision{},
}
// Setup the state bag and initial state for the steps
state := new(multistep.BasicStateBag)
state.Put("config", b.config)
state.Put("hook", hook)
state.Put("ui", ui)
// Run!
if b.config.PackerDebug {
b.runner = &multistep.DebugRunner{
Steps: steps,
PauseFn: common.MultistepDebugFn(ui),
}
} else {
b.runner = &multistep.BasicRunner{Steps: steps}
}
b.runner.Run(state)
// If there was an error, return that
if rawErr, ok := state.GetOk("error"); ok {
return nil, rawErr.(error)
}
// No errors, must've worked
artifact := &NullArtifact{}
return artifact, nil
}
func (b *Builder) Cancel() {
if b.runner != nil {
log.Println("Cancelling the step runner...")
b.runner.Cancel()
}
}

View File

@ -0,0 +1,10 @@
package null
import (
"github.com/mitchellh/packer/packer"
"testing"
)
func TestBuilder_implBuilder(t *testing.T) {
var _ packer.Builder = new(Builder)
}

68
builder/null/config.go Normal file
View File

@ -0,0 +1,68 @@
package null
import (
"fmt"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
)
type Config struct {
common.PackerConfig `mapstructure:",squash"`
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
SSHUsername string `mapstructure:"ssh_username"`
SSHPassword string `mapstructure:"ssh_password"`
SSHPrivateKeyFile string `mapstructure:"ssh_private_key_file"`
tpl *packer.ConfigTemplate
}
func NewConfig(raws ...interface{}) (*Config, []string, error) {
c := new(Config)
md, err := common.DecodeConfig(c, raws...)
if err != nil {
return nil, nil, err
}
c.tpl, err = packer.NewConfigTemplate()
if err != nil {
return nil, nil, err
}
c.tpl.UserVars = c.PackerUserVars
// Defaults
if c.Port == 0 {
c.Port = 22
}
// (none so far)
errs := common.CheckUnusedConfig(md)
if c.Host == "" {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("host must be specified"))
}
if c.SSHUsername == "" {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("ssh_username must be specified"))
}
if c.SSHPassword == "" && c.SSHPrivateKeyFile == "" {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("one of ssh_password and ssh_private_key_file must be specified"))
}
if c.SSHPassword != "" && c.SSHPrivateKeyFile != "" {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("only one of ssh_password and ssh_private_key_file must be specified"))
}
if errs != nil && len(errs.Errors) > 0 {
return nil, nil, errs
}
return c, nil, nil
}

109
builder/null/config_test.go Normal file
View File

@ -0,0 +1,109 @@
package null
import (
"testing"
)
func testConfig() map[string]interface{} {
return map[string]interface{}{
"host": "foo",
"ssh_username": "bar",
"ssh_password": "baz",
}
}
func testConfigStruct(t *testing.T) *Config {
c, warns, errs := NewConfig(testConfig())
if len(warns) > 0 {
t.Fatalf("bad: %#v", len(warns))
}
if errs != nil {
t.Fatalf("bad: %#v", errs)
}
return c
}
func testConfigErr(t *testing.T, warns []string, err error) {
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should error")
}
}
func testConfigOk(t *testing.T, warns []string, err error) {
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("bad: %s", err)
}
}
func TestConfigPrepare_port(t *testing.T) {
raw := testConfig()
// default port should be 22
delete(raw, "port")
c, warns, errs := NewConfig(raw)
if c.Port != 22 {
t.Fatalf("bad: port should default to 22, not %d", c.Port)
}
testConfigOk(t, warns, errs)
}
func TestConfigPrepare_host(t *testing.T) {
raw := testConfig()
// No host
delete(raw, "host")
_, warns, errs := NewConfig(raw)
testConfigErr(t, warns, errs)
// Good host
raw["host"] = "good"
_, warns, errs = NewConfig(raw)
testConfigOk(t, warns, errs)
}
func TestConfigPrepare_sshUsername(t *testing.T) {
raw := testConfig()
// No ssh_username
delete(raw, "ssh_username")
_, warns, errs := NewConfig(raw)
testConfigErr(t, warns, errs)
// Good ssh_username
raw["ssh_username"] = "good"
_, warns, errs = NewConfig(raw)
testConfigOk(t, warns, errs)
}
func TestConfigPrepare_sshCredential(t *testing.T) {
raw := testConfig()
// no ssh_password and no ssh_private_key_file
delete(raw, "ssh_password")
delete(raw, "ssh_private_key_file")
_, warns, errs := NewConfig(raw)
testConfigErr(t, warns, errs)
// only ssh_password
raw["ssh_password"] = "good"
_, warns, errs = NewConfig(raw)
testConfigOk(t, warns, errs)
// only ssh_private_key_file
raw["ssh_private_key_file"] = "good"
delete(raw, "ssh_password")
_, warns, errs = NewConfig(raw)
testConfigOk(t, warns, errs)
// both ssh_password and ssh_private_key_file set
raw["ssh_password"] = "bad"
_, warns, errs = NewConfig(raw)
testConfigErr(t, warns, errs)
}

57
builder/null/ssh.go Normal file
View File

@ -0,0 +1,57 @@
package null
import (
gossh "code.google.com/p/go.crypto/ssh"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/communicator/ssh"
"io/ioutil"
)
// SSHAddress returns a function that can be given to the SSH communicator
// for determining the SSH address
func SSHAddress(host string, port int) func(multistep.StateBag) (string, error) {
return func(state multistep.StateBag) (string, error) {
return fmt.Sprintf("%s:%d", host, port), nil
}
}
// SSHConfig returns a function that can be used for the SSH communicator
// config for connecting to the specified host via SSH
// private_key_file has precedence over password!
func SSHConfig(username string, password string, privateKeyFile string) func(multistep.StateBag) (*gossh.ClientConfig, error) {
return func(state multistep.StateBag) (*gossh.ClientConfig, error) {
if privateKeyFile != "" {
// key based auth
bytes, err := ioutil.ReadFile(privateKeyFile)
if err != nil {
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
}
privateKey := string(bytes)
keyring := new(ssh.SimpleKeychain)
if err := keyring.AddPEMKey(privateKey); err != nil {
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
}
return &gossh.ClientConfig{
User: username,
Auth: []gossh.ClientAuth{
gossh.ClientAuthKeyring(keyring),
},
}, nil
} else {
// password based auth
return &gossh.ClientConfig{
User: username,
Auth: []gossh.ClientAuth{
gossh.ClientAuthPassword(ssh.Password(password)),
gossh.ClientAuthKeyboardInteractive(ssh.PasswordKeyboardInteractive(password)),
},
}, nil
}
}
}

View File

@ -30,7 +30,8 @@ const defaultConfig = `
"virtualbox-iso": "packer-builder-virtualbox-iso",
"virtualbox-ovf": "packer-builder-virtualbox-ovf",
"vmware-iso": "packer-builder-vmware-iso",
"vmware-vmx": "packer-builder-vmware-vmx"
"vmware-vmx": "packer-builder-vmware-vmx",
"null": "packer-builder-null"
},
"commands": {

View File

@ -0,0 +1,15 @@
package main
import (
"github.com/mitchellh/packer/builder/null"
"github.com/mitchellh/packer/packer/plugin"
)
func main() {
server, err := plugin.Server()
if err != nil {
panic(err)
}
server.RegisterBuilder(new(null.Builder))
server.Serve()
}

View File

@ -0,0 +1 @@
package main

View File

@ -0,0 +1,48 @@
---
layout: "docs"
---
# Null Builder
Type: `null`
The null builder is not really a builder, it just setups a SSH connection
and runs the provisioners. It can be used to debug provisioners without
incurring high wait times. It does not create any kind of image or artifact.
## Basic Example
Below is a fully functioning example. It doesn't do anything useful, since
no provisioners are defined, but it will connect to the specified host via ssh.
<pre class="prettyprint">
{
"type": "null",
"host": "127.0.0.1",
"username": "foo",
"password": "bar"
}
</pre>
## Configuration Reference
Configuration options are organized below into two categories: required and
optional. Within each category, the available options are alphabetized and
described.
Required:
* `host` (string) - The hostname or IP address to connect to.
* `ssh_password` (string) - The password to be used for the ssh connection.
Cannot be combined with ssh_private_key_file.
* `ssh_private_key_file` (string) - The filename of the ssh private key to be
used for the ssh connection. E.g. /home/user/.ssh/identity_rsa.
* `ssh_username` (string) - The username to be used for the ssh connection.
Optional:
* `port` (int) - port to connect to, defaults to 22.