From 787a3178b3d3fc4ac6f64171de9077d60240ff2d Mon Sep 17 00:00:00 2001 From: Jack Pearkes Date: Thu, 13 Jun 2013 16:03:10 +0200 Subject: [PATCH] builder/digitalocean: WIP commit of api interface and initial config --- builder/digitalocean/api.go | 162 ++++++++++++++++++++++++++++++++ builder/digitalocean/builder.go | 112 ++++++++++++++++++++++ 2 files changed, 274 insertions(+) create mode 100644 builder/digitalocean/api.go create mode 100644 builder/digitalocean/builder.go diff --git a/builder/digitalocean/api.go b/builder/digitalocean/api.go new file mode 100644 index 000000000..24aaddc67 --- /dev/null +++ b/builder/digitalocean/api.go @@ -0,0 +1,162 @@ +// All of the methods used to communicate with the digital_ocean API +// are here. Their API is on a path to V2, so just plain JSON is used +// in place of a proper client library for now. + +package digitalocean + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" +) + +const DIGITALOCEAN_API_URL = "https://api.digitalocean.com" + +type DigitalOceanClient struct { + // The http client for communicating + client *http.client + + // The base URL of the API + BaseURL string + + // Credentials + ClientID string + APIKey string +} + +// Creates a new client for communicating with DO +func (d DigitalOceanClient) New(client string, key string) *Client { + c := &DigitalOceanClient{ + client: http.DefaultClient, + BaseURL: DIGITALOCEAN_API_URL, + ClientID: client, + APIKey: key, + } + return c +} + +// Creates an SSH Key and returns it's id +func (d DigitalOceanClient) CreateKey(name string, pub string) (uint, error) { + params := fmt.Sprintf("?name=%s&ssh_pub_key=%s", name, pub) + + body, err := NewRequest(d, "ssh_keys/new", params) + if err != nil { + return nil, err + } + + // Read the SSH key's ID we just created + key := body["ssh_key"].(map[string]interface{}) + keyId := key["id"].(float64) + return uint(keyId), nil +} + +// Destroys an SSH key +func (d DigitalOceanClient) DestroyKey(id uint) error { + path := fmt.Sprintf("ssh_keys/%s/destroy", id) + _, err := NewRequest(d, path, "") + return err +} + +// Creates a droplet and returns it's id +func (d DigitalOceanClient) CreateDroplet(name string, size uint, image uint, region uint, keyId uint) (uint, error) { + params := fmt.Sprintf( + "name=%s&size_id=%s&image_id=%s&size_id=%s&image_id=%s®ion_id=%s&ssh_key_ids=%s", + name, size, image, size, region, keyId) + + body, err := NewRequest(d, "droplets/new", params) + if err != nil { + return nil, err + } + + // Read the Droplets ID + droplet := body["droplet"].(map[string]interface{}) + dropletId := droplet["id"].(float64) + return dropletId, err +} + +// Destroys a droplet +func (d DigitalOceanClient) DestroyDroplet(id uint) error { + path := fmt.Sprintf("droplets/%s/destroy", id) + _, err := NewRequest(d, path, "") + return err +} + +// Powers off a droplet +func (d DigitalOceanClient) PowerOffDroplet(name string, pub string) error { + path := fmt.Sprintf("droplets/%s/power_off", id) + + _, err := NewRequest(d, path, "") + + return err +} + +// Creates a snaphot of a droplet by it's ID +func (d DigitalOceanClient) CreateSnapshot(id uint, name string) error { + path := fmt.Sprintf("droplets/%s/snapshot", id) + params := fmt.Sprintf("name=%s", name) + + _, err := NewRequest(d, path, params) + + return err +} + +// Returns DO's string representation of status "off" "new" "active" etc. +func (d DigitalOceanClient) DropletStatus(id uint) (string, error) { + path := fmt.Sprintf("droplets/%s", id) + + body, err := NewRequest(d, path, "") + if err != nil { + return nil, err + } + + // Read the droplet's "status" + droplet := body["droplet"].(map[string]interface{}) + status := droplet["status"].(string) + + return status, err +} + +// Sends an api request and returns a generic map[string]interface of +// the response. +func NewRequest(d DigitalOceanClient, path string, params string) (map[string]interface{}, error) { + client := d.client + url := fmt.Sprintf("%s/%s?%s&client_id=%s&api_key=%s", + DIGITALOCEAN_API_URL, path, params, d.ClientID, d.APIKey) + + resp, err := client.Get(url) + if err != nil { + return nil, err + } + + body, err = ioutil.ReadAll(resp.Body) + + resp.Body.Close() + if err != nil { + return nil, err + } + + // Catch all non-200 status and return an error + if resp.StatusCode != 200 { + err = errors.New("recieved non-200 status from digitalocean: %d", resp.StatusCode) + return nil, err + } + + var decodedResponse map[string]interface{} + + err = json.Unmarshal(body, &decodedResponse) + + if err != nil { + return nil, err + } + + // Catch all non-OK statuses from DO and return an error + status := decodedResponse["status"] + if status != "OK" { + err = errors.New("recieved non-OK status from digitalocean: %d", status) + return nil, err + } + + return decodedResponse, nil +} diff --git a/builder/digitalocean/builder.go b/builder/digitalocean/builder.go new file mode 100644 index 000000000..1b3919e29 --- /dev/null +++ b/builder/digitalocean/builder.go @@ -0,0 +1,112 @@ +// The digitalocean package contains a packer.Builder implementation +// that builds DigitalOcean images (snapshots). + +package digitalocean + +import ( + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" + "time" +) + +// The unique id for the builder +const BuilderId = "pearkes.digitalocean" + +// Configuration tells the builder the credentials +// to use while communicating with DO and describes the image +// you are creating +type config struct { + // Credentials + ClientID string `mapstructure:"client_id"` + APIKey string `mapstructure:"api_key"` + + RegionID uint `mapstructure:"region_id"` + SizeID uint `mapstructure:"size_id"` + ImageID uint `mapstructure:"image_id"` + SSHUsername string `mapstructure:"ssh_username"` + SSHPort uint `mapstructure:"ssh_port"` + + // Configuration for the image being built + SnapshotName string `mapstructure:"snapshot_name"` + + RawSSHTimeout string `mapstructure:"ssh_timeout"` +} + +type Builder struct { + config config + runner multistep.Runner +} + +func (b *Builder) Prepare(raw interface{}) error { + if err := mapstructure.Decode(raw, &b.config); err != nil { + return err + } + + // Optional configuration with defaults + // + if b.config.RegionID == 0 { + // Default to Region "New York" + b.config.RegionID = 1 + } + + if b.config.SizeID == 0 { + // Default to 512mb, the smallest droplet size + b.config.SizeID = 66 + } + + if b.config.ImageID == 0 { + // Default to base image "Ubuntu 12.04 x64 Server" + b.config.ImageID = 2676 + } + + if b.config.SSHUsername == "" { + // Default to "root". You can override this if your + // SourceImage has a different user account then the DO default + b.config.SSHUsername = "root" + } + + if b.config.SSHPort == 0 { + // Default to port 22 per DO default + b.config.SSHPort = 22 + } + + if b.config.SnapshotName == "" { + // Default to packer-{{ unix timestamp (utc) }} + b.config.SnapshotName = "packer-{{.CreateTime}}" + } + + if b.config.RawSSHTimeout == "" { + // Default to 1 minute timeouts + b.config.RawSSHTimeout = "1m" + } + + // A list of errors on the configuration + errs := make([]error, 0) + + // Required configurations that will display errors if not set + // + if b.config.ClientId == "" { + errs = append(errs, errors.New("a client_id must be specified")) + } + + if b.config.APIKey == "" { + errs = append(errs, errors.New("an api_key must be specified")) + } + + b.config.SSHTimeout, err = time.ParseDuration(b.config.RawSSHTimeout) + if err != nil { + errs = append(errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err)) + } + + if len(errs) > 0 { + return &packer.MultiError{errs} + } + + log.Printf("Config: %+v", b.config) + return nil +} + +func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { + +}