Commit 572926d9 authored by Mitchell Hashimoto's avatar Mitchell Hashimoto

Merge pull request #2206 from mitchellh/b-do

builder/digitalocean: use official API client, v2 only
parents 9da9ce60 311c9eb5
// 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
type Region struct {
Slug string `json:"slug"`
Name string `json:"name"`
// v1 only
Id uint `json:"id,omitempty"`
// v2 only
Sizes []string `json:"sizes,omitempty"`
Available bool `json:"available,omitempty"`
Features []string `json:"features,omitempty"`
}
type RegionsResp struct {
Regions []Region
}
type Size struct {
Slug string `json:"slug"`
// v1 only
Id uint `json:"id,omitempty"`
Name string `json:"name,omitempty"`
// v2 only
Memory uint `json:"memory,omitempty"`
VCPUS uint `json:"vcpus,omitempty"`
Disk uint `json:"disk,omitempty"`
Transfer float64 `json:"transfer,omitempty"`
PriceMonthly float64 `json:"price_monthly,omitempty"`
PriceHourly float64 `json:"price_hourly,omitempty"`
}
type SizesResp struct {
Sizes []Size
}
type Image struct {
Id uint `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
Distribution string `json:"distribution"`
// v2 only
Public bool `json:"public,omitempty"`
ActionIds []string `json:"action_ids,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
}
type ImagesResp struct {
Images []Image
}
type DigitalOceanClient interface {
CreateKey(string, string) (uint, error)
DestroyKey(uint) error
CreateDroplet(string, string, string, string, uint, bool) (uint, error)
DestroyDroplet(uint) error
PowerOffDroplet(uint) error
ShutdownDroplet(uint) error
CreateSnapshot(uint, string) error
Images() ([]Image, error)
DestroyImage(uint) error
DropletStatus(uint) (string, string, error)
Image(string) (Image, error)
Regions() ([]Region, error)
Region(string) (Region, error)
Sizes() ([]Size, error)
Size(string) (Size, error)
}
// 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"
"log"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/mitchellh/mapstructure"
)
type DigitalOceanClientV1 struct {
// The http client for communicating
client *http.Client
// Credentials
ClientID string
APIKey string
// The base URL of the API
APIURL string
}
// Creates a new client for communicating with DO
func DigitalOceanClientNewV1(client string, key string, url string) *DigitalOceanClientV1 {
c := &DigitalOceanClientV1{
client: &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
},
},
APIURL: url,
ClientID: client,
APIKey: key,
}
return c
}
// Creates an SSH Key and returns it's id
func (d DigitalOceanClientV1) CreateKey(name string, pub string) (uint, error) {
params := url.Values{}
params.Set("name", name)
params.Set("ssh_pub_key", pub)
body, err := NewRequestV1(d, "ssh_keys/new", params)
if err != nil {
return 0, 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 DigitalOceanClientV1) DestroyKey(id uint) error {
path := fmt.Sprintf("ssh_keys/%v/destroy", id)
_, err := NewRequestV1(d, path, url.Values{})
return err
}
// Creates a droplet and returns it's id
func (d DigitalOceanClientV1) CreateDroplet(name string, size string, image string, region string, keyId uint, privateNetworking bool) (uint, error) {
params := url.Values{}
params.Set("name", name)
found_size, err := d.Size(size)
if err != nil {
return 0, fmt.Errorf("Invalid size or lookup failure: '%s': %s", size, err)
}
found_image, err := d.Image(image)
if err != nil {
return 0, fmt.Errorf("Invalid image or lookup failure: '%s': %s", image, err)
}
found_region, err := d.Region(region)
if err != nil {
return 0, fmt.Errorf("Invalid region or lookup failure: '%s': %s", region, err)
}
params.Set("size_slug", found_size.Slug)
params.Set("image_slug", found_image.Slug)
params.Set("region_slug", found_region.Slug)
params.Set("ssh_key_ids", fmt.Sprintf("%v", keyId))
params.Set("private_networking", fmt.Sprintf("%v", privateNetworking))
body, err := NewRequestV1(d, "droplets/new", params)
if err != nil {
return 0, err
}
// Read the Droplets ID
droplet := body["droplet"].(map[string]interface{})
dropletId := droplet["id"].(float64)
return uint(dropletId), err
}
// Destroys a droplet
func (d DigitalOceanClientV1) DestroyDroplet(id uint) error {
path := fmt.Sprintf("droplets/%v/destroy", id)
_, err := NewRequestV1(d, path, url.Values{})
return err
}
// Powers off a droplet
func (d DigitalOceanClientV1) PowerOffDroplet(id uint) error {
path := fmt.Sprintf("droplets/%v/power_off", id)
_, err := NewRequestV1(d, path, url.Values{})
return err
}
// Shutsdown a droplet. This is a "soft" shutdown.
func (d DigitalOceanClientV1) ShutdownDroplet(id uint) error {
path := fmt.Sprintf("droplets/%v/shutdown", id)
_, err := NewRequestV1(d, path, url.Values{})
return err
}
// Creates a snaphot of a droplet by it's ID
func (d DigitalOceanClientV1) CreateSnapshot(id uint, name string) error {
path := fmt.Sprintf("droplets/%v/snapshot", id)
params := url.Values{}
params.Set("name", name)
_, err := NewRequestV1(d, path, params)
return err
}
// Returns all available images.
func (d DigitalOceanClientV1) Images() ([]Image, error) {
resp, err := NewRequestV1(d, "images", url.Values{})
if err != nil {
return nil, err
}
var result ImagesResp
if err := mapstructure.Decode(resp, &result); err != nil {
return nil, err
}
return result.Images, nil
}
// Destroys an image by its ID.
func (d DigitalOceanClientV1) DestroyImage(id uint) error {
path := fmt.Sprintf("images/%d/destroy", id)
_, err := NewRequestV1(d, path, url.Values{})
return err
}
// Returns DO's string representation of status "off" "new" "active" etc.
func (d DigitalOceanClientV1) DropletStatus(id uint) (string, string, error) {
path := fmt.Sprintf("droplets/%v", id)
body, err := NewRequestV1(d, path, url.Values{})
if err != nil {
return "", "", err
}
var ip string
// Read the droplet's "status"
droplet := body["droplet"].(map[string]interface{})
status := droplet["status"].(string)
if droplet["ip_address"] != nil {
ip = droplet["ip_address"].(string)
}
return ip, status, err
}
// Sends an api request and returns a generic map[string]interface of
// the response.
func NewRequestV1(d DigitalOceanClientV1, path string, params url.Values) (map[string]interface{}, error) {
client := d.client
// Add the authentication parameters
params.Set("client_id", d.ClientID)
params.Set("api_key", d.APIKey)
url := fmt.Sprintf("%s/%s?%s", d.APIURL, path, params.Encode())
// Do some basic scrubbing so sensitive information doesn't appear in logs
scrubbedUrl := strings.Replace(url, d.ClientID, "CLIENT_ID", -1)
scrubbedUrl = strings.Replace(scrubbedUrl, d.APIKey, "API_KEY", -1)
log.Printf("sending new request to digitalocean: %s", scrubbedUrl)
var lastErr error
for attempts := 1; attempts < 10; attempts++ {
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
}
log.Printf("response from digitalocean: %s", body)
var decodedResponse map[string]interface{}
err = json.Unmarshal(body, &decodedResponse)
if err != nil {
err = errors.New(fmt.Sprintf("Failed to decode JSON response (HTTP %v) from DigitalOcean: %s",
resp.StatusCode, body))
return decodedResponse, err
}
// Check for errors sent by digitalocean
status := decodedResponse["status"].(string)
if status == "OK" {
return decodedResponse, nil
}
if status == "ERROR" {
statusRaw, ok := decodedResponse["error_message"]
if ok {
status = statusRaw.(string)
} else {
status = fmt.Sprintf(
"Unknown error. Full response body: %s", body)
}
}
lastErr = errors.New(fmt.Sprintf("Received error from DigitalOcean (%d): %s",
resp.StatusCode, status))
log.Println(lastErr)
if strings.Contains(status, "a pending event") {
// Retry, DigitalOcean sends these dumb "pending event"
// errors all the time.
time.Sleep(5 * time.Second)
continue
}
// Some other kind of error. Just return.
return decodedResponse, lastErr
}
return nil, lastErr
}
func (d DigitalOceanClientV1) Image(slug_or_name_or_id string) (Image, error) {
images, err := d.Images()
if err != nil {
return Image{}, err
}
for _, image := range images {
if strings.EqualFold(image.Slug, slug_or_name_or_id) {
return image, nil
}
}
for _, image := range images {
if strings.EqualFold(image.Name, slug_or_name_or_id) {
return image, nil
}
}
for _, image := range images {
id, err := strconv.Atoi(slug_or_name_or_id)
if err == nil {
if image.Id == uint(id) {
return image, nil
}
}
}
err = errors.New(fmt.Sprintf("Unknown image '%v'", slug_or_name_or_id))
return Image{}, err
}
// Returns all available regions.
func (d DigitalOceanClientV1) Regions() ([]Region, error) {
resp, err := NewRequestV1(d, "regions", url.Values{})
if err != nil {
return nil, err
}
var result RegionsResp
if err := mapstructure.Decode(resp, &result); err != nil {
return nil, err
}
return result.Regions, nil
}
func (d DigitalOceanClientV1) Region(slug_or_name_or_id string) (Region, error) {
regions, err := d.Regions()
if err != nil {
return Region{}, err
}
for _, region := range regions {
if strings.EqualFold(region.Slug, slug_or_name_or_id) {
return region, nil
}
}
for _, region := range regions {
if strings.EqualFold(region.Name, slug_or_name_or_id) {
return region, nil
}
}
for _, region := range regions {
id, err := strconv.Atoi(slug_or_name_or_id)
if err == nil {
if region.Id == uint(id) {
return region, nil
}
}
}
err = errors.New(fmt.Sprintf("Unknown region '%v'", slug_or_name_or_id))
return Region{}, err
}
// Returns all available sizes.
func (d DigitalOceanClientV1) Sizes() ([]Size, error) {
resp, err := NewRequestV1(d, "sizes", url.Values{})
if err != nil {
return nil, err
}
var result SizesResp
if err := mapstructure.Decode(resp, &result); err != nil {
return nil, err
}
return result.Sizes, nil
}
func (d DigitalOceanClientV1) Size(slug_or_name_or_id string) (Size, error) {
sizes, err := d.Sizes()
if err != nil {
return Size{}, err
}
for _, size := range sizes {
if strings.EqualFold(size.Slug, slug_or_name_or_id) {
return size, nil
}
}
for _, size := range sizes {
if strings.EqualFold(size.Name, slug_or_name_or_id) {
return size, nil
}
}
for _, size := range sizes {
id, err := strconv.Atoi(slug_or_name_or_id)
if err == nil {
if size.Id == uint(id) {
return size, nil
}
}
}
err = errors.New(fmt.Sprintf("Unknown size '%v'", slug_or_name_or_id))
return Size{}, err
}
This diff is collapsed.
......@@ -4,6 +4,8 @@ import (
"fmt"
"log"
"strconv"
"github.com/digitalocean/godo"
)
type Artifact struct {
......@@ -11,13 +13,13 @@ type Artifact struct {
snapshotName string
// The ID of the image
snapshotId uint
snapshotId int
// The name of the region
regionName string
// The client for making API calls
client DigitalOceanClient
client *godo.Client
}
func (*Artifact) BuilderId() string {
......@@ -43,5 +45,6 @@ func (a *Artifact) State(name string) interface{} {
func (a *Artifact) Destroy() error {
log.Printf("Destroying image: %d (%s)", a.snapshotId, a.snapshotName)
return a.client.DestroyImage(a.snapshotId)
_, err := a.client.Images.Delete(a.snapshotId)
return err
}
......@@ -4,18 +4,14 @@
package digitalocean
import (
"errors"
"fmt"
"log"
"os"
"time"
"github.com/digitalocean/godo"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate"
"golang.org/x/oauth2"
)
// see https://api.digitalocean.com/images/?client_id=[client_id]&api_key=[api_key]
......@@ -33,179 +29,25 @@ const DefaultSize = "512mb"
// 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 {
common.PackerConfig `mapstructure:",squash"`
ClientID string `mapstructure:"client_id"`
APIKey string `mapstructure:"api_key"`
APIURL string `mapstructure:"api_url"`
APIToken string `mapstructure:"api_token"`
RegionID uint `mapstructure:"region_id"`
SizeID uint `mapstructure:"size_id"`
ImageID uint `mapstructure:"image_id"`
Region string `mapstructure:"region"`
Size string `mapstructure:"size"`
Image string `mapstructure:"image"`
PrivateNetworking bool `mapstructure:"private_networking"`
SnapshotName string `mapstructure:"snapshot_name"`
DropletName string `mapstructure:"droplet_name"`
SSHUsername string `mapstructure:"ssh_username"`
SSHPort uint `mapstructure:"ssh_port"`
RawSSHTimeout string `mapstructure:"ssh_timeout"`
RawStateTimeout string `mapstructure:"state_timeout"`
// These are unexported since they're set by other fields
// being set.
sshTimeout time.Duration
stateTimeout time.Duration
ctx *interpolate.Context
}
type Builder struct {
config Config
runner multistep.Runner
}
func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
err := config.Decode(&b.config, &config.DecodeOpts{
Interpolate: true,
}, raws...)
if err != nil {
return nil, err
}
// Optional configuration with defaults
if b.config.APIKey == "" {
// Default to environment variable for api_key, if it exists
b.config.APIKey = os.Getenv("DIGITALOCEAN_API_KEY")
}
if b.config.ClientID == "" {
// Default to environment variable for client_id, if it exists
b.config.ClientID = os.Getenv("DIGITALOCEAN_CLIENT_ID")
}
if b.config.APIURL == "" {
// Default to environment variable for api_url, if it exists
b.config.APIURL = os.Getenv("DIGITALOCEAN_API_URL")
}
if b.config.APIToken == "" {
// Default to environment variable for api_token, if it exists
b.config.APIToken = os.Getenv("DIGITALOCEAN_API_TOKEN")
}
if b.config.Region == "" {
if b.config.RegionID != 0 {
b.config.Region = fmt.Sprintf("%v", b.config.RegionID)
} else {
b.config.Region = DefaultRegion
}
}
if b.config.Size == "" {
if b.config.SizeID != 0 {
b.config.Size = fmt.Sprintf("%v", b.config.SizeID)
} else {
b.config.Size = DefaultSize
}
}
if b.config.Image == "" {
if b.config.ImageID != 0 {
b.config.Image = fmt.Sprintf("%v", b.config.ImageID)
} else {
b.config.Image = DefaultImage
}
}
if b.config.SnapshotName == "" {
// Default to packer-{{ unix timestamp (utc) }}
b.config.SnapshotName = "packer-{{timestamp}}"
}
if b.config.DropletName == "" {
// Default to packer-[time-ordered-uuid]
b.config.DropletName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
c, warnings, errs := NewConfig(raws...)
if errs != nil {
return warnings, errs
}
b.config = *c
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.RawSSHTimeout == "" {
// Default to 1 minute timeouts
b.config.RawSSHTimeout = "1m"
}
if b.config.RawStateTimeout == "" {
// Default to 6 minute timeouts waiting for
// desired state. i.e waiting for droplet to become active
b.config.RawStateTimeout = "6m"
}
var errs *packer.MultiError
if b.config.APIToken == "" {
// Required configurations that will display errors if not set
if b.config.ClientID == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("a client_id for v1 auth or api_token for v2 auth must be specified"))
}
if b.config.APIKey == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("a api_key for v1 auth or api_token for v2 auth must be specified"))
}
}
if b.config.APIURL == "" {
b.config.APIURL = "https://api.digitalocean.com"
}
sshTimeout, err := time.ParseDuration(b.config.RawSSHTimeout)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
}
b.config.sshTimeout = sshTimeout
stateTimeout, err := time.ParseDuration(b.config.RawStateTimeout)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing state_timeout: %s", err))
}
b.config.stateTimeout = stateTimeout
if errs != nil && len(errs.Errors) > 0 {
return nil, errs
}
common.ScrubConfig(b.config, b.config.ClientID, b.config.APIKey)
return nil, nil
}
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
var client DigitalOceanClient
// Initialize the DO API client
if b.config.APIToken == "" {
client = DigitalOceanClientNewV1(b.config.ClientID, b.config.APIKey, b.config.APIURL)
} else {
client = DigitalOceanClientNewV2(b.config.APIToken, b.config.APIURL)
}
client := godo.NewClient(oauth2.NewClient(oauth2.NoContext, &apiTokenSource{
AccessToken: b.config.APIToken,
}))
// Set up the state
state := new(multistep.BasicStateBag)
......@@ -252,26 +94,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
return nil, nil
}
sregion := state.Get("region")
var region string
if sregion != nil {
region = sregion.(string)
} else {
region = fmt.Sprintf("%v", state.Get("region_id").(uint))
}
found_region, err := client.Region(region)
if err != nil {
return nil, err
}
artifact := &Artifact{
snapshotName: state.Get("snapshot_name").(string),
snapshotId: state.Get("snapshot_image_id").(uint),
regionName: found_region.Name,
snapshotId: state.Get("snapshot_image_id").(int),
regionName: state.Get("region").(string),
client: client,
}
......
package digitalocean
import (
"os"
"testing"
builderT "github.com/mitchellh/packer/helper/builder/testing"
)
func TestBuilderAcc_basic(t *testing.T) {
builderT.Test(t, builderT.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Builder: &Builder{},
Template: testBuilderAccBasic,
})
}
func testAccPreCheck(t *testing.T) {
if v := os.Getenv("DIGITALOCEAN_API_TOKEN"); v == "" {
t.Fatal("DIGITALOCEAN_API_TOKEN must be set for acceptance tests")
}
}
const testBuilderAccBasic = `
{
"builders": [{
"type": "test"
}]
}
`
package digitalocean
import (
"github.com/mitchellh/packer/packer"
"os"
"strconv"
"testing"
)
func init() {
// Clear out the credential env vars
os.Setenv("DIGITALOCEAN_API_KEY", "")
os.Setenv("DIGITALOCEAN_CLIENT_ID", "")
}
"github.com/mitchellh/packer/packer"
)
func testConfig() map[string]interface{} {
return map[string]interface{}{
"client_id": "foo",
"api_key": "bar",
"api_token": "bar",
}
}
......@@ -43,90 +36,6 @@ func TestBuilder_Prepare_BadType(t *testing.T) {
}
}
func TestBuilderPrepare_APIKey(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["api_key"] = "foo"
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.APIKey != "foo" {
t.Errorf("access key invalid: %s", b.config.APIKey)
}
// Test bad
delete(config, "api_key")
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
// Test env variable
delete(config, "api_key")
os.Setenv("DIGITALOCEAN_API_KEY", "foo")
defer os.Setenv("DIGITALOCEAN_API_KEY", "")
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_ClientID(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["client_id"] = "foo"
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.ClientID != "foo" {
t.Errorf("invalid: %s", b.config.ClientID)
}
// Test bad
delete(config, "client_id")
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
// Test env variable
delete(config, "client_id")
os.Setenv("DIGITALOCEAN_CLIENT_ID", "foo")
defer os.Setenv("DIGITALOCEAN_CLIENT_ID", "")
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_InvalidKey(t *testing.T) {
var b Builder
config := testConfig()
......@@ -162,7 +71,6 @@ func TestBuilderPrepare_Region(t *testing.T) {
expected := "sfo1"
// Test set
config["region_id"] = 0
config["region"] = expected
b = Builder{}
warnings, err = b.Prepare(config)
......@@ -198,7 +106,6 @@ func TestBuilderPrepare_Size(t *testing.T) {
expected := "1024mb"
// Test set
config["size_id"] = 0
config["size"] = expected
b = Builder{}
warnings, err = b.Prepare(config)
......@@ -234,7 +141,6 @@ func TestBuilderPrepare_Image(t *testing.T) {
expected := "ubuntu-14-04-x64"
// Test set
config["image_id"] = 0
config["image"] = expected
b = Builder{}
warnings, err = b.Prepare(config)
......
package digitalocean
import (
"errors"
"fmt"
"os"
"time"
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate"
)
type Config struct {
common.PackerConfig `mapstructure:",squash"`
APIToken string `mapstructure:"api_token"`
Region string `mapstructure:"region"`
Size string `mapstructure:"size"`
Image string `mapstructure:"image"`
PrivateNetworking bool `mapstructure:"private_networking"`
SnapshotName string `mapstructure:"snapshot_name"`
DropletName string `mapstructure:"droplet_name"`
SSHUsername string `mapstructure:"ssh_username"`
SSHPort uint `mapstructure:"ssh_port"`
RawSSHTimeout string `mapstructure:"ssh_timeout"`
RawStateTimeout string `mapstructure:"state_timeout"`
// These are unexported since they're set by other fields
// being set.
sshTimeout time.Duration
stateTimeout time.Duration
ctx *interpolate.Context
}
func NewConfig(raws ...interface{}) (*Config, []string, error) {
var c Config
var md mapstructure.Metadata
err := config.Decode(&c, &config.DecodeOpts{
Metadata: &md,
Interpolate: true,
InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{
"run_command",
},
},
}, raws...)
if err != nil {
return nil, nil, err
}
// Defaults
if c.APIToken == "" {
// Default to environment variable for api_token, if it exists
c.APIToken = os.Getenv("DIGITALOCEAN_API_TOKEN")
}
if c.Region == "" {
c.Region = DefaultRegion
}
if c.Size == "" {
c.Size = DefaultSize
}
if c.Image == "" {
c.Image = DefaultImage
}
if c.SnapshotName == "" {
// Default to packer-{{ unix timestamp (utc) }}
c.SnapshotName = "packer-{{timestamp}}"
}
if c.DropletName == "" {
// Default to packer-[time-ordered-uuid]
c.DropletName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
}
if c.SSHUsername == "" {
// Default to "root". You can override this if your
// SourceImage has a different user account then the DO default
c.SSHUsername = "root"
}
if c.SSHPort == 0 {
// Default to port 22 per DO default
c.SSHPort = 22
}
if c.RawSSHTimeout == "" {
// Default to 1 minute timeouts
c.RawSSHTimeout = "1m"
}
if c.RawStateTimeout == "" {
// Default to 6 minute timeouts waiting for
// desired state. i.e waiting for droplet to become active
c.RawStateTimeout = "6m"
}
var errs *packer.MultiError
if c.APIToken == "" {
// Required configurations that will display errors if not set
errs = packer.MultiErrorAppend(
errs, errors.New("api_token for auth must be specified"))
}
sshTimeout, err := time.ParseDuration(c.RawSSHTimeout)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
}
c.sshTimeout = sshTimeout
stateTimeout, err := time.ParseDuration(c.RawStateTimeout)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing state_timeout: %s", err))
}
c.stateTimeout = stateTimeout
if errs != nil && len(errs.Errors) > 0 {
return nil, nil, errs
}
common.ScrubConfig(c, c.APIToken)
return &c, nil, nil
}
......@@ -3,25 +3,35 @@ package digitalocean
import (
"fmt"
"github.com/digitalocean/godo"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
type stepCreateDroplet struct {
dropletId uint
dropletId int
}
func (s *stepCreateDroplet) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(DigitalOceanClient)
client := state.Get("client").(*godo.Client)
ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(Config)
sshKeyId := state.Get("ssh_key_id").(uint)
ui.Say("Creating droplet...")
sshKeyId := state.Get("ssh_key_id").(int)
// Create the droplet based on configuration
dropletId, err := client.CreateDroplet(c.DropletName, c.Size, c.Image, c.Region, sshKeyId, c.PrivateNetworking)
ui.Say("Creating droplet...")
droplet, _, err := client.Droplets.Create(&godo.DropletCreateRequest{
Name: c.DropletName,
Region: c.Region,
Size: c.Size,
Image: godo.DropletCreateImage{
Slug: c.Image,
},
SSHKeys: []godo.DropletCreateSSHKey{
godo.DropletCreateSSHKey{ID: int(sshKeyId)},
},
PrivateNetworking: c.PrivateNetworking,
})
if err != nil {
err := fmt.Errorf("Error creating droplet: %s", err)
state.Put("error", err)
......@@ -30,10 +40,10 @@ func (s *stepCreateDroplet) Run(state multistep.StateBag) multistep.StepAction {
}
// We use this in cleanup
s.dropletId = dropletId
s.dropletId = droplet.ID
// Store the droplet id for later
state.Put("droplet_id", dropletId)
state.Put("droplet_id", droplet.ID)
return multistep.ActionContinue
}
......@@ -44,19 +54,14 @@ func (s *stepCreateDroplet) Cleanup(state multistep.StateBag) {
return
}
client := state.Get("client").(DigitalOceanClient)
client := state.Get("client").(*godo.Client)
ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(Config)
// Destroy the droplet we just created
ui.Say("Destroying droplet...")
err := client.DestroyDroplet(s.dropletId)
_, err := client.Droplets.Delete(s.dropletId)
if err != nil {
curlstr := fmt.Sprintf("curl '%v/droplets/%v/destroy?client_id=%v&api_key=%v'",
c.APIURL, s.dropletId, c.ClientID, c.APIKey)
ui.Error(fmt.Sprintf(
"Error destroying droplet. Please destroy it manually: %v", curlstr))
"Error destroying droplet. Please destroy it manually: %s", err))
}
}
......@@ -9,17 +9,18 @@ import (
"log"
"code.google.com/p/gosshold/ssh"
"github.com/digitalocean/godo"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/packer"
)
type stepCreateSSHKey struct {
keyId uint
keyId int
}
func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(DigitalOceanClient)
client := state.Get("client").(*godo.Client)
ui := state.Get("ui").(packer.Ui)
ui.Say("Creating temporary ssh key for droplet...")
......@@ -46,7 +47,10 @@ func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction {
name := fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
// Create the key!
keyId, err := client.CreateKey(name, pub_sshformat)
key, _, err := client.Keys.Create(&godo.KeyCreateRequest{
Name: name,
PublicKey: pub_sshformat,
})
if err != nil {
err := fmt.Errorf("Error creating temporary SSH key: %s", err)
state.Put("error", err)
......@@ -55,12 +59,12 @@ func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction {
}
// We use this to check cleanup
s.keyId = keyId
s.keyId = key.ID
log.Printf("temporary ssh key name: %s", name)
// Remember some state for the future
state.Put("ssh_key_id", keyId)
state.Put("ssh_key_id", key.ID)
return multistep.ActionContinue
}
......@@ -71,18 +75,14 @@ func (s *stepCreateSSHKey) Cleanup(state multistep.StateBag) {
return
}
client := state.Get("client").(DigitalOceanClient)
client := state.Get("client").(*godo.Client)
ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(Config)
ui.Say("Deleting temporary ssh key...")
err := client.DestroyKey(s.keyId)
curlstr := fmt.Sprintf("curl -H 'Authorization: Bearer #TOKEN#' -X DELETE '%v/v2/account/keys/%v'", c.APIURL, s.keyId)
_, err := client.Keys.DeleteByID(s.keyId)
if err != nil {
log.Printf("Error cleaning up ssh key: %v", err.Error())
log.Printf("Error cleaning up ssh key: %s", err)
ui.Error(fmt.Sprintf(
"Error cleaning up ssh key. Please delete the key manually: %v", curlstr))
"Error cleaning up ssh key. Please delete the key manually: %s", err))
}
}
......@@ -3,6 +3,7 @@ package digitalocean
import (
"fmt"
"github.com/digitalocean/godo"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
......@@ -10,10 +11,10 @@ import (
type stepDropletInfo struct{}
func (s *stepDropletInfo) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(DigitalOceanClient)
client := state.Get("client").(*godo.Client)
ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(Config)
dropletId := state.Get("droplet_id").(uint)
dropletId := state.Get("droplet_id").(int)
ui.Say("Waiting for droplet to become active...")
......@@ -26,16 +27,25 @@ func (s *stepDropletInfo) Run(state multistep.StateBag) multistep.StepAction {
}
// Set the IP on the state for later
ip, _, err := client.DropletStatus(dropletId)
droplet, _, err := client.Droplets.Get(dropletId)
if err != nil {
err := fmt.Errorf("Error retrieving droplet ID: %s", err)
err := fmt.Errorf("Error retrieving droplet: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
state.Put("droplet_ip", ip)
// Verify we have an IPv4 address
invalid := droplet.Networks == nil ||
len(droplet.Networks.V4) == 0
if invalid {
err := fmt.Errorf("IPv4 address not found for droplet!")
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
state.Put("droplet_ip", droplet.Networks.V4[0].IPAddress)
return multistep.ActionContinue
}
......
......@@ -4,6 +4,7 @@ import (
"fmt"
"log"
"github.com/digitalocean/godo"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
......@@ -11,12 +12,12 @@ import (
type stepPowerOff struct{}
func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(DigitalOceanClient)
client := state.Get("client").(*godo.Client)
c := state.Get("config").(Config)
ui := state.Get("ui").(packer.Ui)
dropletId := state.Get("droplet_id").(uint)
dropletId := state.Get("droplet_id").(int)
_, status, err := client.DropletStatus(dropletId)
droplet, _, err := client.Droplets.Get(dropletId)
if err != nil {
err := fmt.Errorf("Error checking droplet state: %s", err)
state.Put("error", err)
......@@ -24,14 +25,14 @@ func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionHalt
}
if status == "off" {
if droplet.Status == "off" {
// Droplet is already off, don't do anything
return multistep.ActionContinue
}
// Pull the plug on the Droplet
ui.Say("Forcefully shutting down Droplet...")
err = client.PowerOffDroplet(dropletId)
_, _, err = client.DropletActions.PowerOff(dropletId)
if err != nil {
err := fmt.Errorf("Error powering off droplet: %s", err)
state.Put("error", err)
......
......@@ -5,6 +5,7 @@ import (
"log"
"time"
"github.com/digitalocean/godo"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
......@@ -12,16 +13,16 @@ import (
type stepShutdown struct{}
func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(DigitalOceanClient)
client := state.Get("client").(*godo.Client)
ui := state.Get("ui").(packer.Ui)
dropletId := state.Get("droplet_id").(uint)
dropletId := state.Get("droplet_id").(int)
// Gracefully power off the droplet. We have to retry this a number
// of times because sometimes it says it completed when it actually
// did absolutely nothing (*ALAKAZAM!* magic!). We give up after
// a pretty arbitrary amount of time.
ui.Say("Gracefully shutting down droplet...")
err := client.ShutdownDroplet(dropletId)
_, _, err := client.DropletActions.Shutdown(dropletId)
if err != nil {
// If we get an error the first time, actually report it
err := fmt.Errorf("Error shutting down droplet: %s", err)
......@@ -48,7 +49,7 @@ func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction {
for attempts := 2; attempts > 0; attempts++ {
log.Printf("ShutdownDroplet attempt #%d...", attempts)
err := client.ShutdownDroplet(dropletId)
_, _, err := client.DropletActions.Shutdown(dropletId)
if err != nil {
log.Printf("Shutdown retry error: %s", err)
}
......
......@@ -5,6 +5,7 @@ import (
"fmt"
"log"
"github.com/digitalocean/godo"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
......@@ -12,13 +13,13 @@ import (
type stepSnapshot struct{}
func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(DigitalOceanClient)
client := state.Get("client").(*godo.Client)
ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(Config)
dropletId := state.Get("droplet_id").(uint)
dropletId := state.Get("droplet_id").(int)
ui.Say(fmt.Sprintf("Creating snapshot: %v", c.SnapshotName))
err := client.CreateSnapshot(dropletId, c.SnapshotName)
_, _, err := client.DropletActions.Snapshot(dropletId, c.SnapshotName)
if err != nil {
err := fmt.Errorf("Error creating snapshot: %s", err)
state.Put("error", err)
......@@ -36,7 +37,7 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
}
log.Printf("Looking up snapshot ID for snapshot: %s", c.SnapshotName)
images, err := client.Images()
images, _, err := client.Images.ListUser(&godo.ListOptions{PerPage: 200})
if err != nil {
err := fmt.Errorf("Error looking up snapshot ID: %s", err)
state.Put("error", err)
......@@ -44,10 +45,10 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionHalt
}
var imageId uint
var imageId int
for _, image := range images {
if image.Name == c.SnapshotName {
imageId = image.Id
imageId = image.ID
break
}
}
......@@ -60,7 +61,6 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
}
log.Printf("Snapshot image ID: %d", imageId)
state.Put("snapshot_image_id", imageId)
state.Put("snapshot_name", c.SnapshotName)
state.Put("region", c.Region)
......
package digitalocean
import (
"golang.org/x/oauth2"
)
type apiTokenSource struct {
AccessToken string
}
func (t *apiTokenSource) Token() (*oauth2.Token, error) {
return &oauth2.Token{
AccessToken: t.AccessToken,
}, nil
}
......@@ -4,11 +4,15 @@ import (
"fmt"
"log"
"time"
"github.com/digitalocean/godo"
)
// waitForState simply blocks until the droplet is in
// a state we expect, while eventually timing out.
func waitForDropletState(desiredState string, dropletId uint, client DigitalOceanClient, timeout time.Duration) error {
func waitForDropletState(
desiredState string, dropletId int,
client *godo.Client, timeout time.Duration) error {
done := make(chan struct{})
defer close(done)
......@@ -19,13 +23,13 @@ func waitForDropletState(desiredState string, dropletId uint, client DigitalOcea
attempts += 1
log.Printf("Checking droplet status... (attempt: %d)", attempts)
_, status, err := client.DropletStatus(dropletId)
droplet, _, err := client.Droplets.Get(dropletId)
if err != nil {
result <- err
return
}
if status == desiredState {
if droplet.Status == desiredState {
result <- nil
return
}
......
......@@ -24,31 +24,13 @@ There are many configuration options available for the builder. They are
segmented below into two categories: required and optional parameters. Within
each category, the available configuration keys are alphabetized.
### Required v1 api:
### Required:
* `api_key` (string) - The API key to use to access your account. You can
retrieve this on the "API" page visible after logging into your account
on DigitalOcean.
If not specified, Packer will use the environment variable
`DIGITALOCEAN_API_KEY`, if set.
* `client_id` (string) - The client ID to use to access your account. You can
find this on the "API" page visible after logging into your account on
DigitalOcean.
If not specified, Packer will use the environment variable
`DIGITALOCEAN_CLIENT_ID`, if set.
### Required v2 api:
* `api_token` (string) - The client TOKEN to use to access your account. If it
specified, then use v2 api (current), if not then used old (v1) deprecated api.
Also it can be specified via environment variable `DIGITALOCEAN_API_TOKEN`, if set.
* `api_token` (string) - The client TOKEN to use to access your account.
It can also be specified via environment variable `DIGITALOCEAN_API_TOKEN`, if set.
### Optional:
* `api_url` (string) - API endpoint, by default use https://api.digitalocean.com
Also it can be specified via environment variable `DIGITALOCEAN_API_URL`, if set.
* `droplet_name` (string) - The name assigned to the droplet. DigitalOcean
sets the hostname of the machine to this value.
......@@ -57,10 +39,6 @@ each category, the available configuration keys are alphabetized.
defaults to 'ubuntu-12-04-x64' which is the slug for "Ubuntu 12.04.4 x64".
See https://developers.digitalocean.com/documentation/v2/#list-all-images for details on how to get a list of the the accepted image names/slugs.
* `image_id` (integer) - The ID of the base image to use. This is the image that
will be used to launch a new droplet and provision it.
This setting is deprecated. Use `image` instead.
* `private_networking` (boolean) - Set to `true` to enable private networking
for the droplet being created. This defaults to `false`, or not enabled.
......@@ -69,17 +47,10 @@ each category, the available configuration keys are alphabetized.
This defaults to "nyc3", which is the slug for "New York 3".
See https://developers.digitalocean.com/documentation/v2/#list-all-regions for the accepted region names/slugs.
* `region_id` (integer) - The ID of the region to launch the droplet in. Consequently,
this is the region where the snapshot will be available.
This setting is deprecated. Use `region` instead.
* `size` (string) - The name (or slug) of the droplet size to use.
This defaults to "512mb", which is the slug for "512MB".
See https://developers.digitalocean.com/documentation/v2/#list-all-sizes for the accepted size names/slugs.
* `size_id` (integer) - The ID of the droplet size to use.
This setting is deprecated. Use `size` instead.
* `snapshot_name` (string) - The name of the resulting snapshot that will
appear in your account. This must be unique.
To help make this unique, use a function like `timestamp` (see
......@@ -107,20 +78,6 @@ own access tokens:
```javascript
{
"type": "digitalocean",
"client_id": "YOUR CLIENT ID",
"api_key": "YOUR API KEY"
"api_token": "YOUR API KEY"
}
```
## Finding Image, Region, and Size IDs
Unfortunately, finding a list of available values for `image_id`, `region_id`,
and `size_id` is not easy at the moment. Basically, it has to be done through
the [DigitalOcean API](https://www.digitalocean.com/api_access) using the
`/images`, `/regions`, and `/sizes` endpoints. You can use `curl` for this
or request it in your browser.
If you're comfortable installing RubyGems, [Tugboat](https://github.com/pearkes/tugboat)
is a fantastic DigitalOcean command-line client that has commands to
find the available images, regions, and sizes. For example, to see all the
global images, you can run `tugboat images --global`.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment