Skip to content
This repository has been archived by the owner on Oct 4, 2021. It is now read-only.

Commit

Permalink
Add initial prototype
Browse files Browse the repository at this point in the history
Works fine, not all features tested, installation instructions semi-incomplete, missing a few minor features + more documentation
  • Loading branch information
Sam Stenvall committed Jul 12, 2016
1 parent d439875 commit 803d3e6
Show file tree
Hide file tree
Showing 11 changed files with 747 additions and 25 deletions.
25 changes: 1 addition & 24 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,24 +1 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so

# Folders
_obj
_test

# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out

*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*

_testmain.go

*.exe
*.test
*.prof
.idea
File renamed without changes.
68 changes: 67 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,68 @@
# packer-builder-upcloud
A Packer builder for UpCloud

This is a Packer builder which can be used to generate storage templates on UpCloud.

## Installation

To install the builder you'll need to download it, install it, then copy the binary to Packer's plugin directory.

```
go get github.com/jalle19/packer-builder-upcloud
cd $GOPATH/src/github.com/jalle19/packer-builder-upcloud
go install
cp $GOPATH/bin/packer-builder-upcloud ~/.packer.d/plugins
```

## Usage

Here is a sample template:

```json
{
"variables": {
"UPCLOUD_API_USERNAME": "your username",
"UPCLOUD_API_PASSWORD": "your password"
},
"builders": [
{
"type": "upcloud",
"username": "{{ user `UPCLOUD_API_USERNAME` }}",
"password": "{{ user `UPCLOUD_API_PASSWORD` }}",
"plan": "1xCPU-1GB",
"zone": "fi-hel1",
"storage_uuid": "01000000-0000-4000-8000-000030040200"
}
]
}
```

If everything goes according to plan, you should see something like this:

```
packer build packer.json
upcloud output will be in this color.
==> upcloud: Creating temporary SSH key ...
==> upcloud: Creating server "packer-builder-upcloud-1468327456" ...
==> upcloud: Waiting for server "packer-builder-upcloud-1468327456" to enter the "started" state ...
==> upcloud: Server "packer-builder-upcloud-1468327456" is now in "started" state
==> upcloud: Waiting for SSH to become available...
==> upcloud: Connected to SSH!
==> upcloud: Provisioning with shell script: scripts/update.sh
...
==> upcloud: Stopping server "packer-builder-upcloud-1468327456" ...
==> upcloud: Waiting for server "packer-builder-upcloud-1468327456" to enter the "stopped" state ...
==> upcloud: Server "packer-builder-upcloud-1468327456" is now in "stopped" state
==> upcloud: Templatizing storage device "packer-builder-upcloud-1468327456-disk1" ...
==> upcloud: Waiting for storage "packer-builder-upcloud-1468327456-disk1-template-1468327515" to enter the "online" state
==> upcloud: Waiting for server "packer-builder-upcloud-1468327456" to exit the "maintenance" state ...
==> upcloud: Deleting server "packer-builder-upcloud-1468327456" ...
Build 'upcloud' finished.
==> Builds finished. The artifacts of successful builds are:
--> upcloud: Private template (UUID: 01875f67-4eb5-4d90-982c-d7a164646fcb, Title: packer-builder-upcloud-1468327456-disk1-template-1468327515, Zone: fi-hel1)
```

## License

MIT
48 changes: 48 additions & 0 deletions builder/upcloud/artifact.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package upcloud

import (
"fmt"
"github.com/jalle19/upcloud-go-sdk/upcloud/request"
"github.com/jalle19/upcloud-go-sdk/upcloud/service"
"log"
)

// Artifact represents a template of a storage as the result of a Packer build
type Artifact struct {
UUID string
Zone string
Title string
service *service.Service
}

// BuilderId returns the unique identifier of this builder
func (*Artifact) BuilderId() string {
return BuilderId
}

// Destroy destroys the template
func (a *Artifact) Destroy() error {
log.Printf("Deleting template \"%s\"", a.Title)

return a.service.DeleteStorage(&request.DeleteStorageRequest{
UUID: a.UUID,
})
}

// Files returns the files represented by the artifact
func (*Artifact) Files() []string {
return nil
}

func (a *Artifact) Id() string {
return a.UUID
}

func (*Artifact) State(name string) interface{} {
return nil
}

// String returns the string representation of the artifact. It is printed at the end of builds.
func (a *Artifact) String() string {
return fmt.Sprintf("Private template (UUID: %s, Title: %s, Zone: %s)", a.UUID, a.Title, a.Zone)
}
211 changes: 211 additions & 0 deletions builder/upcloud/builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package upcloud

import (
"errors"
"fmt"
"github.com/jalle19/upcloud-go-sdk/upcloud"
"github.com/jalle19/upcloud-go-sdk/upcloud/client"
"github.com/jalle19/upcloud-go-sdk/upcloud/service"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate"
"log"
"time"
)

// The unique ID for this builder.
const BuilderId = "jalle19.upcloud"

