diff --git a/builder/null/artifact_export.go b/builder/null/artifact_export.go new file mode 100644 index 000000000..ad7bebc15 --- /dev/null +++ b/builder/null/artifact_export.go @@ -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 +} diff --git a/builder/null/artifact_export_test.go b/builder/null/artifact_export_test.go new file mode 100644 index 000000000..9e71613a6 --- /dev/null +++ b/builder/null/artifact_export_test.go @@ -0,0 +1,10 @@ +package null + +import ( + "github.com/mitchellh/packer/packer" + "testing" +) + +func TestNullArtifact(t *testing.T) { + var _ packer.Artifact = new(NullArtifact) +} diff --git a/builder/null/builder.go b/builder/null/builder.go new file mode 100644 index 000000000..7ca1b57fd --- /dev/null +++ b/builder/null/builder.go @@ -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() + } +} diff --git a/builder/null/builder_test.go b/builder/null/builder_test.go new file mode 100644 index 000000000..3749b2f68 --- /dev/null +++ b/builder/null/builder_test.go @@ -0,0 +1,10 @@ +package null + +import ( + "github.com/mitchellh/packer/packer" + "testing" +) + +func TestBuilder_implBuilder(t *testing.T) { + var _ packer.Builder = new(Builder) +} diff --git a/builder/null/config.go b/builder/null/config.go new file mode 100644 index 000000000..94f3b21e7 --- /dev/null +++ b/builder/null/config.go @@ -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 +} diff --git a/builder/null/config_test.go b/builder/null/config_test.go new file mode 100644 index 000000000..dd574de35 --- /dev/null +++ b/builder/null/config_test.go @@ -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) +} diff --git a/builder/null/ssh.go b/builder/null/ssh.go new file mode 100644 index 000000000..97546a219 --- /dev/null +++ b/builder/null/ssh.go @@ -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 + } + } +} diff --git a/config.go b/config.go index 1597032d2..1a58a0862 100644 --- a/config.go +++ b/config.go @@ -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": { diff --git a/plugin/builder-null/main.go b/plugin/builder-null/main.go new file mode 100644 index 000000000..e50c6fe40 --- /dev/null +++ b/plugin/builder-null/main.go @@ -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() +} diff --git a/plugin/builder-null/main_test.go b/plugin/builder-null/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/plugin/builder-null/main_test.go @@ -0,0 +1 @@ +package main diff --git a/website/source/docs/builders/null.html.markdown b/website/source/docs/builders/null.html.markdown new file mode 100644 index 000000000..c1c6c1ebc --- /dev/null +++ b/website/source/docs/builders/null.html.markdown @@ -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. + +
+{ + "type": "null", + "host": "127.0.0.1", + "username": "foo", + "password": "bar" +} ++ +## 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. +