diff --git a/builder/profitbricks/builder.go b/builder/profitbricks/builder.go index 799b5650e..3194b2224 100644 --- a/builder/profitbricks/builder.go +++ b/builder/profitbricks/builder.go @@ -31,7 +31,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe steps := []multistep.Step{ &StepCreateSSHKey{ Debug: b.config.PackerDebug, - DebugKeyPath: fmt.Sprintf("pb_%s.pem", b.config.ServerName), + DebugKeyPath: fmt.Sprintf("pb_%s", b.config.SnapshotName), }, new(stepCreateServer), &communicator.StepConnect{ diff --git a/builder/profitbricks/builder_test.go b/builder/profitbricks/builder_test.go index 7c770bccd..06e6fb981 100644 --- a/builder/profitbricks/builder_test.go +++ b/builder/profitbricks/builder_test.go @@ -81,7 +81,7 @@ func TestBuilderPrepare_Servername(t *testing.T) { t.Fatalf("should not have error: %s", err) } - if b.config.ServerName != expected { - t.Errorf("found %s, expected %s", b.config.ServerName, expected) + if b.config.SnapshotName != expected { + t.Errorf("found %s, expected %s", b.config.SnapshotName, expected) } } \ No newline at end of file diff --git a/builder/profitbricks/config.go b/builder/profitbricks/config.go index eb312d174..da8fe4837 100644 --- a/builder/profitbricks/config.go +++ b/builder/profitbricks/config.go @@ -13,23 +13,25 @@ import ( type Config struct { common.PackerConfig `mapstructure:",squash"` - Comm communicator.Config `mapstructure:",squash"` + Comm communicator.Config `mapstructure:",squash"` - PBUsername string `mapstructure:"pbusername"` - PBPassword string `mapstructure:"pbpassword"` - PBUrl string `mapstructure:"pburl"` + PBUsername string `mapstructure:"username"` + PBPassword string `mapstructure:"password"` + PBUrl string `mapstructure:"url"` - Region string `mapstructure:"region"` - Image string `mapstructure:"image"` - SSHKey string `mapstructure:"sshkey"` - ServerName string `mapstructure:"servername"` - DiskSize int `mapstructure:"disksize"` - DiskType string `mapstructure:"disktype"` - Cores int `mapstructure:"cores"` - Ram int `mapstructure:"ram"` + Region string `mapstructure:"region"` + Image string `mapstructure:"image"` + SSHKey string + SSHKey_path string `mapstructure:"sshkey_path"` + SnapshotName string `mapstructure:"snapshot_name"` + SnapshotPassword string `mapstructure:"snapshot_password"` + DiskSize int `mapstructure:"disksize"` + DiskType string `mapstructure:"disktype"` + Cores int `mapstructure:"cores"` + Ram int `mapstructure:"ram"` - CommConfig communicator.Config `mapstructure:",squash"` - ctx interpolate.Context + CommConfig communicator.Config `mapstructure:",squash"` + ctx interpolate.Context } func NewConfig(raws ...interface{}) (*Config, []string, error) { @@ -57,12 +59,22 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { } c.Comm.SSHPort = 22 - if c.PBUsername == ""{ - c.PBUsername = os.Getenv("PROFITBRICKS_USERNAME") + if c.SnapshotName == "" { + def, err := interpolate.Render("packer-{{timestamp}}", nil) + if err != nil { + panic(err) + } + + // Default to packer-{{ unix timestamp (utc) }} + c.SnapshotName = def } - if c.PBPassword == ""{ - c.PBPassword = os.Getenv("PROFITBRICKS_PASSWORD") + if c.PBUsername == "" { + c.PBUsername = os.Getenv("PROFITBRICKS_USERNAME") + } + + if c.PBPassword == "" { + c.PBPassword = os.Getenv("PROFITBRICKS_PASSWORD") } if c.PBUrl == "" { @@ -108,11 +120,6 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { errs, errors.New("ProfitBricks password is required")) } - if c.ServerName == "" { - errs = packer.MultiErrorAppend( - errs, errors.New("Server Name is required")) - } - if errs != nil && len(errs.Errors) > 0 { return nil, nil, errs } diff --git a/builder/profitbricks/step_create_server.go b/builder/profitbricks/step_create_server.go index a30ac5f79..f65b02a9d 100644 --- a/builder/profitbricks/step_create_server.go +++ b/builder/profitbricks/step_create_server.go @@ -22,11 +22,11 @@ func (s *stepCreateServer) Run(state multistep.StateBag) multistep.StepAction { profitbricks.SetAuth(c.PBUsername, c.PBPassword) - ui.Say("Creating Virutal datacenter...") + ui.Say("Creating Virutal Data Center...") datacenter := profitbricks.CreateDatacenter(profitbricks.CreateDatacenterRequest{ DCProperties: profitbricks.DCProperties{ - Name: c.ServerName, + Name: c.SnapshotName, Location: c.Region, }, }) @@ -45,7 +45,7 @@ func (s *stepCreateServer) Run(state multistep.StateBag) multistep.StepAction { server := profitbricks.CreateServer(datacenter.Id, profitbricks.CreateServerRequest{ ServerProperties: profitbricks.ServerProperties{ - Name: c.ServerName, + Name: c.SnapshotName, Ram: c.Ram, Cores: c.Cores, }, @@ -68,10 +68,11 @@ func (s *stepCreateServer) Run(state multistep.StateBag) multistep.StepAction { volume := profitbricks.CreateVolume(datacenter.Id, profitbricks.CreateVolumeRequest{ VolumeProperties: profitbricks.VolumeProperties{ Size: c.DiskSize, - Name: c.ServerName, + Name: c.SnapshotName, Image: img, Type: c.DiskType, SshKey: []string{c.SSHKey}, + ImagePassword: c.SnapshotPassword, }, }) @@ -91,7 +92,7 @@ func (s *stepCreateServer) Run(state multistep.StateBag) multistep.StepAction { lan := profitbricks.CreateLan(datacenter.Id, profitbricks.CreateLanRequest{ LanProperties: profitbricks.LanProperties{ Public: true, - Name: c.ServerName, + Name: c.SnapshotName, }, }) @@ -107,7 +108,7 @@ func (s *stepCreateServer) Run(state multistep.StateBag) multistep.StepAction { nic := profitbricks.CreateNic(datacenter.Id, server.Id, profitbricks.NicCreateRequest{ NicProperties: profitbricks.NicProperties{ - Name: c.ServerName, + Name: c.SnapshotName, Lan: lan.Id, Dhcp: true, }, diff --git a/builder/profitbricks/step_create_ssh_key.go b/builder/profitbricks/step_create_ssh_key.go index a11f49bae..0d4b36ce8 100644 --- a/builder/profitbricks/step_create_ssh_key.go +++ b/builder/profitbricks/step_create_ssh_key.go @@ -11,6 +11,7 @@ import ( "github.com/mitchellh/multistep" "github.com/mitchellh/packer/packer" "golang.org/x/crypto/ssh" + "io/ioutil" ) type StepCreateSSHKey struct { @@ -20,46 +21,83 @@ type StepCreateSSHKey struct { func (s *StepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction { ui := state.Get("ui").(packer.Ui) + c := state.Get("config").(*Config) - ui.Say("Creating temporary SSH key for instance...") - priv, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - err := fmt.Errorf("Error creating temporary ssh key: %s", err) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt - } + if (c.SSHKey_path == "") { + ui.Say("Creating temporary SSH key for instance...") + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + err := fmt.Errorf("Error creating temporary ssh key: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } - priv_blk := pem.Block{ - Type: "RSA PRIVATE KEY", - Headers: nil, - Bytes: x509.MarshalPKCS1PrivateKey(priv), - } + priv_blk := pem.Block{ + Type: "RSA PRIVATE KEY", + Headers: nil, + Bytes: x509.MarshalPKCS1PrivateKey(priv), + } - pub, err := ssh.NewPublicKey(&priv.PublicKey) - if err != nil { - err := fmt.Errorf("Error creating temporary ssh key: %s", err) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt - } - state.Put("privateKey", string(pem.EncodeToMemory(&priv_blk))) - state.Put("publicKey", string(ssh.MarshalAuthorizedKey(pub))) + pub, err := ssh.NewPublicKey(&priv.PublicKey) + if err != nil { + err := fmt.Errorf("Error creating temporary ssh key: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + state.Put("privateKey", string(pem.EncodeToMemory(&priv_blk))) + state.Put("publicKey", string(ssh.MarshalAuthorizedKey(pub))) - if s.Debug { - ui.Message(fmt.Sprintf("Saving key for debug purposes: %s", s.DebugKeyPath)) + ui.Message(fmt.Sprintf("Saving key to: %s", s.DebugKeyPath)) f, err := os.Create(s.DebugKeyPath) if err != nil { state.Put("error", fmt.Errorf("Error saving debug key: %s", err)) return multistep.ActionHalt } + f.Chmod(os.FileMode(int(0700))) err = pem.Encode(f, &priv_blk) f.Close() if err != nil { state.Put("error", fmt.Errorf("Error saving debug key: %s", err)) return multistep.ActionHalt } + } else { + ui.Say(c.SSHKey_path) + pemBytes, err := ioutil.ReadFile(c.SSHKey_path) + + if (err != nil) { + ui.Error(err.Error()) + return multistep.ActionHalt + } + + block, _ := pem.Decode(pemBytes) + + priv, err := x509.ParsePKCS1PrivateKey(block.Bytes) + + if err != nil { + err := fmt.Errorf("Error creating temporary ssh key: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + priv_blk := pem.Block{ + Type: "RSA PRIVATE KEY", + Headers: nil, + Bytes: x509.MarshalPKCS1PrivateKey(priv), + } + + pub, err := ssh.NewPublicKey(&priv.PublicKey) + if err != nil { + err := fmt.Errorf("Error creating temporary ssh key: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + state.Put("privateKey", string(pem.EncodeToMemory(&priv_blk))) + state.Put("publicKey", string(ssh.MarshalAuthorizedKey(pub))) } return multistep.ActionContinue } diff --git a/builder/profitbricks/step_take_snapshot.go b/builder/profitbricks/step_take_snapshot.go index 945f663cc..04ea8a685 100644 --- a/builder/profitbricks/step_take_snapshot.go +++ b/builder/profitbricks/step_take_snapshot.go @@ -23,9 +23,9 @@ func (s *stepTakeSnapshot) Run(state multistep.StateBag) multistep.StepAction { dcId := state.Get("datacenter_id").(string) volumeId := state.Get("volume_id").(string) - snapshot := profitbricks.CreateSnapshot(dcId, volumeId, c.ServerName+"Snapshot") + snapshot := profitbricks.CreateSnapshot(dcId, volumeId, c.SnapshotName) - state.Put("snapshotname", c.ServerName+"Snapshot") + state.Put("snapshotname", c.SnapshotName) err := s.checkForErrors(snapshot) if err != nil { diff --git a/website/source/docs/builders/profitbricks.html.md b/website/source/docs/builders/profitbricks.html.md index 0810b7cf1..a923c3baf 100644 --- a/website/source/docs/builders/profitbricks.html.md +++ b/website/source/docs/builders/profitbricks.html.md @@ -1,71 +1,66 @@ ---- -description: | - The `profitbricks` Packer builder is able to create new images for use with - ProfitBricks. The builder takes a source image, runs any provisioning necessary - on the image after launching it, then snapshots it into a reusable image. This - reusable image can then be used as the foundation for new servers that are - launched within ProfitBricks. -layout: docs -page_title: ProfitBricks Builder -... +# Profitbricks packer builder -# ProfitBricks Builder +This builder plugin extends packer.io to support building images for ProfitBricks. -Type: `profitbricks` +You can check out Packer [here](https://packer.io). -The `profitbricks` Packer builder is able to create new images for use with -[ProfitBricks](https://www.profitbricks.com). The builder takes a source image, -runs any provisioning necessary on the image after launching it, then snapshots -it into a reusable image. This reusable image can then be used as the foundation -for new servers that are launched within ProfitBricks. -The builder does *not* manage images. Once it creates an image, it is up to you -to either use it or delete it. +## Dependencies +* Packer >= v0.10.1 (https://packer.io) +* Golang (tested with 1.6) +* Godep >= v62 -## Configuration Reference -There are many configuration options available for the builder. They are -divided into two categories: required and optional parameters. Within -each category, the available configuration keys are alphabetized. +## Install Go -In addition to the options listed here, a -[communicator](/docs/templates/communicator.html) can be configured for this -builder. +Follow these instructions to install Go(lang) on your system: +* https://golang.org/doc/install -### Required: +## Install Packer -- `pbpasswrod` (string) - ProfitBricks password. It - can also be specified via the environment variable `PROFITBRICKS_PASSWORD`, - if set. - -- `pbusername` (string) - ProfitBricks username. It - can also be specified via the environment variable `PROFITBRICKS_USERNAME`, - if set. +Follow these instructions to install Packer: +* https://www.packer.io/intro/getting-started/setup.html -- `servername` (string) - The name of the server that will be created. +## Compile the plugin -### Optional: +Once you have installed Packer, you must compile this plugin and install the +resulting binary. -- `cores` (int) - Number of server cores. Default value is 4. +```shell +go get https://github.com/profitbricks/packer-builder-profitbricks +cd $GOPATH/src/github.com/profitbricks/packer-builder-profitbricks +make install +``` -- `disksize` (string) - Desired disk size. Default value is 50GB. +If the build is successful, you should now have the `packer-builder-profitbricks` +binary in: -- `disktype` (string) - Desired disk type. The default value is "HDD". +* Linux/Mac: the `~/.packer.d/plugins` directory. +* Windows: the `%APPDATA%/packer.d/plugins` directory. -- `image` (string) - ProfitBricks volume image. The default value is `Ubuntu-16.04`. +If this binary is in the right location, you are ready to get started with Packer. -- `pburl` (string) - ProfitBricks REST Url. +## Download the plugin -- `ram` (int) - RAM size for the server. The default value is 2048. +Alternatively, you can download prebuilt binaries from https://github.com/profitbricks/packer-builder-profitbricks/releases/tag/v1.0.0. -- `region` (string) - ProfitBricks region. The default value is "us/las". +After you have downloaded the binary for your operating system: -## Basic Example +* Linux/Mac: Place the binary in the `~/.packer.d/plugins` directory. +* Windows: Place the binary in the `%APPDATA%/packer.d/plugins` directory. -Here is a basic example. It is completely valid as soon as you enter your own -access tokens: +## Example -``` {.javascript} +Once you have set everything up, you are ready to start with an example. +To get a quick start run: + +```shell +cd $GOPATH/src/github.com/profitbricks/packer-builder-profitbricks +``` + +There you will find example `config.json` + +``` { "builders": [ { @@ -77,3 +72,66 @@ access tokens: } } ``` + +To validate `config.json` run: + +```shell +packer validate config.json +``` + +Or if you want to get suggestions on how to fix your config, run: + +```shell +packer fix config.json +``` + +To build a ProfitBricks Packer image run: + +```shell +packer build config.json + +==> profitbricks: Creating temporary SSH key for instance... +==> profitbricks: Creating Virutal Data Center... +==> profitbricks: Creating ProfitBricks server... +==> profitbricks: Creating a volume +==> profitbricks: Creating a LAN +==> profitbricks: Creating a NIC +==> profitbricks: Waiting for SSH to become available... +==> profitbricks: Creating ProfitBricks snapshot... +==> profitbricks: Removing Virtual Data Center +Build 'profitbricks' finished. + + +==> Builds finished. The artifacts of successful builds are: +--> profitbricks: A snapshot was created: 'packerSnapshot' + +``` + +## Available config parameters + +Required parameters: + +```shell +"type" - Builder type +"username" - ProfitBricks username +"password" - ProfitiBricks password +``` + +Optional parameters: + +```shell +"snapshot_password" - Snapshot password +"snapshot_name" - Snapshot name. If snapshot name is not provided Packer will generate it +"sshkey_path" - Path to private SSHkey. If no path to the key is provided Packer will create one under the name [snapshot_name] +"url" - ProfitBricks REST Url +"image" - ProfitBricks volume image +"region" - ProfitBricks region default value "us/las" +"disksize" - Desired disk size default value 50gb +"disktype" - Desired disk type default value "HDD" +"cores" - Number of server cores default value 4 +"ram" - RAM size for the server default value "2048" +``` + +## Support + +You are welcome to contact us with questions or comments at [ProfitBricks DevOps Central](https://devops.profitbricks.com/). Please report any issues via [GitHub's issue tracker](https://github.com/profitbricks/docker-machine-driver-profitbricks/issues).