// Config represents the configuration for this builder
type Config struct {
common.PackerConfig `mapstructure:",squash"`
Comm communicator.Config `mapstructure:",squash"`

Username string `mapstructure:"username"`
Password string `mapstructure:"password"`

Plan string `mapstructure:"plan"`
CoreNumber int `mapstructure:"core_number"`
MemoryAmount int `mapstructure:"memory_amount"`
Title string `mapstructure:"title"`
Hostname string `mapstructure:"hostname"`
Zone string `mapstructure:"zone"`
StorageUUID string `mapstructure:"storage_uuid"`
StorageTitle string `mapstructure:"storage_title"`
StorageSize int `mapstructure:"storage_size"`
StorageTier string `mapstructure:"storage_tier"`

RawStateTimeoutDuration string `mapstructure:"state_timeout_duration"`
StateTimeoutDuration time.Duration

ctx interpolate.Context
}

// Builder represents a Packer Builder.
type Builder struct {
config Config
runner multistep.Runner
}

// Prepare processes the build configuration parameters and validates the configuration
func (self *Builder) Prepare(raws ...interface{}) (parms []string, retErr error) {
// Parse configuration
err := config.Decode(&self.config, &config.DecodeOpts{
Interpolate: true,
InterpolateContext: &self.config.ctx,
}, raws...)

if err != nil {
return nil, err
}

// Assign default values if possible
if self.config.Title == "" {
self.config.Title = fmt.Sprintf("packer-builder-upcloud-%d", time.Now().Unix())
}

if self.config.Hostname == "" {
self.config.Hostname = fmt.Sprintf("%s.example.com", self.config.Title)
}

if self.config.StorageSize == 0 {
self.config.StorageSize = 30
}

if self.config.StorageTitle == "" {
self.config.StorageTitle = fmt.Sprintf("%s-disk1", self.config.Title)
}

if self.config.StorageTier == "" {
self.config.StorageTier = upcloud.StorageTierMaxIOPS
}

if self.config.Comm.SSHUsername == "" {
self.config.Comm.SSHUsername = "root"
}

if self.config.RawStateTimeoutDuration == "" {
self.config.RawStateTimeoutDuration = "5m"
}

// Validation
var errs *packer.MultiError
errs = packer.MultiErrorAppend(errs, self.config.Comm.Prepare(&self.config.ctx)...)

// Check for required configurations that will display errors if not set
if self.config.Username == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("\"username\" must be specified"))
}

if self.config.Password == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("\"password\" must be specified"))
}

if self.config.Plan == "" && (self.config.CoreNumber == 0 || self.config.MemoryAmount == 0) {
errs = packer.MultiErrorAppend(
errs, errors.New("\"core_number\" and \"memory_amount\" must be specified if \"plan\" is not specified"))
}

if self.config.Plan != "" && (self.config.CoreNumber > 0 || self.config.MemoryAmount > 0) {
errs = packer.MultiErrorAppend(
errs, errors.New("\"core_number\" and \"memory_amount\" must not be specified when \"plan\" is specified"))
}

if self.config.Zone == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("\"zone\" must be specified"))
}

if self.config.StorageUUID == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("\"storage_uuid\" must be specified"))
}

stateTimeout, err := time.ParseDuration(self.config.RawStateTimeoutDuration)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed to parse state_timeout_duration: %s", err))
}
self.config.StateTimeoutDuration = stateTimeout

log.Println(common.ScrubConfig(self.config, self.config.Password, self.config.Username))

if len(errs.Errors) > 0 {
retErr = errors.New(errs.Error())
}

return nil, retErr
}

// Run executes the actual build steps
func (self *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
// Create the service
client := client.New(self.config.Username, self.config.Password)
service := service.New(client)

// Set up the state which is used to share state between the steps
state := new(multistep.BasicStateBag)
state.Put("config", self.config)
state.Put("service", *service)
state.Put("hook", hook)
state.Put("ui", ui)

// Build the steps
steps := []multistep.Step{
&StepCreateSSHKey{
Debug: self.config.PackerDebug,
DebugKeyPath: fmt.Sprintf("packer-builder-upcloud-%s.pem", self.config.PackerBuildName),
},
new(StepCreateServer),
&communicator.StepConnect{
Config: &self.config.Comm,
Host: sshHostCallback,
SSHConfig: sshConfigCallback,
},
new(common.StepProvision),
new(StepTemplatizeStorage),
}

// Create the runner which will run the steps we just build
self.runner = &multistep.BasicRunner{Steps: steps}
self.runner.Run(state)

if rawErr, ok := state.GetOk("error"); ok {
return nil, rawErr.(error)
}

// Extract the final storage details from the state
rawDetails, ok := state.GetOk("storage_details")

if !ok {
log.Println("No storage details found in state, the build was probably cancelled")
return nil, nil
}

storageDetails := rawDetails.(*upcloud.StorageDetails)

// Create an artifact and return it
artifact := &Artifact{
UUID: storageDetails.UUID,
Zone: storageDetails.Zone,
Title: storageDetails.Title,
service: service,
}

return artifact, nil
}

// Cancel is called when the build is cancelled
func (self *Builder) Cancel() {
if self.runner != nil {
log.Println("Cancelling the step runner ...")
self.runner.Cancel()
}

fmt.Println("Cancelling the builder ...")
}
15 changes: 15 additions & 0 deletions builder/upcloud/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package upcloud

import (
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)

// Helper for easily bailing out of the execution flow
func handleError(err error, state multistep.StateBag) multistep.StepAction {
state.Put("error", err)
ui := state.Get("ui").(packer.Ui)
ui.Error(err.Error())

return multistep.ActionHalt
}
Loading

0 comments on commit 803d3e6

Please sign in to comment.