implemented null buider

The null builder is not really a bulider, 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.
This commit is contained in:
Florian Noeding 2014-03-24 11:20:32 +01:00
parent dc21bf011a
commit b879ec85cc
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-iso": "packer-builder-virtualbox-iso",
"virtualbox-ovf": "packer-builder-virtualbox-ovf", "virtualbox-ovf": "packer-builder-virtualbox-ovf",
"vmware-iso": "packer-builder-vmware-iso", "vmware-iso": "packer-builder-vmware-iso",
"vmware-vmx": "packer-builder-vmware-vmx" "vmware-vmx": "packer-builder-vmware-vmx",
"null": "packer-builder-null"
}, },
"commands": { "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.