Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
P
packer
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Kristopher Ruzic
packer
Commits
572926d9
Commit
572926d9
authored
Jun 10, 2015
by
Mitchell Hashimoto
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #2206 from mitchellh/b-do
builder/digitalocean: use official API client, v2 only
parents
9da9ce60
311c9eb5
Changes
17
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
281 additions
and
1306 deletions
+281
-1306
builder/digitalocean/api.go
builder/digitalocean/api.go
+0
-76
builder/digitalocean/api_v1.go
builder/digitalocean/api_v1.go
+0
-382
builder/digitalocean/api_v2.go
builder/digitalocean/api_v2.go
+0
-462
builder/digitalocean/artifact.go
builder/digitalocean/artifact.go
+6
-3
builder/digitalocean/builder.go
builder/digitalocean/builder.go
+11
-185
builder/digitalocean/builder_acc_test.go
builder/digitalocean/builder_acc_test.go
+30
-0
builder/digitalocean/builder_test.go
builder/digitalocean/builder_test.go
+3
-97
builder/digitalocean/config.go
builder/digitalocean/config.go
+137
-0
builder/digitalocean/step_create_droplet.go
builder/digitalocean/step_create_droplet.go
+22
-17
builder/digitalocean/step_create_ssh_key.go
builder/digitalocean/step_create_ssh_key.go
+13
-13
builder/digitalocean/step_droplet_info.go
builder/digitalocean/step_droplet_info.go
+15
-5
builder/digitalocean/step_power_off.go
builder/digitalocean/step_power_off.go
+6
-5
builder/digitalocean/step_shutdown.go
builder/digitalocean/step_shutdown.go
+5
-4
builder/digitalocean/step_snapshot.go
builder/digitalocean/step_snapshot.go
+7
-7
builder/digitalocean/token_source.go
builder/digitalocean/token_source.go
+15
-0
builder/digitalocean/wait.go
builder/digitalocean/wait.go
+7
-3
website/source/docs/builders/digitalocean.html.markdown
website/source/docs/builders/digitalocean.html.markdown
+4
-47
No files found.
builder/digitalocean/api.go
deleted
100644 → 0
View file @
9da9ce60
// 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
)
}
builder/digitalocean/api_v1.go
deleted
100644 → 0
View file @
9da9ce60
// 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
}
builder/digitalocean/api_v2.go
deleted
100644 → 0
View file @
9da9ce60
This diff is collapsed.
Click to expand it.
builder/digitalocean/artifact.go
View file @
572926d9
...
...
@@ -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
u
int
snapshotId
int
// The name of the region
regionName
string
// The client for making API calls
client
DigitalOcean
Client
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
}
builder/digitalocean/builder.go
View file @
572926d9
...
...
@@ -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"
"g
ithub.com/mitchellh/packer/template/interpolate
"
"g
olang.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"
)
.
(
u
int
),
regionName
:
found_region
.
Name
,
snapshotId
:
state
.
Get
(
"snapshot_image_id"
)
.
(
int
),
regionName
:
state
.
Get
(
"region"
)
.
(
string
)
,
client
:
client
,
}
...
...
builder/digitalocean/builder_acc_test.go
0 → 100644
View file @
572926d9
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"
}]
}
`
builder/digitalocean/builder_test.go
View file @
572926d9
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
)
...
...
builder/digitalocean/config.go
0 → 100644
View file @
572926d9
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
}
builder/digitalocean/step_create_droplet.go
View file @
572926d9
...
...
@@ -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
u
int
dropletId
int
}
func
(
s
*
stepCreateDroplet
)
Run
(
state
multistep
.
StateBag
)
multistep
.
StepAction
{
client
:=
state
.
Get
(
"client"
)
.
(
DigitalOcean
Client
)
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
=
droplet
Id
s
.
dropletId
=
droplet
.
ID
// Store the droplet id for later
state
.
Put
(
"droplet_id"
,
droplet
Id
)
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"
)
.
(
DigitalOcean
Client
)
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"
,
curlst
r
))
"Error destroying droplet. Please destroy it manually: %
s"
,
er
r
))
}
}
builder/digitalocean/step_create_ssh_key.go
View file @
572926d9
...
...
@@ -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
u
int
keyId
int
}
func
(
s
*
stepCreateSSHKey
)
Run
(
state
multistep
.
StateBag
)
multistep
.
StepAction
{
client
:=
state
.
Get
(
"client"
)
.
(
DigitalOcean
Client
)
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
=
key
Id
s
.
keyId
=
key
.
ID
log
.
Printf
(
"temporary ssh key name: %s"
,
name
)
// Remember some state for the future
state
.
Put
(
"ssh_key_id"
,
key
Id
)
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"
)
.
(
DigitalOcean
Client
)
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"
,
curlst
r
))
"Error cleaning up ssh key. Please delete the key manually: %
s"
,
er
r
))
}
}
builder/digitalocean/step_droplet_info.go
View file @
572926d9
...
...
@@ -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"
)
.
(
DigitalOcean
Client
)
client
:=
state
.
Get
(
"client"
)
.
(
*
godo
.
Client
)
ui
:=
state
.
Get
(
"ui"
)
.
(
packer
.
Ui
)
c
:=
state
.
Get
(
"config"
)
.
(
Config
)
dropletId
:=
state
.
Get
(
"droplet_id"
)
.
(
u
int
)
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
}
...
...
builder/digitalocean/step_power_off.go
View file @
572926d9
...
...
@@ -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"
)
.
(
DigitalOcean
Client
)
client
:=
state
.
Get
(
"client"
)
.
(
*
godo
.
Client
)
c
:=
state
.
Get
(
"config"
)
.
(
Config
)
ui
:=
state
.
Get
(
"ui"
)
.
(
packer
.
Ui
)
dropletId
:=
state
.
Get
(
"droplet_id"
)
.
(
u
int
)
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
s
tatus
==
"off"
{
if
droplet
.
S
tatus
==
"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
)
...
...
builder/digitalocean/step_shutdown.go
View file @
572926d9
...
...
@@ -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"
)
.
(
DigitalOcean
Client
)
client
:=
state
.
Get
(
"client"
)
.
(
*
godo
.
Client
)
ui
:=
state
.
Get
(
"ui"
)
.
(
packer
.
Ui
)
dropletId
:=
state
.
Get
(
"droplet_id"
)
.
(
u
int
)
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
)
}
...
...
builder/digitalocean/step_snapshot.go
View file @
572926d9
...
...
@@ -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"
)
.
(
DigitalOcean
Client
)
client
:=
state
.
Get
(
"client"
)
.
(
*
godo
.
Client
)
ui
:=
state
.
Get
(
"ui"
)
.
(
packer
.
Ui
)
c
:=
state
.
Get
(
"config"
)
.
(
Config
)
dropletId
:=
state
.
Get
(
"droplet_id"
)
.
(
u
int
)
dropletId
:=
state
.
Get
(
"droplet_id"
)
.
(
int
)
ui
.
Say
(
fmt
.
Sprintf
(
"Creating snapshot: %v"
,
c
.
SnapshotName
))
err
:=
client
.
Create
Snapshot
(
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
u
int
var
imageId
int
for
_
,
image
:=
range
images
{
if
image
.
Name
==
c
.
SnapshotName
{
imageId
=
image
.
I
d
imageId
=
image
.
I
D
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
)
...
...
builder/digitalocean/token_source.go
0 → 100644
View file @
572926d9
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
}
builder/digitalocean/wait.go
View file @
572926d9
...
...
@@ -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
s
tatus
==
desiredState
{
if
droplet
.
S
tatus
==
desiredState
{
result
<-
nil
return
}
...
...
website/source/docs/builders/digitalocean.html.markdown
View file @
572926d9
...
...
@@ -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`
.
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment