Conflicts:
	provisioner/salt-masterless/provisioner.go
This commit is contained in:
Nathan Hartwell 2014-11-05 10:58:25 -06:00
commit e5c6f1a753
275 changed files with 3957 additions and 3586 deletions

View File

@ -7,7 +7,7 @@ go:
install: make updatedeps
script:
- make test
- GOMAXPROCS=2 make test
#- go test -race ./...
matrix:

View File

@ -1,15 +1,57 @@
## 0.7.2 (unreleased)
## 0.8.0 (unreleased)
## 0.7.2 (October 28, 2014)
FEATURES:
* builder/digitalocean: API V2 support. [GH-1463]
* builder/parallels: Don't depend on _prl-utils_ [GH-1499]
IMPROVEMENTS:
* builder/amazon/all: Support new AWS Frankfurt region.
* builder/docker: Allow remote `DOCKER_HOST`, which works as long as
volumes work. [GH-1594]
* builder/qemu: Can set cache mode for main disk. [GH-1558]
* builder/qemu: Can build from pre-existing disk. [GH-1342]
* builder/vmware: Can specify path to Fusion installation with environmental
variable `FUSION_APP_PATH`. [GH-1552]
* builder/vmware: Can specify the HW version for the VMX. [GH-1530]
* builder/vmware/esxi: Will now cache ISOs/floppies remotely. [GH-1479]
* builder/vmware/vmx: Source VMX can have a disk connected via SATA. [GH-1604]
* post-processors/vagrant: Support Qemu (libvirt) boxes. [GH-1330]
* post-processors/vagrantcloud: Support self-hosted box URLs.
BUG FIXES:
* core: Fix loading plugins from pwd. [GH-1521]
* builder/amazon: Prefer token in config if given. [GH-1544]
* builder/amazon/all: Extended timeout for waiting for AMI. [GH-1533]
* builder/virtualbox: Can read VirtualBox version on FreeBSD. [GH-1570]
* builder/virtualbox: More robust reading of guest additions URL. [GH-1509]
* builder/vmware: Always remove floppies/drives. [GH-1504]
* builder/vmware: Wait some time so that post-VMX update aren't
overwritten. [GH-1504]
* builder/vmware/esxi: Retry power on if it fails. [GH-1334]
* builder/vmware-vmx: Fix issue with order of boot command support [GH-1492]
* builder/amazon: Extend timeout and allow user override [GH-1533]
* builder/parallels: Ignore 'The fdd0 device does not exist' [GH-1501]
* builder/parallels: Rely on Cleanup functions to detach devices [GH-1502]
* builder/parallels: Create VM without hdd and then add it later [GH-1548]
* builder/parallels: Disconnect cdrom0 [GH-1605]
* builder/qemu: Don't use `-redir` flag anymore, replace with
`hostfwd` options. [GH-1561]
* builder/qmeu: Use `pc` as default machine type instead of `pc-1.0`.
* providers/aws: Ignore transient network errors. [GH-1579]
* provisioner/ansible: Don't buffer output so output streams in. [GH-1585]
* provisioner/ansible: Use inventory file always to avoid potentially
deprecated feature. [GH-1562]
* provisioner/shell: Quote environmental variables. [GH-1568]
* provisioner/salt: Bootstrap over SSL. [GH-1608]
* post-processors/docker-push: Work with docker-tag artifacts. [GH-1526]
* post-processors/vsphere: Append "/" to object address. [GH-1615]
## 0.7.1 (September 10, 2014)

View File

@ -15,6 +15,6 @@ testrace:
go test -race $(TEST) $(TESTARGS)
updatedeps:
go get -u -v -p 2 ./...
go get -d -v -p 2 ./...
.PHONY: bin default test updatedeps

5
Vagrantfile vendored
View File

@ -1,9 +1,6 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
VAGRANTFILE_API_VERSION = "2"
$script = <<SCRIPT
SRCROOT="/opt/go"
@ -31,7 +28,7 @@ sudo chown -R vagrant:vagrant /opt/gopath
sudo apt-get install -y curl git-core zip
SCRIPT
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
Vagrant.configure(2) do |config|
config.vm.box = "chef/ubuntu-12.04"
config.vm.provision "shell", inline: $script

View File

@ -26,7 +26,7 @@ func (c *AccessConfig) Auth() (aws.Auth, error) {
c.SecretKey = auth.SecretKey
c.Token = auth.Token
}
if auth.Token == "" && c.Token != "" {
if c.Token != "" {
auth.Token = c.Token
}

View File

@ -52,6 +52,10 @@ func (a *Artifact) String() string {
return fmt.Sprintf("AMIs were created:\n\n%s", strings.Join(amiStrings, "\n"))
}
func (a *Artifact) State(name string) interface{} {
return nil
}
func (a *Artifact) Destroy() error {
errors := make([]error, 0)

View File

@ -6,6 +6,9 @@ import (
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"log"
"net"
"os"
"strconv"
"time"
)
@ -38,6 +41,9 @@ func AMIStateRefreshFunc(conn *ec2.EC2, imageId string) StateRefreshFunc {
if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidAMIID.NotFound" {
// Set this to nil as if we didn't find anything.
resp = nil
} else if isTransientNetworkError(err) {
// Transient network error, treat it as if we didn't find anything
resp = nil
} else {
log.Printf("Error on AMIStateRefresh: %s", err)
return nil, "", err
@ -64,6 +70,9 @@ func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) StateRefreshFunc {
if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidInstanceID.NotFound" {
// Set this to nil as if we didn't find anything.
resp = nil
} else if isTransientNetworkError(err) {
// Transient network error, treat it as if we didn't find anything
resp = nil
} else {
log.Printf("Error on InstanceStateRefresh: %s", err)
return nil, "", err
@ -90,6 +99,9 @@ func SpotRequestStateRefreshFunc(conn *ec2.EC2, spotRequestId string) StateRefre
if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidSpotInstanceRequestID.NotFound" {
// Set this to nil as if we didn't find anything.
resp = nil
} else if isTransientNetworkError(err) {
// Transient network error, treat it as if we didn't find anything
resp = nil
} else {
log.Printf("Error on SpotRequestStateRefresh: %s", err)
return nil, "", err
@ -112,6 +124,8 @@ func SpotRequestStateRefreshFunc(conn *ec2.EC2, spotRequestId string) StateRefre
func WaitForState(conf *StateChangeConf) (i interface{}, err error) {
log.Printf("Waiting for state to become: %s", conf.Target)
sleepSeconds := 2
maxTicks := int(TimeoutSeconds()/sleepSeconds) + 1
notfoundTick := 0
for {
@ -125,7 +139,7 @@ func WaitForState(conf *StateChangeConf) (i interface{}, err error) {
// If we didn't find the resource, check if we have been
// not finding it for awhile, and if so, report an error.
notfoundTick += 1
if notfoundTick > 20 {
if notfoundTick > maxTicks {
return nil, errors.New("couldn't find resource")
}
} else {
@ -156,8 +170,36 @@ func WaitForState(conf *StateChangeConf) (i interface{}, err error) {
}
}
time.Sleep(2 * time.Second)
time.Sleep(time.Duration(sleepSeconds) * time.Second)
}
return
}
func isTransientNetworkError(err error) bool {
if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
return true
}
return false
}
// Returns 300 seconds (5 minutes) by default
// Some AWS operations, like copying an AMI to a distant region, take a very long time
// Allow user to override with AWS_TIMEOUT_SECONDS environment variable
func TimeoutSeconds() (seconds int) {
seconds = 300
override := os.Getenv("AWS_TIMEOUT_SECONDS")
if override != "" {
n, err := strconv.Atoi(override)
if err != nil {
log.Printf("Invalid timeout seconds '%s', using default", override)
} else {
seconds = n
}
}
log.Printf("Allowing %ds to complete (change with AWS_TIMEOUT_SECONDS)", seconds)
return seconds
}

View File

@ -4,36 +4,13 @@
package digitalocean
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/mitchellh/mapstructure"
)
type Image struct {
Id uint
Name string
Slug string
Distribution string
}
type ImagesResp struct {
Images []Image
}
type Region struct {
Id uint
Name string
Slug string
Id uint `json:"id,omitempty"` //only in v1 api
Slug string `json:"slug"` //presen in both api
Name string `json:"name"` //presen in both api
Sizes []string `json:"sizes,omitempty"` //only in v2 api
Available bool `json:"available,omitempty"` //only in v2 api
Features []string `json:"features,omitempty"` //only in v2 api
}
type RegionsResp struct {
@ -41,378 +18,51 @@ type RegionsResp struct {
}
type Size struct {
Id uint
Name string
Slug string
Id uint `json:"id,omitempty"` //only in v1 api
Name string `json:"name,omitempty"` //only in v1 api
Slug string `json:"slug"` //presen in both api
Memory uint `json:"memory,omitempty"` //only in v2 api
VCPUS uint `json:"vcpus,omitempty"` //only in v2 api
Disk uint `json:"disk,omitempty"` //only in v2 api
Transfer float64 `json:"transfer,omitempty"` //only in v2 api
PriceMonthly float64 `json:"price_monthly,omitempty"` //only in v2 api
PriceHourly float64 `json:"price_hourly,omitempty"` //only in v2 api
Regions []string `json:"regions,omitempty"` //only in v2 api
}
type SizesResp struct {
Sizes []Size
}
type DigitalOceanClient struct {
// The http client for communicating
client *http.Client
// Credentials
ClientID string
APIKey string
// The base URL of the API
APIURL string
type Image struct {
Id uint `json:"id"` //presen in both api
Name string `json:"name"` //presen in both api
Slug string `json:"slug"` //presen in both api
Distribution string `json:"distribution"` //presen in both api
Public bool `json:"public,omitempty"` //only in v2 api
Regions []string `json:"regions,omitempty"` //only in v2 api
ActionIds []string `json:"action_ids,omitempty"` //only in v2 api
CreatedAt string `json:"created_at,omitempty"` //only in v2 api
}
// Creates a new client for communicating with DO
func (d DigitalOceanClient) New(client string, key string, url string) *DigitalOceanClient {
c := &DigitalOceanClient{
client: &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
},
},
APIURL: url,
ClientID: client,
APIKey: key,
}
return c
type ImagesResp struct {
Images []Image
}
// Creates an SSH Key and returns it's id
func (d DigitalOceanClient) CreateKey(name string, pub string) (uint, error) {
params := url.Values{}
params.Set("name", name)
params.Set("ssh_pub_key", pub)
body, err := NewRequest(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 DigitalOceanClient) DestroyKey(id uint) error {
path := fmt.Sprintf("ssh_keys/%v/destroy", id)
_, err := NewRequest(d, path, url.Values{})
return err
}
// Creates a droplet and returns it's id
func (d DigitalOceanClient) 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 := NewRequest(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 DigitalOceanClient) DestroyDroplet(id uint) error {
path := fmt.Sprintf("droplets/%v/destroy", id)
_, err := NewRequest(d, path, url.Values{})
return err
}
// Powers off a droplet
func (d DigitalOceanClient) PowerOffDroplet(id uint) error {
path := fmt.Sprintf("droplets/%v/power_off", id)
_, err := NewRequest(d, path, url.Values{})
return err
}
// Shutsdown a droplet. This is a "soft" shutdown.
func (d DigitalOceanClient) ShutdownDroplet(id uint) error {
path := fmt.Sprintf("droplets/%v/shutdown", id)
_, err := NewRequest(d, path, url.Values{})
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/%v/snapshot", id)
params := url.Values{}
params.Set("name", name)
_, err := NewRequest(d, path, params)
return err
}
// Returns all available images.
func (d DigitalOceanClient) Images() ([]Image, error) {
resp, err := NewRequest(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 DigitalOceanClient) DestroyImage(id uint) error {
path := fmt.Sprintf("images/%d/destroy", id)
_, err := NewRequest(d, path, url.Values{})
return err
}
// Returns DO's string representation of status "off" "new" "active" etc.
func (d DigitalOceanClient) DropletStatus(id uint) (string, string, error) {
path := fmt.Sprintf("droplets/%v", id)
body, err := NewRequest(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 NewRequest(d DigitalOceanClient, 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 DigitalOceanClient) 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 DigitalOceanClient) Regions() ([]Region, error) {
resp, err := NewRequest(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 DigitalOceanClient) 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 DigitalOceanClient) Sizes() ([]Size, error) {
resp, err := NewRequest(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 DigitalOceanClient) 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
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)
}

View File

@ -0,0 +1,382 @@
// 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
}

View File

@ -0,0 +1,448 @@
// 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 (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"strconv"
"strings"
)
type DigitalOceanClientV2 struct {
// The http client for communicating
client *http.Client
// Credentials
APIToken string
// The base URL of the API
APIURL string
}
// Creates a new client for communicating with DO
func DigitalOceanClientNewV2(token string, url string) *DigitalOceanClientV2 {
c := &DigitalOceanClientV2{
client: &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
},
},
APIURL: url,
APIToken: token,
}
return c
}
// Creates an SSH Key and returns it's id
func (d DigitalOceanClientV2) CreateKey(name string, pub string) (uint, error) {
type KeyReq struct {
Name string `json:"name"`
PublicKey string `json:"public_key"`
}
type KeyRes struct {
SSHKey struct {
Id uint
Name string
Fingerprint string
PublicKey string `json:"public_key"`
} `json:"ssh_key"`
}
req := &KeyReq{Name: name, PublicKey: pub}
res := KeyRes{}
err := NewRequestV2(d, "v2/account/keys", "POST", req, &res)
if err != nil {
return 0, err
}
return res.SSHKey.Id, err
}
// Destroys an SSH key
func (d DigitalOceanClientV2) DestroyKey(id uint) error {
path := fmt.Sprintf("v2/account/keys/%v", id)
return NewRequestV2(d, path, "DELETE", nil, nil)
}
// Creates a droplet and returns it's id
func (d DigitalOceanClientV2) CreateDroplet(name string, size string, image string, region string, keyId uint, privateNetworking bool) (uint, error) {
type DropletReq struct {
Name string `json:"name"`
Region string `json:"region"`
Size string `json:"size"`
Image string `json:"image"`
SSHKeys []string `json:"ssh_keys,omitempty"`
Backups bool `json:"backups,omitempty"`
IPv6 bool `json:"ipv6,omitempty"`
PrivateNetworking bool `json:"private_networking,omitempty"`
}
type DropletRes struct {
Droplet struct {
Id uint
Name string
Memory uint
VCPUS uint `json:"vcpus"`
Disk uint
Region Region
Image Image
Size Size
Locked bool
CreateAt string `json:"created_at"`
Status string
Networks struct {
V4 []struct {
IPAddr string `json:"ip_address"`
Netmask string
Gateway string
Type string
} `json:"v4,omitempty"`
V6 []struct {
IPAddr string `json:"ip_address"`
CIDR uint `json:"cidr"`
Gateway string
Type string
} `json:"v6,omitempty"`
}
Kernel struct {
Id uint
Name string
Version string
}
BackupIds []uint
SnapshotIds []uint
ActionIds []uint
Features []string `json:"features,omitempty"`
}
}
req := &DropletReq{Name: name}
res := DropletRes{}
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)
}
req.Size = found_size.Slug
req.Image = found_image.Slug
req.Region = found_region.Slug
req.SSHKeys = []string{fmt.Sprintf("%v", keyId)}
req.PrivateNetworking = privateNetworking
err = NewRequestV2(d, "v2/droplets", "POST", req, &res)
if err != nil {
return 0, err
}
return res.Droplet.Id, err
}
// Destroys a droplet
func (d DigitalOceanClientV2) DestroyDroplet(id uint) error {
path := fmt.Sprintf("v2/droplets/%v", id)
return NewRequestV2(d, path, "DELETE", nil, nil)
}
// Powers off a droplet
func (d DigitalOceanClientV2) PowerOffDroplet(id uint) error {
type ActionReq struct {
Type string `json:"type"`
}
type ActionRes struct {
}
req := &ActionReq{Type: "power_off"}
path := fmt.Sprintf("v2/droplets/%v/actions", id)
return NewRequestV2(d, path, "POST", req, nil)
}
// Shutsdown a droplet. This is a "soft" shutdown.
func (d DigitalOceanClientV2) ShutdownDroplet(id uint) error {
type ActionReq struct {
Type string `json:"type"`
}
type ActionRes struct {
}
req := &ActionReq{Type: "shutdown"}
path := fmt.Sprintf("v2/droplets/%v/actions", id)
return NewRequestV2(d, path, "POST", req, nil)
}
// Creates a snaphot of a droplet by it's ID
func (d DigitalOceanClientV2) CreateSnapshot(id uint, name string) error {
type ActionReq struct {
Type string `json:"type"`
Name string `json:"name"`
}
type ActionRes struct {
}
req := &ActionReq{Type: "snapshot", Name: name}
path := fmt.Sprintf("v2/droplets/%v/actions", id)
return NewRequestV2(d, path, "POST", req, nil)
}
// Returns all available images.
func (d DigitalOceanClientV2) Images() ([]Image, error) {
res := ImagesResp{}
err := NewRequestV2(d, "v2/images?per_page=200", "GET", nil, &res)
if err != nil {
return nil, err
}
return res.Images, nil
}
// Destroys an image by its ID.
func (d DigitalOceanClientV2) DestroyImage(id uint) error {
path := fmt.Sprintf("v2/images/%d", id)
return NewRequestV2(d, path, "DELETE", nil, nil)
}
// Returns DO's string representation of status "off" "new" "active" etc.
func (d DigitalOceanClientV2) DropletStatus(id uint) (string, string, error) {
path := fmt.Sprintf("v2/droplets/%v", id)
type DropletRes struct {
Droplet struct {
Id uint
Name string
Memory uint
VCPUS uint `json:"vcpus"`
Disk uint
Region Region
Image Image
Size Size
Locked bool
CreateAt string `json:"created_at"`
Status string
Networks struct {
V4 []struct {
IPAddr string `json:"ip_address"`
Netmask string
Gateway string
Type string
} `json:"v4,omitempty"`
V6 []struct {
IPAddr string `json:"ip_address"`
CIDR uint `json:"cidr"`
Gateway string
Type string
} `json:"v6,omitempty"`
}
Kernel struct {
Id uint
Name string
Version string
}
BackupIds []uint
SnapshotIds []uint
ActionIds []uint
Features []string `json:"features,omitempty"`
}
}
res := DropletRes{}
err := NewRequestV2(d, path, "GET", nil, &res)
if err != nil {
return "", "", err
}
var ip string
if len(res.Droplet.Networks.V4) > 0 {
ip = res.Droplet.Networks.V4[0].IPAddr
}
return ip, res.Droplet.Status, err
}
// Sends an api request and returns a generic map[string]interface of
// the response.
func NewRequestV2(d DigitalOceanClientV2, path string, method string, req interface{}, res interface{}) error {
var err error
var request *http.Request
client := d.client
buf := new(bytes.Buffer)
// Add the authentication parameters
url := fmt.Sprintf("%s/%s", d.APIURL, path)
if req != nil {
enc := json.NewEncoder(buf)
enc.Encode(req)
defer buf.Reset()
request, err = http.NewRequest(method, url, buf)
} else {
request, err = http.NewRequest(method, url, nil)
}
if err != nil {
return err
}
// Add the authentication parameters
request.Header.Add("Authorization", "Bearer "+d.APIToken)
log.Printf("sending new request to digitalocean: %s", url)
resp, err := client.Do(request)
if err != nil {
return err
}
if method == "DELETE" && resp.StatusCode == 204 {
if resp.Body != nil {
resp.Body.Close()
}
return nil
}
if resp.Body == nil {
return errors.New("Request returned empty body")
}
body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return err
}
log.Printf("response from digitalocean: %s", body)
err = json.Unmarshal(body, &res)
if err != nil {
return errors.New(fmt.Sprintf("Failed to decode JSON response %s (HTTP %v) from DigitalOcean: %s", err.Error(),
resp.StatusCode, body))
}
return nil
}
func (d DigitalOceanClientV2) 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 DigitalOceanClientV2) Regions() ([]Region, error) {
res := RegionsResp{}
err := NewRequestV2(d, "v2/regions?per_page=200", "GET", nil, &res)
if err != nil {
return nil, err
}
return res.Regions, nil
}
func (d DigitalOceanClientV2) 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 DigitalOceanClientV2) Sizes() ([]Size, error) {
res := SizesResp{}
err := NewRequestV2(d, "v2/sizes?per_page=200", "GET", nil, &res)
if err != nil {
return nil, err
}
return res.Sizes, nil
}
func (d DigitalOceanClientV2) 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
}

View File

@ -16,7 +16,7 @@ type Artifact struct {
regionName string
// The client for making API calls
client *DigitalOceanClient
client DigitalOceanClient
}
func (*Artifact) BuilderId() string {
@ -37,6 +37,10 @@ func (a *Artifact) String() string {
return fmt.Sprintf("A snapshot was created: '%v' in region '%v'", a.snapshotName, a.regionName)
}
func (a *Artifact) State(name string) interface{} {
return nil
}
func (a *Artifact) Destroy() error {
log.Printf("Destroying image: %d (%s)", a.snapshotId, a.snapshotName)
return a.client.DestroyImage(a.snapshotId)

View File

@ -40,6 +40,7 @@ type config struct {
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"`
@ -101,6 +102,11 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
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)
@ -164,6 +170,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
"client_id": &b.config.ClientID,
"api_key": &b.config.APIKey,
"api_url": &b.config.APIURL,
"api_token": &b.config.APIToken,
"snapshot_name": &b.config.SnapshotName,
"droplet_name": &b.config.DropletName,
"ssh_username": &b.config.SSHUsername,
@ -180,21 +187,23 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
}
}
// Required configurations that will display errors if not set
if b.config.ClientID == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("a client_id must be specified"))
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"
}
if b.config.APIKey == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("an api_key must be specified"))
}
sshTimeout, err := time.ParseDuration(b.config.RawSSHTimeout)
if err != nil {
errs = packer.MultiErrorAppend(
@ -218,8 +227,13 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
}
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
var client DigitalOceanClient
// Initialize the DO API client
client := DigitalOceanClient{}.New(b.config.ClientID, b.config.APIKey, b.config.APIURL)
if b.config.APIToken == "" {
client = DigitalOceanClientNewV1(b.config.ClientID, b.config.APIKey, b.config.APIURL)
} else {
client = DigitalOceanClientNewV2(b.config.APIToken, b.config.APIURL)
}
// Set up the state
state := new(multistep.BasicStateBag)

View File

@ -12,7 +12,7 @@ type stepCreateDroplet struct {
}
func (s *stepCreateDroplet) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*DigitalOceanClient)
client := state.Get("client").(DigitalOceanClient)
ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(config)
sshKeyId := state.Get("ssh_key_id").(uint)
@ -44,7 +44,7 @@ func (s *stepCreateDroplet) Cleanup(state multistep.StateBag) {
return
}
client := state.Get("client").(*DigitalOceanClient)
client := state.Get("client").(DigitalOceanClient)
ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(config)

View File

@ -19,7 +19,7 @@ type stepCreateSSHKey struct {
}
func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*DigitalOceanClient)
client := state.Get("client").(DigitalOceanClient)
ui := state.Get("ui").(packer.Ui)
ui.Say("Creating temporary ssh key for droplet...")
@ -71,15 +71,14 @@ func (s *stepCreateSSHKey) Cleanup(state multistep.StateBag) {
return
}
client := state.Get("client").(*DigitalOceanClient)
client := state.Get("client").(DigitalOceanClient)
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 '%v/ssh_keys/%v/destroy?client_id=%v&api_key=%v'",
c.APIURL, s.keyId, c.ClientID, c.APIKey)
curlstr := fmt.Sprintf("curl -H 'Authorization: Bearer #TOKEN#' -X DELETE '%v/v2/account/keys/%v'", c.APIURL, s.keyId)
if err != nil {
log.Printf("Error cleaning up ssh key: %v", err.Error())

View File

@ -2,6 +2,7 @@ package digitalocean
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
@ -9,7 +10,7 @@ import (
type stepDropletInfo struct{}
func (s *stepDropletInfo) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*DigitalOceanClient)
client := state.Get("client").(DigitalOceanClient)
ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(config)
dropletId := state.Get("droplet_id").(uint)

View File

@ -2,15 +2,16 @@ package digitalocean
import (
"fmt"
"log"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
)
type stepPowerOff struct{}
func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*DigitalOceanClient)
client := state.Get("client").(DigitalOceanClient)
c := state.Get("config").(config)
ui := state.Get("ui").(packer.Ui)
dropletId := state.Get("droplet_id").(uint)

View File

@ -2,16 +2,17 @@ package digitalocean
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"time"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
type stepShutdown struct{}
func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*DigitalOceanClient)
client := state.Get("client").(DigitalOceanClient)
ui := state.Get("ui").(packer.Ui)
dropletId := state.Get("droplet_id").(uint)

View File

@ -3,15 +3,16 @@ package digitalocean
import (
"errors"
"fmt"
"log"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
)
type stepSnapshot struct{}
func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*DigitalOceanClient)
client := state.Get("client").(DigitalOceanClient)
ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(config)
dropletId := state.Get("droplet_id").(uint)

View File

@ -8,7 +8,7 @@ import (
// 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 uint, client DigitalOceanClient, timeout time.Duration) error {
done := make(chan struct{})
defer close(done)

View File

@ -27,6 +27,10 @@ func (a *ExportArtifact) String() string {
return fmt.Sprintf("Exported Docker file: %s", a.path)
}
func (a *ExportArtifact) State(name string) interface{} {
return nil
}
func (a *ExportArtifact) Destroy() error {
return os.Remove(a.path)
}

View File

@ -28,6 +28,10 @@ func (a *ImportArtifact) String() string {
return fmt.Sprintf("Imported Docker image: %s", a.Id())
}
func (*ImportArtifact) State(name string) interface{} {
return nil
}
func (a *ImportArtifact) Destroy() error {
return a.Driver.DeleteImage(a.Id())
}

View File

@ -258,10 +258,5 @@ func (d *DockerDriver) Verify() error {
return err
}
if v := os.Getenv("DOCKER_HOST"); v != "" {
return fmt.Errorf(
"DOCKER_HOST cannot be set with the Packer Docker builder.")
}
return nil
}

View File

@ -37,3 +37,7 @@ func (a *Artifact) Id() string {
func (a *Artifact) String() string {
return fmt.Sprintf("A disk image was created: %v", a.imageName)
}
func (a *Artifact) State(name string) interface{} {
return nil
}

View File

@ -24,6 +24,25 @@ func (config *Config) getImage() Image {
return Image{Name: config.SourceImage, ProjectId: project}
}
func (config *Config) getInstanceMetadata(sshPublicKey string) map[string]string {
instanceMetadata := make(map[string]string)
// Copy metadata from config
for k, v := range config.Metadata {
instanceMetadata[k] = v
}
// Merge any existing ssh keys with our public key
sshMetaKey := "sshKeys"
sshKeys := fmt.Sprintf("%s:%s", config.SSHUsername, sshPublicKey)
if confSshKeys, exists := instanceMetadata[sshMetaKey]; exists {
sshKeys = fmt.Sprintf("%s\n%s", sshKeys, confSshKeys)
}
instanceMetadata[sshMetaKey] = sshKeys
return instanceMetadata
}
// Run executes the Packer build step that creates a GCE instance.
func (s *StepCreateInstance) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
@ -39,13 +58,11 @@ func (s *StepCreateInstance) Run(state multistep.StateBag) multistep.StepAction
DiskSizeGb: config.DiskSizeGb,
Image: config.getImage(),
MachineType: config.MachineType,
Metadata: map[string]string{
"sshKeys": fmt.Sprintf("%s:%s", config.SSHUsername, sshPublicKey),
},
Name: name,
Network: config.Network,
Tags: config.Tags,
Zone: config.Zone,
Metadata: config.getInstanceMetadata(sshPublicKey),
Name: name,
Network: config.Network,
Tags: config.Tags,
Zone: config.Zone,
})
if err == nil {

View File

@ -24,6 +24,10 @@ func (a *NullArtifact) String() string {
return fmt.Sprintf("Did not export anything. This is the null builder")
}
func (a *NullArtifact) State(name string) interface{} {
return nil
}
func (a *NullArtifact) Destroy() error {
return nil
}

View File

@ -5,11 +5,12 @@ import (
"fmt"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud"
"net/http"
"net/url"
"os"
"strings"
"github.com/mitchellh/gophercloud-fork-40444fb"
)
// AccessConfig is for common configuration related to openstack access

View File

@ -2,8 +2,9 @@ package openstack
import (
"fmt"
"github.com/rackspace/gophercloud"
"log"
"github.com/mitchellh/gophercloud-fork-40444fb"
)
// Artifact is an artifact implementation that contains built images.
@ -35,6 +36,10 @@ func (a *Artifact) String() string {
return fmt.Sprintf("An image was created: %v", a.ImageId)
}
func (a *Artifact) State(name string) interface{} {
return nil
}
func (a *Artifact) Destroy() error {
log.Printf("Destroying image: %d", a.ImageId)
return a.Conn.DeleteImageById(a.ImageId)

View File

@ -8,8 +8,9 @@ import (
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud"
"log"
"github.com/mitchellh/gophercloud-fork-40444fb"
)
// The unique ID for this builder

View File

@ -5,9 +5,10 @@ import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/racker/perigee"
"github.com/rackspace/gophercloud"
"log"
"time"
"github.com/mitchellh/gophercloud-fork-40444fb"
)
// StateRefreshFunc is a function type used for StateChangeConf that is

View File

@ -5,8 +5,9 @@ import (
"errors"
"fmt"
"github.com/mitchellh/multistep"
"github.com/rackspace/gophercloud"
"time"
"github.com/mitchellh/gophercloud-fork-40444fb"
)
// SSHAddress returns a function that can be given to the SSH communicator

View File

@ -4,7 +4,8 @@ import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud"
"github.com/mitchellh/gophercloud-fork-40444fb"
)
type StepAllocateIp struct {

View File

@ -4,9 +4,10 @@ import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud"
"log"
"time"
"github.com/mitchellh/gophercloud-fork-40444fb"
)
type stepCreateImage struct{}

View File

@ -5,10 +5,11 @@ import (
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud"
"log"
"os"
"runtime"
"github.com/mitchellh/gophercloud-fork-40444fb"
)
type StepKeyPair struct {

View File

@ -4,8 +4,9 @@ import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud"
"log"
"github.com/mitchellh/gophercloud-fork-40444fb"
)
type StepRunSourceServer struct {

View File

@ -66,6 +66,10 @@ func (a *artifact) String() string {
return fmt.Sprintf("VM files in directory: %s", a.dir)
}
func (a *artifact) State(name string) interface{} {
return nil
}
func (a *artifact) Destroy() error {
return os.RemoveAll(a.dir)
}

View File

@ -81,7 +81,7 @@ func (s *stepAttachISO) Cleanup(state multistep.StateBag) {
log.Println("Enabling default CD/DVD drive...")
command := []string{
"set", vmName,
"--device-set", "cdrom0", "--enable",
"--device-set", "cdrom0", "--enable", "--disconnect",
}
if err := driver.Prlctl(command...); err != nil {

View File

@ -8,8 +8,9 @@ import (
// Artifact is the result of running the Qemu builder, namely a set
// of files associated with the resulting machine.
type Artifact struct {
dir string
f []string
dir string
f []string
state map[string]interface{}
}
func (*Artifact) BuilderId() string {
@ -28,6 +29,10 @@ func (a *Artifact) String() string {
return fmt.Sprintf("VM files in directory: %s", a.dir)
}
func (a *Artifact) State(name string) interface{} {
return a.state[name]
}
func (a *Artifact) Destroy() error {
return os.RemoveAll(a.dir)
}

View File

@ -56,6 +56,14 @@ var diskInterface = map[string]bool{
"virtio": true,
}
var diskCache = map[string]bool{
"writethrough": true,
"writeback": true,
"none": true,
"unsafe": true,
"directsync": true,
}
type Builder struct {
config config
runner multistep.Runner
@ -68,9 +76,11 @@ type config struct {
BootCommand []string `mapstructure:"boot_command"`
DiskInterface string `mapstructure:"disk_interface"`
DiskSize uint `mapstructure:"disk_size"`
DiskCache string `mapstructure:"disk_cache"`
FloppyFiles []string `mapstructure:"floppy_files"`
Format string `mapstructure:"format"`
Headless bool `mapstructure:"headless"`
DiskImage bool `mapstructure:"disk_image"`
HTTPDir string `mapstructure:"http_directory"`
HTTPPortMin uint `mapstructure:"http_port_min"`
HTTPPortMax uint `mapstructure:"http_port_max"`
@ -126,6 +136,10 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
b.config.DiskSize = 40000
}
if b.config.DiskCache == "" {
b.config.DiskCache = "writeback"
}
if b.config.Accelerator == "" {
b.config.Accelerator = "kvm"
}
@ -139,7 +153,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
}
if b.config.MachineType == "" {
b.config.MachineType = "pc-1.0"
b.config.MachineType = "pc"
}
if b.config.OutputDir == "" {
@ -280,6 +294,11 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
errs, errors.New("unrecognized disk interface type"))
}
if _, ok := diskCache[b.config.DiskCache]; !ok {
errs = packer.MultiErrorAppend(
errs, errors.New("unrecognized disk cache type"))
}
if b.config.HTTPPortMin > b.config.HTTPPortMax {
errs = packer.MultiErrorAppend(
errs, errors.New("http_port_min must be less than http_port_max"))
@ -412,6 +431,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
Files: b.config.FloppyFiles,
},
new(stepCreateDisk),
new(stepCopyDisk),
new(stepResizeDisk),
new(stepHTTPServer),
new(stepForwardSSH),
new(stepConfigureVNC),
@ -479,10 +500,16 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
}
artifact := &Artifact{
dir: b.config.OutputDir,
f: files,
dir: b.config.OutputDir,
f: files,
state: make(map[string]interface{}),
}
artifact.state["diskName"] = state.Get("disk_filename").(string)
artifact.state["diskType"] = b.config.Format
artifact.state["diskSize"] = uint64(b.config.DiskSize)
artifact.state["domainType"] = b.config.Accelerator
return artifact, nil
}

View File

@ -0,0 +1,45 @@
package qemu
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"path/filepath"
"strings"
)
// This step copies the virtual disk that will be used as the
// hard drive for the virtual machine.
type stepCopyDisk struct{}
func (s *stepCopyDisk) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*config)
driver := state.Get("driver").(Driver)
isoPath := state.Get("iso_path").(string)
ui := state.Get("ui").(packer.Ui)
path := filepath.Join(config.OutputDir, fmt.Sprintf("%s.%s", config.VMName,
strings.ToLower(config.Format)))
command := []string{
"convert",
"-f", config.Format,
isoPath,
path,
}
if config.DiskImage == false {
return multistep.ActionContinue
}
ui.Say("Copying hard drive...")
if err := driver.QemuImg(command...); err != nil {
err := fmt.Errorf("Error creating hard drive: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *stepCopyDisk) Cleanup(state multistep.StateBag) {}

View File

@ -16,8 +16,8 @@ func (s *stepCreateDisk) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*config)
driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packer.Ui)
path := filepath.Join(config.OutputDir, fmt.Sprintf("%s.%s", config.VMName,
strings.ToLower(config.Format)))
name := config.VMName + "." + strings.ToLower(config.Format)
path := filepath.Join(config.OutputDir, name)
command := []string{
"create",
@ -26,6 +26,10 @@ func (s *stepCreateDisk) Run(state multistep.StateBag) multistep.StepAction {
fmt.Sprintf("%vM", config.DiskSize),
}
if config.DiskImage == true {
return multistep.ActionContinue
}
ui.Say("Creating hard drive...")
if err := driver.QemuImg(command...); err != nil {
err := fmt.Errorf("Error creating hard drive: %s", err)
@ -34,6 +38,8 @@ func (s *stepCreateDisk) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionHalt
}
state.Put("disk_filename", name)
return multistep.ActionContinue
}

View File

@ -2,11 +2,12 @@ package qemu
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"math/rand"
"net"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
// This step adds a NAT port forwarding definition so that SSH is available
@ -23,9 +24,16 @@ func (s *stepForwardSSH) Run(state multistep.StateBag) multistep.StepAction {
log.Printf("Looking for available SSH port between %d and %d", config.SSHHostPortMin, config.SSHHostPortMax)
var sshHostPort uint
var offset uint = 0
portRange := int(config.SSHHostPortMax - config.SSHHostPortMin)
if portRange > 0 {
// Have to check if > 0 to avoid a panic
offset = uint(rand.Intn(portRange))
}
for {
sshHostPort = uint(rand.Intn(portRange)) + config.SSHHostPortMin
sshHostPort = offset + config.SSHHostPortMin
log.Printf("Trying port: %d", sshHostPort)
l, err := net.Listen("tcp", fmt.Sprintf(":%d", sshHostPort))
if err == nil {

View File

@ -0,0 +1,43 @@
package qemu
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"path/filepath"
"strings"
)
// This step resizes the virtual disk that will be used as the
// hard drive for the virtual machine.
type stepResizeDisk struct{}
func (s *stepResizeDisk) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*config)
driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packer.Ui)
path := filepath.Join(config.OutputDir, fmt.Sprintf("%s.%s", config.VMName,
strings.ToLower(config.Format)))
command := []string{
"resize",
path,
fmt.Sprintf("%vM", config.DiskSize),
}
if config.DiskImage == false {
return multistep.ActionContinue
}
ui.Say("Resizing hard drive...")
if err := driver.QemuImg(command...); err != nil {
err := fmt.Errorf("Error creating hard drive: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *stepResizeDisk) Cleanup(state multistep.StateBag) {}

View File

@ -2,11 +2,12 @@ package qemu
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"path/filepath"
"strings"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
// stepRun runs the virtual machine
@ -78,13 +79,12 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error
defaultArgs["-name"] = vmName
defaultArgs["-machine"] = fmt.Sprintf("type=%s", config.MachineType)
defaultArgs["-netdev"] = "user,id=user.0"
defaultArgs["-netdev"] = fmt.Sprintf("user,id=user.0,hostfwd=tcp::%v-:22", sshHostPort)
defaultArgs["-device"] = fmt.Sprintf("%s,netdev=user.0", config.NetDevice)
defaultArgs["-drive"] = fmt.Sprintf("file=%s,if=%s", imgPath, config.DiskInterface)
defaultArgs["-drive"] = fmt.Sprintf("file=%s,if=%s,cache=%s", imgPath, config.DiskInterface, config.DiskCache)
defaultArgs["-cdrom"] = isoPath
defaultArgs["-boot"] = bootDrive
defaultArgs["-m"] = "512M"
defaultArgs["-redir"] = fmt.Sprintf("tcp:%v::22", sshHostPort)
defaultArgs["-vnc"] = vnc
// Append the accelerator to the machine type if it is specified

View File

@ -56,6 +56,10 @@ func (a *artifact) String() string {
return fmt.Sprintf("VM files in directory: %s", a.dir)
}
func (a *artifact) State(name string) interface{} {
return nil
}
func (a *artifact) Destroy() error {
return os.RemoveAll(a.dir)
}

View File

@ -49,11 +49,12 @@ func (d *VBox42Driver) Iso() (string, error) {
return "", err
}
DefaultGuestAdditionsRe := regexp.MustCompile("Default Guest Additions ISO:(.*)")
DefaultGuestAdditionsRe := regexp.MustCompile("Default Guest Additions ISO:(.+)")
for _, line := range strings.Split(stdout.String(), "\n") {
// Need to trim off CR character when running in windows
line = strings.TrimRight(line, "\r")
// Trimming whitespaces at this point helps to filter out empty value
line = strings.TrimRight(line, " \r")
matches := DefaultGuestAdditionsRe.FindStringSubmatch(line)
if matches == nil {
@ -66,7 +67,7 @@ func (d *VBox42Driver) Iso() (string, error) {
return isoname, nil
}
return "", fmt.Errorf("Cannot find \"Default Guest Additions ISO\" in vboxmanage output")
return "", fmt.Errorf("Cannot find \"Default Guest Additions ISO\" in vboxmanage output (or it is empty)")
}
func (d *VBox42Driver) Import(name string, path string, flags []string) error {
@ -195,12 +196,12 @@ func (d *VBox42Driver) Version() (string, error) {
return "", fmt.Errorf("VirtualBox is not properly setup: %s", versionOutput)
}
versionRe := regexp.MustCompile("^[.0-9]+(?:_RC[0-9]+)?")
matches := versionRe.FindAllString(versionOutput, 1)
if matches == nil {
versionRe := regexp.MustCompile("^([.0-9]+)(?:_(?:RC|OSEr)[0-9]+)?")
matches := versionRe.FindAllStringSubmatch(versionOutput, 1)
if matches == nil || len(matches[0]) != 2 {
return "", fmt.Errorf("No version found: %s", versionOutput)
}
log.Printf("VirtualBox version: %s", matches[0])
return matches[0], nil
log.Printf("VirtualBox version: %s", matches[0][1])
return matches[0][1], nil
}

View File

@ -56,6 +56,10 @@ func (a *localArtifact) String() string {
return fmt.Sprintf("VM files in directory: %s", a.dir)
}
func (a *localArtifact) State(name string) interface{} {
return nil
}
func (a *localArtifact) Destroy() error {
return os.RemoveAll(a.dir)
}

View File

@ -2,6 +2,7 @@ package common
import (
"fmt"
"os"
"github.com/mitchellh/packer/packer"
)
@ -11,6 +12,9 @@ type DriverConfig struct {
}
func (c *DriverConfig) Prepare(t *packer.ConfigTemplate) []error {
if c.FusionAppPath == "" {
c.FusionAppPath = os.Getenv("FUSION_APP_PATH")
}
if c.FusionAppPath == "" {
c.FusionAppPath = "/Applications/VMware Fusion.app"
}

View File

@ -26,7 +26,7 @@ func (d *Fusion6Driver) Clone(dst, src string) error {
if _, _, err := runAndLog(cmd); err != nil {
if strings.Contains(err.Error(), "parameters was invalid") {
return fmt.Errorf(
"Clone is not supported with your version of Fusion. Packer " +
"Clone is not supported with your version of Fusion. Packer "+
"only works with Fusion %s Professional or above. Please verify your version.", VMWARE_FUSION_VERSION)
}

View File

@ -32,38 +32,27 @@ func (s StepCleanVMX) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionHalt
}
if _, ok := state.GetOk("floppy_path"); ok {
// Delete the floppy0 entries so the floppy is no longer mounted
ui.Message("Unmounting floppy from VMX...")
for k, _ := range vmxData {
if strings.HasPrefix(k, "floppy0.") {
log.Printf("Deleting key: %s", k)
delete(vmxData, k)
}
// Delete the floppy0 entries so the floppy is no longer mounted
ui.Message("Unmounting floppy from VMX...")
for k, _ := range vmxData {
if strings.HasPrefix(k, "floppy0.") {
log.Printf("Deleting key: %s", k)
delete(vmxData, k)
}
vmxData["floppy0.present"] = "FALSE"
}
vmxData["floppy0.present"] = "FALSE"
if isoPathRaw, ok := state.GetOk("iso_path"); ok {
isoPath := isoPathRaw.(string)
devRe := regexp.MustCompile(`^ide\d:\d\.`)
for k, v := range vmxData {
ide := devRe.FindString(k)
if ide == "" || v != "cdrom-image" {
continue
}
ui.Message("Detaching ISO from CD-ROM device...")
devRe := regexp.MustCompile(`^ide\d:\d\.`)
for k, _ := range vmxData {
match := devRe.FindString(k)
if match == "" {
continue
}
filenameKey := match + "filename"
if filename, ok := vmxData[filenameKey]; ok {
if filename == isoPath {
// Change the CD-ROM device back to auto-detect to eject
vmxData[filenameKey] = "auto detect"
vmxData[match+"devicetype"] = "cdrom-raw"
}
}
}
vmxData[ide+"devicetype"] = "cdrom-raw"
vmxData[ide+"filename"] = "auto detect"
}
// Rewrite the VMX

View File

@ -39,7 +39,6 @@ func TestStepCleanVMX_floppyPath(t *testing.T) {
t.Fatalf("err: %s", err)
}
state.Put("floppy_path", "foo")
state.Put("vmx_path", vmxPath)
// Test the run
@ -89,7 +88,6 @@ func TestStepCleanVMX_isoPath(t *testing.T) {
t.Fatalf("err: %s", err)
}
state.Put("iso_path", "foo")
state.Put("vmx_path", vmxPath)
// Test the run
@ -136,6 +134,7 @@ floppy0.filetype = "file"
`
const testVMXISOPath = `
ide0:0.devicetype = "cdrom-image"
ide0:0.filename = "foo"
ide0:1.filename = "bar"
foo = "bar"

View File

@ -137,10 +137,14 @@ LockWaitLoop:
}
}
if runtime.GOOS == "windows" && !s.Testing {
if runtime.GOOS != "darwin" && !s.Testing {
// Windows takes a while to yield control of the files when the
// process is exiting. We just sleep here. In the future, it'd be
// nice to find a better solution to this.
// process is exiting. Ubuntu will yield control of the files but
// VMWare may overwrite the VMX cleanup steps that run after this,
// so we wait to ensure VMWare has exited and flushed the VMX.
// We just sleep here. In the future, it'd be nice to find a better
// solution to this.
time.Sleep(5 * time.Second)
}

View File

@ -28,6 +28,10 @@ func (a *Artifact) String() string {
return fmt.Sprintf("VM files in directory: %s", a.dir)
}
func (a *Artifact) State(name string) interface{} {
return nil
}
func (a *Artifact) Destroy() error {
return a.dir.RemoveAll()
}

View File

@ -40,17 +40,20 @@ type config struct {
ISOChecksum string `mapstructure:"iso_checksum"`
ISOChecksumType string `mapstructure:"iso_checksum_type"`
ISOUrls []string `mapstructure:"iso_urls"`
Version string `mapstructure:"version"`
VMName string `mapstructure:"vm_name"`
BootCommand []string `mapstructure:"boot_command"`
SkipCompaction bool `mapstructure:"skip_compaction"`
VMXTemplatePath string `mapstructure:"vmx_template_path"`
RemoteType string `mapstructure:"remote_type"`
RemoteDatastore string `mapstructure:"remote_datastore"`
RemoteHost string `mapstructure:"remote_host"`
RemotePort uint `mapstructure:"remote_port"`
RemoteUser string `mapstructure:"remote_username"`
RemotePassword string `mapstructure:"remote_password"`
RemoteType string `mapstructure:"remote_type"`
RemoteDatastore string `mapstructure:"remote_datastore"`
RemoteCacheDatastore string `mapstructure:"remote_cache_datastore"`
RemoteCacheDirectory string `mapstructure:"remote_cache_directory"`
RemoteHost string `mapstructure:"remote_host"`
RemotePort uint `mapstructure:"remote_port"`
RemoteUser string `mapstructure:"remote_username"`
RemotePassword string `mapstructure:"remote_password"`
RawSingleISOUrl string `mapstructure:"iso_url"`
@ -110,6 +113,10 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName)
}
if b.config.Version == "" {
b.config.Version = "9"
}
if b.config.RemoteUser == "" {
b.config.RemoteUser = "root"
}
@ -118,24 +125,34 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
b.config.RemoteDatastore = "datastore1"
}
if b.config.RemoteCacheDatastore == "" {
b.config.RemoteCacheDatastore = b.config.RemoteDatastore
}
if b.config.RemoteCacheDirectory == "" {
b.config.RemoteCacheDirectory = "packer_cache"
}
if b.config.RemotePort == 0 {
b.config.RemotePort = 22
}
// Errors
templates := map[string]*string{
"disk_name": &b.config.DiskName,
"guest_os_type": &b.config.GuestOSType,
"iso_checksum": &b.config.ISOChecksum,
"iso_checksum_type": &b.config.ISOChecksumType,
"iso_url": &b.config.RawSingleISOUrl,
"vm_name": &b.config.VMName,
"vmx_template_path": &b.config.VMXTemplatePath,
"remote_type": &b.config.RemoteType,
"remote_host": &b.config.RemoteHost,
"remote_datastore": &b.config.RemoteDatastore,
"remote_user": &b.config.RemoteUser,
"remote_password": &b.config.RemotePassword,
"disk_name": &b.config.DiskName,
"guest_os_type": &b.config.GuestOSType,
"iso_checksum": &b.config.ISOChecksum,
"iso_checksum_type": &b.config.ISOChecksumType,
"iso_url": &b.config.RawSingleISOUrl,
"vm_name": &b.config.VMName,
"vmx_template_path": &b.config.VMXTemplatePath,
"remote_type": &b.config.RemoteType,
"remote_host": &b.config.RemoteHost,
"remote_datastore": &b.config.RemoteDatastore,
"remote_cache_datastore": &b.config.RemoteCacheDatastore,
"remote_cache_directory": &b.config.RemoteCacheDirectory,
"remote_user": &b.config.RemoteUser,
"remote_password": &b.config.RemotePassword,
}
for n, ptr := range templates {

View File

@ -134,6 +134,10 @@ func TestBuilderPrepare_Defaults(t *testing.T) {
t.Errorf("bad output dir: %s", b.config.OutputDir)
}
if b.config.Version != "9" {
t.Errorf("bad Version: %s", b.config.Version)
}
if b.config.SSHWaitTimeout != (20 * time.Minute) {
t.Errorf("bad wait timeout: %s", b.config.SSHWaitTimeout)
}

View File

@ -17,11 +17,13 @@ func NewDriver(config *config) (vmwcommon.Driver, error) {
drivers = []vmwcommon.Driver{
&ESX5Driver{
Host: config.RemoteHost,
Port: config.RemotePort,
Username: config.RemoteUser,
Password: config.RemotePassword,
Datastore: config.RemoteDatastore,
Host: config.RemoteHost,
Port: config.RemotePort,
Username: config.RemoteUser,
Password: config.RemotePassword,
Datastore: config.RemoteDatastore,
CacheDatastore: config.RemoteCacheDatastore,
CacheDirectory: config.RemoteCacheDirectory,
},
}

View File

@ -22,11 +22,13 @@ import (
// ESX5 driver talks to an ESXi5 hypervisor remotely over SSH to build
// virtual machines. This driver can only manage one machine at a time.
type ESX5Driver struct {
Host string
Port uint
Username string
Password string
Datastore string
Host string
Port uint
Username string
Password string
Datastore string
CacheDatastore string
CacheDirectory string
comm packer.Communicator
outputDir string
@ -55,7 +57,21 @@ func (d *ESX5Driver) IsRunning(string) (bool, error) {
}
func (d *ESX5Driver) Start(vmxPathLocal string, headless bool) error {
return d.sh("vim-cmd", "vmsvc/power.on", d.vmId)
for i := 0; i < 20; i++ {
err := d.sh("vim-cmd", "vmsvc/power.on", d.vmId)
if err != nil {
return err
}
time.Sleep((time.Duration(i) * time.Second) + 1)
running, err := d.IsRunning(vmxPathLocal)
if err != nil {
return err
}
if running {
return nil
}
}
return errors.New("Retry limit exceeded")
}
func (d *ESX5Driver) Stop(vmxPathLocal string) error {
@ -84,13 +100,7 @@ func (d *ESX5Driver) Unregister(vmxPathLocal string) error {
}
func (d *ESX5Driver) UploadISO(localPath string, checksum string, checksumType string) (string, error) {
cacheRoot, _ := filepath.Abs(".")
targetFile, err := filepath.Rel(cacheRoot, localPath)
if err != nil {
return "", err
}
finalPath := d.datastorePath(targetFile)
finalPath := d.cachePath(localPath)
if err := d.mkdir(filepath.ToSlash(filepath.Dir(finalPath))); err != nil {
return "", err
}
@ -300,6 +310,10 @@ func (d *ESX5Driver) datastorePath(path string) string {
return filepath.ToSlash(filepath.Join("/vmfs/volumes", d.Datastore, baseDir, filepath.Base(path)))
}
func (d *ESX5Driver) cachePath(path string) string {
return filepath.ToSlash(filepath.Join("/vmfs/volumes", d.CacheDatastore, d.CacheDirectory, filepath.Base(path)))
}
func (d *ESX5Driver) connect() error {
address := fmt.Sprintf("%s:%d", d.Host, d.Port)

View File

@ -15,6 +15,7 @@ type vmxTemplateData struct {
GuestOS string
DiskName string
ISOPath string
Version string
}
// This step creates the VMX file for the VM.
@ -41,6 +42,7 @@ func (s *stepCreateVMX) Run(state multistep.StateBag) multistep.StepAction {
Name: config.VMName,
GuestOS: config.GuestOSType,
DiskName: config.DiskName,
Version: config.Version,
ISOPath: isoPath,
}
@ -180,7 +182,7 @@ tools.upgrade.policy = "upgradeAtPowerCycle"
usb.pciSlotNumber = "32"
usb.present = "FALSE"
virtualHW.productCompatibility = "hosted"
virtualHW.version = "9"
virtualHW.version = "{{ .Version }}"
vmci0.id = "1861462627"
vmci0.pciSlotNumber = "35"
vmci0.present = "TRUE"

View File

@ -37,8 +37,17 @@ func (s *StepCloneVMX) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionHalt
}
diskName, ok := vmxData["scsi0:0.filename"]
if !ok {
var diskName string
if _, ok := vmxData["scsi0:0.filename"]; ok {
diskName = vmxData["scsi0:0.filename"]
}
if _, ok := vmxData["sata0:0.filename"]; ok {
diskName = vmxData["sata0:0.filename"]
}
if _, ok := vmxData["ide0:0.filename"]; ok {
diskName = vmxData["ide0:0.filename"]
}
if diskName == "" {
err := fmt.Errorf("Root disk filename could not be found!")
state.Put("error", err)
return multistep.ActionHalt

View File

@ -6,11 +6,10 @@ import (
"path/filepath"
"github.com/hashicorp/go-checkpoint"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/command"
)
func init() {
packer.VersionChecker = packerVersionCheck
checkpointResult = make(chan *checkpoint.CheckResponse, 1)
}
@ -33,9 +32,9 @@ func runCheckpoint(c *config) {
return
}
version := packer.Version
if packer.VersionPrerelease != "" {
version += fmt.Sprintf(".%s", packer.VersionPrerelease)
version := Version
if VersionPrerelease != "" {
version += fmt.Sprintf(".%s", VersionPrerelease)
}
signaturePath := filepath.Join(configDir, "checkpoint_signature")
@ -58,21 +57,23 @@ func runCheckpoint(c *config) {
checkpointResult <- resp
}
// packerVersionCheck implements packer.VersionCheckFunc and is used
// commandVersionCheck implements command.VersionCheckFunc and is used
// as the version checker.
func packerVersionCheck(current string) (packer.VersionCheckInfo, error) {
func commandVersionCheck() (command.VersionCheckInfo, error) {
// Wait for the result to come through
info := <-checkpointResult
if info == nil {
var zero packer.VersionCheckInfo
var zero command.VersionCheckInfo
return zero, nil
}
// Build the alerts that we may have received about our version
alerts := make([]string, len(info.Alerts))
for i, a := range info.Alerts {
alerts[i] = a.Message
}
return packer.VersionCheckInfo{
return command.VersionCheckInfo{
Outdated: info.Outdated,
Latest: info.CurrentVersion,
Alerts: alerts,

View File

@ -1,4 +1,4 @@
package build
package command
import (
"bytes"
@ -14,16 +14,20 @@ import (
"sync"
)
type Command byte
func (Command) Help() string {
return strings.TrimSpace(helpText)
type BuildCommand struct {
Meta
}
func (c Command) Run(env packer.Environment, args []string) int {
func (c BuildCommand) Run(args []string) int {
var cfgColor, cfgDebug, cfgForce, cfgParallel bool
buildOptions := new(cmdcommon.BuildOptions)
env, err := c.Meta.Environment()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error initializing environment: %s", err))
return 1
}
cmdFlags := flag.NewFlagSet("build", flag.ContinueOnError)
cmdFlags.Usage = func() { env.Ui().Say(c.Help()) }
cmdFlags.BoolVar(&cfgColor, "color", true, "enable or disable color")
@ -278,6 +282,28 @@ func (c Command) Run(env packer.Environment, args []string) int {
return 0
}
func (Command) Synopsis() string {
func (BuildCommand) Help() string {
helpText := `
Usage: packer build [options] TEMPLATE
Will execute multiple builds in parallel as defined in the template.
The various artifacts created by the template will be outputted.
Options:
-debug Debug mode enabled for builds
-force Force a build to continue if artifacts exist, deletes existing artifacts
-machine-readable Machine-readable output
-except=foo,bar,baz Build all builds other than these
-only=foo,bar,baz Only build the given builds by name
-parallel=false Disable parallelization (on by default)
-var 'key=value' Variable for templates, can be used multiple times.
-var-file=path JSON file containing user variables.
`
return strings.TrimSpace(helpText)
}
func (BuildCommand) Synopsis() string {
return "build image(s) from template"
}

View File

@ -1,54 +0,0 @@
package build
import (
"bytes"
"github.com/mitchellh/packer/packer"
"testing"
)
func testEnvironment() packer.Environment {
config := packer.DefaultEnvironmentConfig()
config.Ui = &packer.BasicUi{
Reader: new(bytes.Buffer),
Writer: new(bytes.Buffer),
}
env, err := packer.NewEnvironment(config)
if err != nil {
panic(err)
}
return env
}
func TestCommand_Implements(t *testing.T) {
var _ packer.Command = new(Command)
}
func TestCommand_Run_NoArgs(t *testing.T) {
command := new(Command)
result := command.Run(testEnvironment(), make([]string, 0))
if result != 1 {
t.Fatalf("bad: %d", result)
}
}
func TestCommand_Run_MoreThanOneArg(t *testing.T) {
command := new(Command)
args := []string{"one", "two"}
result := command.Run(testEnvironment(), args)
if result != 1 {
t.Fatalf("bad: %d", result)
}
}
func TestCommand_Run_MissingFile(t *testing.T) {
command := new(Command)
args := []string{"i-better-not-exist"}
result := command.Run(testEnvironment(), args)
if result != 1 {
t.Fatalf("bad: %d", result)
}
}

View File

@ -1,19 +0,0 @@
package build
const helpText = `
Usage: packer build [options] TEMPLATE
Will execute multiple builds in parallel as defined in the template.
The various artifacts created by the template will be outputted.
Options:
-debug Debug mode enabled for builds
-force Force a build to continue if artifacts exist, deletes existing artifacts
-machine-readable Machine-readable output
-except=foo,bar,baz Build all builds other than these
-only=foo,bar,baz Only build the given builds by name
-parallel=false Disable parallelization (on by default)
-var 'key=value' Variable for templates, can be used multiple times.
-var-file=path JSON file containing user variables.
`

View File

@ -1,23 +1,28 @@
package fix
package command
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"github.com/mitchellh/packer/packer"
"log"
"os"
"strings"
"github.com/mitchellh/packer/fix"
)
type Command byte
func (Command) Help() string {
return strings.TrimSpace(helpString)
type FixCommand struct {
Meta
}
func (c Command) Run(env packer.Environment, args []string) int {
func (c *FixCommand) Run(args []string) int {
env, err := c.Meta.Environment()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error initializing environment: %s", err))
return 1
}
cmdFlags := flag.NewFlagSet("fix", flag.ContinueOnError)
cmdFlags.Usage = func() { env.Ui().Say(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
@ -50,9 +55,9 @@ func (c Command) Run(env packer.Environment, args []string) int {
tplF.Close()
input := templateData
for _, name := range FixerOrder {
for _, name := range fix.FixerOrder {
var err error
fixer, ok := Fixers[name]
fixer, ok := fix.Fixers[name]
if !ok {
panic("fixer not found: " + name)
}
@ -85,6 +90,30 @@ func (c Command) Run(env packer.Environment, args []string) int {
return 0
}
func (c Command) Synopsis() string {
func (*FixCommand) Help() string {
helpText := `
Usage: packer fix [options] TEMPLATE
Reads the JSON template and attempts to fix known backwards
incompatibilities. The fixed template will be outputted to standard out.
If the template cannot be fixed due to an error, the command will exit
with a non-zero exit status. Error messages will appear on standard error.
Fixes that are run:
iso-md5 Replaces "iso_md5" in builders with newer "iso_checksum"
createtime Replaces ".CreateTime" in builder configs with "{{timestamp}}"
virtualbox-gaattach Updates VirtualBox builders using "guest_additions_attach"
to use "guest_additions_mode"
pp-vagrant-override Replaces old-style provider overrides for the Vagrant
post-processor to new-style as of Packer 0.5.0.
virtualbox-rename Updates "virtualbox" builders to "virtualbox-iso"
`
return strings.TrimSpace(helpText)
}
func (c *FixCommand) Synopsis() string {
return "fixes templates from old versions of packer"
}

View File

@ -1,14 +0,0 @@
package fix
import (
"github.com/mitchellh/packer/packer"
"testing"
)
func TestCommand_Impl(t *testing.T) {
var raw interface{}
raw = new(Command)
if _, ok := raw.(packer.Command); !ok {
t.Fatalf("must be a Command")
}
}

View File

@ -1,22 +0,0 @@
package fix
const helpString = `
Usage: packer fix [options] TEMPLATE
Reads the JSON template and attempts to fix known backwards
incompatibilities. The fixed template will be outputted to standard out.
If the template cannot be fixed due to an error, the command will exit
with a non-zero exit status. Error messages will appear on standard error.
Fixes that are run:
iso-md5 Replaces "iso_md5" in builders with newer "iso_checksum"
createtime Replaces ".CreateTime" in builder configs with "{{timestamp}}"
virtualbox-gaattach Updates VirtualBox builders using "guest_additions_attach"
to use "guest_additions_mode"
pp-vagrant-override Replaces old-style provider overrides for the Vagrant
post-processor to new-style as of Packer 0.5.0.
virtualbox-rename Updates "virtualbox" builders to "virtualbox-iso"
`

View File

@ -1,4 +1,4 @@
package inspect
package command
import (
"flag"
@ -9,17 +9,17 @@ import (
"strings"
)
type Command struct{}
func (Command) Help() string {
return strings.TrimSpace(helpText)
type InspectCommand struct{
Meta
}
func (c Command) Synopsis() string {
return "see components of a template"
}
func (c *InspectCommand) Run(args []string) int {
env, err := c.Meta.Environment()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error initializing environment: %s", err))
return 1
}
func (c Command) Run(env packer.Environment, args []string) int {
flags := flag.NewFlagSet("inspect", flag.ContinueOnError)
flags.Usage = func() { env.Ui().Say(c.Help()) }
if err := flags.Parse(args); err != nil {
@ -148,3 +148,23 @@ func (c Command) Run(env packer.Environment, args []string) int {
return 0
}
func (*InspectCommand) Help() string {
helpText := `
Usage: packer inspect TEMPLATE
Inspects a template, parsing and outputting the components a template
defines. This does not validate the contents of a template (other than
basic syntax by necessity).
Options:
-machine-readable Machine-readable output
`
return strings.TrimSpace(helpText)
}
func (c *InspectCommand) Synopsis() string {
return "see components of a template"
}

View File

@ -1,14 +0,0 @@
package inspect
import (
"github.com/mitchellh/packer/packer"
"testing"
)
func TestCommand_Impl(t *testing.T) {
var raw interface{}
raw = new(Command)
if _, ok := raw.(packer.Command); !ok {
t.Fatalf("must be a Command")
}
}

View File

@ -1,13 +0,0 @@
package inspect
const helpText = `
Usage: packer inspect TEMPLATE
Inspects a template, parsing and outputting the components a template
defines. This does not validate the contents of a template (other than
basic syntax by necessity).
Options:
-machine-readable Machine-readable output
`

15
command/meta.go Normal file
View File

@ -0,0 +1,15 @@
package command
import (
"github.com/mitchellh/cli"
"github.com/mitchellh/packer/packer"
)
type Meta struct {
EnvConfig *packer.EnvironmentConfig
Ui cli.Ui
}
func (m *Meta) Environment() (packer.Environment, error) {
return packer.NewEnvironment(m.EnvConfig)
}

View File

@ -1,4 +1,4 @@
package validate
package command
import (
"flag"
@ -9,16 +9,20 @@ import (
"strings"
)
type Command byte
func (Command) Help() string {
return strings.TrimSpace(helpString)
type ValidateCommand struct {
Meta
}
func (c Command) Run(env packer.Environment, args []string) int {
func (c *ValidateCommand) Run(args []string) int {
var cfgSyntaxOnly bool
buildOptions := new(cmdcommon.BuildOptions)
env, err := c.Meta.Environment()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error initializing environment: %s", err))
return 1
}
cmdFlags := flag.NewFlagSet("validate", flag.ContinueOnError)
cmdFlags.Usage = func() { env.Ui().Say(c.Help()) }
cmdFlags.BoolVar(&cfgSyntaxOnly, "syntax-only", false, "check syntax only")
@ -123,6 +127,29 @@ func (c Command) Run(env packer.Environment, args []string) int {
return 0
}
func (Command) Synopsis() string {
func (*ValidateCommand) Help() string {
helpText := `
Usage: packer validate [options] TEMPLATE
Checks the template is valid by parsing the template and also
checking the configuration with the various builders, provisioners, etc.
If it is not valid, the errors will be shown and the command will exit
with a non-zero exit status. If it is valid, it will exit with a zero
exit status.
Options:
-syntax-only Only check syntax. Do not verify config of the template.
-except=foo,bar,baz Validate all builds other than these
-only=foo,bar,baz Validate only these builds
-var 'key=value' Variable for templates, can be used multiple times.
-var-file=path JSON file containing user variables.
`
return strings.TrimSpace(helpText)
}
func (*ValidateCommand) Synopsis() string {
return "check that a template is valid"
}

View File

@ -1,14 +0,0 @@
package validate
import (
"github.com/mitchellh/packer/packer"
"testing"
)
func TestCommand_Impl(t *testing.T) {
var raw interface{}
raw = new(Command)
if _, ok := raw.(packer.Command); !ok {
t.Fatalf("must be a Command")
}
}

View File

@ -1,20 +0,0 @@
package validate
const helpString = `
Usage: packer validate [options] TEMPLATE
Checks the template is valid by parsing the template and also
checking the configuration with the various builders, provisioners, etc.
If it is not valid, the errors will be shown and the command will exit
with a non-zero exit status. If it is valid, it will exit with a zero
exit status.
Options:
-syntax-only Only check syntax. Do not verify config of the template.
-except=foo,bar,baz Validate all builds other than these
-only=foo,bar,baz Validate only these builds
-var 'key=value' Variable for templates, can be used multiple times.
-var-file=path JSON file containing user variables.
`

83
command/version.go Normal file
View File

@ -0,0 +1,83 @@
package command
import (
"bytes"
"fmt"
)
// VersionCommand is a Command implementation prints the version.
type VersionCommand struct {
Meta
Revision string
Version string
VersionPrerelease string
CheckFunc VersionCheckFunc
}
// VersionCheckFunc is the callback called by the Version command to
// check if there is a new version of Packer.
type VersionCheckFunc func() (VersionCheckInfo, error)
// VersionCheckInfo is the return value for the VersionCheckFunc callback
// and tells the Version command information about the latest version
// of Packer.
type VersionCheckInfo struct {
Outdated bool
Latest string
Alerts []string
}
func (c *VersionCommand) Help() string {
return ""
}
func (c *VersionCommand) Run(args []string) int {
env, err := c.Meta.Environment()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error initializing environment: %s", err))
return 1
}
env.Ui().Machine("version", c.Version)
env.Ui().Machine("version-prelease", c.VersionPrerelease)
env.Ui().Machine("version-commit", c.Revision)
var versionString bytes.Buffer
fmt.Fprintf(&versionString, "Packer v%s", c.Version)
if c.VersionPrerelease != "" {
fmt.Fprintf(&versionString, ".%s", c.VersionPrerelease)
if c.Revision != "" {
fmt.Fprintf(&versionString, " (%s)", c.Revision)
}
}
c.Ui.Output(versionString.String())
// If we have a version check function, then let's check for
// the latest version as well.
if c.CheckFunc != nil {
// Separate the prior output with a newline
c.Ui.Output("")
// Check the latest version
info, err := c.CheckFunc()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error checking latest version: %s", err))
}
if info.Outdated {
c.Ui.Output(fmt.Sprintf(
"Your version of Packer is out of date! The latest version\n"+
"is %s. You can update by downloading from www.packer.io",
info.Latest))
}
}
return 0
}
func (c *VersionCommand) Synopsis() string {
return "Prints the Packer version"
}

11
command/version_test.go Normal file
View File

@ -0,0 +1,11 @@
package command
import (
"testing"
"github.com/mitchellh/cli"
)
func TestVersionCommand_implements(t *testing.T) {
var _ cli.Command = &VersionCommand{}
}

86
commands.go Normal file
View File

@ -0,0 +1,86 @@
package main
import (
"os"
"os/signal"
"github.com/mitchellh/cli"
"github.com/mitchellh/packer/command"
)
// Commands is the mapping of all the available Terraform commands.
var Commands map[string]cli.CommandFactory
// Ui is the cli.Ui used for communicating to the outside world.
var Ui cli.Ui
const ErrorPrefix = "e:"
const OutputPrefix = "o:"
func init() {
Ui = &cli.PrefixedUi{
AskPrefix: OutputPrefix,
OutputPrefix: OutputPrefix,
InfoPrefix: OutputPrefix,
ErrorPrefix: ErrorPrefix,
Ui: &cli.BasicUi{Writer: os.Stdout},
}
meta := command.Meta{
EnvConfig: &EnvConfig,
Ui: Ui,
}
Commands = map[string]cli.CommandFactory{
"build": func() (cli.Command, error) {
return &command.BuildCommand{
Meta: meta,
}, nil
},
"fix": func() (cli.Command, error) {
return &command.FixCommand{
Meta: meta,
}, nil
},
"inspect": func() (cli.Command, error) {
return &command.InspectCommand{
Meta: meta,
}, nil
},
"validate": func() (cli.Command, error) {
return &command.ValidateCommand{
Meta: meta,
}, nil
},
"version": func() (cli.Command, error) {
return &command.VersionCommand{
Meta: meta,
Revision: GitCommit,
Version: Version,
VersionPrerelease: VersionPrerelease,
CheckFunc: commandVersionCheck,
}, nil
},
}
}
// makeShutdownCh creates an interrupt listener and returns a channel.
// A message will be sent on the channel for every interrupt received.
func makeShutdownCh() <-chan struct{} {
resultCh := make(chan struct{})
signalCh := make(chan os.Signal, 4)
signal.Notify(signalCh, os.Interrupt)
go func() {
for {
<-signalCh
resultCh <- struct{}{}
}
}()
return resultCh
}

View File

@ -3,10 +3,11 @@ package common
import (
"encoding/hex"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"time"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
// StepDownload downloads a remote file using the download client within
@ -70,7 +71,7 @@ func (s *StepDownload) Run(state multistep.StateBag) multistep.StepAction {
CopyFile: false,
Hash: HashForType(s.ChecksumType),
Checksum: checksum,
UserAgent: packer.VersionString(),
UserAgent: "Packer",
}
path, err, retry := s.download(config, state)

View File

@ -13,6 +13,9 @@ import (
"github.com/mitchellh/packer/packer/plugin"
)
// EnvConfig is the global EnvironmentConfig we use to initialize the CLI.
var EnvConfig packer.EnvironmentConfig
type config struct {
DisableCheckpoint bool `json:"disable_checkpoint"`
DisableCheckpointSignature bool `json:"disable_checkpoint_signature"`
@ -20,7 +23,6 @@ type config struct {
PluginMaxPort uint
Builders map[string]string
Commands map[string]string
PostProcessors map[string]string `json:"post-processors"`
Provisioners map[string]string
}
@ -79,15 +81,6 @@ func (c *config) Discover() error {
return nil
}
// Returns an array of defined command names.
func (c *config) CommandNames() (result []string) {
result = make([]string, 0, len(c.Commands))
for name := range c.Commands {
result = append(result, name)
}
return
}
// This is a proper packer.BuilderFunc that can be used to load packer.Builder
// implementations from the defined plugins.
func (c *config) LoadBuilder(name string) (packer.Builder, error) {
@ -101,19 +94,6 @@ func (c *config) LoadBuilder(name string) (packer.Builder, error) {
return c.pluginClient(bin).Builder()
}
// This is a proper packer.CommandFunc that can be used to load packer.Command
// implementations from the defined plugins.
func (c *config) LoadCommand(name string) (packer.Command, error) {
log.Printf("Loading command: %s\n", name)
bin, ok := c.Commands[name]
if !ok {
log.Printf("Command not found: %s\n", name)
return nil, nil
}
return c.pluginClient(bin).Command()
}
// This is a proper implementation of packer.HookFunc that can be used
// to load packer.Hook implementations from the defined plugins.
func (c *config) LoadHook(name string) (packer.Hook, error) {
@ -149,14 +129,16 @@ func (c *config) LoadProvisioner(name string) (packer.Provisioner, error) {
func (c *config) discover(path string) error {
var err error
err = c.discoverSingle(
filepath.Join(path, "packer-builder-*"), &c.Builders)
if err != nil {
return err
if !filepath.IsAbs(path) {
path, err = filepath.Abs(path)
if err != nil {
return err
}
}
err = c.discoverSingle(
filepath.Join(path, "packer-command-*"), &c.Commands)
filepath.Join(path, "packer-builder-*"), &c.Builders)
if err != nil {
return err
}

29
log.go Normal file
View File

@ -0,0 +1,29 @@
package main
import (
"io"
"os"
)
// These are the environmental variables that determine if we log, and if
// we log whether or not the log should go to a file.
const EnvLog = "PACKER_LOG" //Set to True
const EnvLogFile = "PACKER_LOG_PATH" //Set to a file
// logOutput determines where we should send logs (if anywhere).
func logOutput() (logOutput io.Writer, err error) {
logOutput = nil
if os.Getenv(EnvLog) != "" {
logOutput = os.Stderr
if logPath := os.Getenv(EnvLogFile); logPath != "" {
var err error
logOutput, err = os.Create(logPath)
if err != nil {
return nil, err
}
}
}
return
}

View File

@ -9,10 +9,13 @@ import (
"os"
"path/filepath"
"runtime"
"sync"
"github.com/mitchellh/cli"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/packer/plugin"
"github.com/mitchellh/panicwrap"
"github.com/mitchellh/prefixedio"
)
func main() {
@ -24,63 +27,82 @@ func main() {
// realMain is executed from main and returns the exit status to exit with.
func realMain() int {
// If there is no explicit number of Go threads to use, then set it
if os.Getenv("GOMAXPROCS") == "" {
runtime.GOMAXPROCS(runtime.NumCPU())
var wrapConfig panicwrap.WrapConfig
if !panicwrap.Wrapped(&wrapConfig) {
// Determine where logs should go in general (requested by the user)
logWriter, err := logOutput()
if err != nil {
fmt.Fprintf(os.Stderr, "Couldn't setup log output: %s", err)
return 1
}
if logWriter == nil {
logWriter = ioutil.Discard
}
// We always send logs to a temporary file that we use in case
// there is a panic. Otherwise, we delete it.
logTempFile, err := ioutil.TempFile("", "packer-log")
if err != nil {
fmt.Fprintf(os.Stderr, "Couldn't setup logging tempfile: %s", err)
return 1
}
defer os.Remove(logTempFile.Name())
defer logTempFile.Close()
// Tell the logger to log to this file
os.Setenv(EnvLog, "")
os.Setenv(EnvLogFile, "")
// Setup the prefixed readers that send data properly to
// stdout/stderr.
doneCh := make(chan struct{})
outR, outW := io.Pipe()
go copyOutput(outR, doneCh)
// Create the configuration for panicwrap and wrap our executable
wrapConfig.Handler = panicHandler(logTempFile)
wrapConfig.Writer = io.MultiWriter(logTempFile, logWriter)
wrapConfig.Stdout = outW
exitStatus, err := panicwrap.Wrap(&wrapConfig)
if err != nil {
fmt.Fprintf(os.Stderr, "Couldn't start Packer: %s", err)
return 1
}
// If >= 0, we're the parent, so just exit
if exitStatus >= 0 {
// Close the stdout writer so that our copy process can finish
outW.Close()
// Wait for the output copying to finish
<-doneCh
return exitStatus
}
// We're the child, so just close the tempfile we made in order to
// save file handles since the tempfile is only used by the parent.
logTempFile.Close()
}
// Determine where logs should go in general (requested by the user)
logWriter, err := logOutput()
if err != nil {
fmt.Fprintf(os.Stderr, "Couldn't setup log output: %s", err)
return 1
}
// We also always send logs to a temporary file that we use in case
// there is a panic. Otherwise, we delete it.
logTempFile, err := ioutil.TempFile("", "packer-log")
if err != nil {
fmt.Fprintf(os.Stderr, "Couldn't setup logging tempfile: %s", err)
return 1
}
defer os.Remove(logTempFile.Name())
defer logTempFile.Close()
// Reset the log variables to minimize work in the subprocess
os.Setenv("PACKER_LOG", "")
os.Setenv("PACKER_LOG_FILE", "")
// Create the configuration for panicwrap and wrap our executable
wrapConfig := &panicwrap.WrapConfig{
Handler: panicHandler(logTempFile),
Writer: io.MultiWriter(logTempFile, logWriter),
}
exitStatus, err := panicwrap.Wrap(wrapConfig)
if err != nil {
fmt.Fprintf(os.Stderr, "Couldn't start Packer: %s", err)
return 1
}
if exitStatus >= 0 {
return exitStatus
}
// We're the child, so just close the tempfile we made in order to
// save file handles since the tempfile is only used by the parent.
logTempFile.Close()
// Call the real main
return wrappedMain()
}
// wrappedMain is called only when we're wrapped by panicwrap and
// returns the exit status to exit with.
func wrappedMain() int {
// If there is no explicit number of Go threads to use, then set it
if os.Getenv("GOMAXPROCS") == "" {
runtime.GOMAXPROCS(runtime.NumCPU())
}
log.SetOutput(os.Stderr)
log.Printf(
"Packer Version: %s %s %s",
packer.Version, packer.VersionPrerelease, packer.GitCommit)
"[INFO] Packer version: %s %s %s",
Version, VersionPrerelease, GitCommit)
log.Printf("Packer Target OS/Arch: %s %s", runtime.GOOS, runtime.GOARCH)
log.Printf("Built with Go Version: %s", runtime.Version())
@ -118,16 +140,14 @@ func wrappedMain() int {
defer plugin.CleanupClients()
// Create the environment configuration
envConfig := packer.DefaultEnvironmentConfig()
envConfig.Cache = cache
envConfig.Commands = config.CommandNames()
envConfig.Components.Builder = config.LoadBuilder
envConfig.Components.Command = config.LoadCommand
envConfig.Components.Hook = config.LoadHook
envConfig.Components.PostProcessor = config.LoadPostProcessor
envConfig.Components.Provisioner = config.LoadProvisioner
EnvConfig = *packer.DefaultEnvironmentConfig()
EnvConfig.Cache = cache
EnvConfig.Components.Builder = config.LoadBuilder
EnvConfig.Components.Hook = config.LoadHook
EnvConfig.Components.PostProcessor = config.LoadPostProcessor
EnvConfig.Components.Provisioner = config.LoadProvisioner
if machineReadable {
envConfig.Ui = &packer.MachineReadableUi{
EnvConfig.Ui = &packer.MachineReadableUi{
Writer: os.Stdout,
}
@ -139,17 +159,18 @@ func wrappedMain() int {
}
}
env, err := packer.NewEnvironment(envConfig)
if err != nil {
fmt.Fprintf(os.Stderr, "Packer initialization error: \n\n%s\n", err)
return 1
//setupSignalHandlers(env)
cli := &cli.CLI{
Args: args,
Commands: Commands,
HelpFunc: cli.BasicHelpFunc("packer"),
HelpWriter: os.Stdout,
}
setupSignalHandlers(env)
exitCode, err := env.Cli(args)
exitCode, err := cli.Run()
if err != nil {
fmt.Fprintf(os.Stderr, "Error executing CLI: %s\n", err.Error())
fmt.Fprintf(os.Stderr, "Error executing CLI: %s\n", err)
return 1
}
@ -220,20 +241,44 @@ func loadConfig() (*config, error) {
return &config, nil
}
// logOutput determines where we should send logs (if anywhere).
func logOutput() (logOutput io.Writer, err error) {
logOutput = ioutil.Discard
if os.Getenv("PACKER_LOG") != "" {
logOutput = os.Stderr
// copyOutput uses output prefixes to determine whether data on stdout
// should go to stdout or stderr. This is due to panicwrap using stderr
// as the log and error channel.
func copyOutput(r io.Reader, doneCh chan<- struct{}) {
defer close(doneCh)
if logPath := os.Getenv("PACKER_LOG_PATH"); logPath != "" {
var err error
logOutput, err = os.Create(logPath)
if err != nil {
return nil, err
}
}
pr, err := prefixedio.NewReader(r)
if err != nil {
panic(err)
}
return
stderrR, err := pr.Prefix(ErrorPrefix)
if err != nil {
panic(err)
}
stdoutR, err := pr.Prefix(OutputPrefix)
if err != nil {
panic(err)
}
defaultR, err := pr.Prefix("")
if err != nil {
panic(err)
}
var wg sync.WaitGroup
wg.Add(3)
go func() {
defer wg.Done()
io.Copy(os.Stderr, stderrR)
}()
go func() {
defer wg.Done()
io.Copy(os.Stdout, stdoutR)
}()
go func() {
defer wg.Done()
io.Copy(os.Stdout, defaultR)
}()
wg.Wait()
}

View File

@ -25,6 +25,10 @@ type Artifact interface {
// This is used for UI output. It can be multiple lines.
String() string
// State allows the caller to ask for builder specific state information
// relating to the artifact instance.
State(name string) interface{}
// Destroy deletes the artifact. Packer calls this for various reasons,
// such as if a post-processor has processed this artifact and it is
// no longer needed.

View File

@ -5,6 +5,7 @@ type MockArtifact struct {
BuilderIdValue string
FilesValue []string
IdValue string
StateValues map[string]interface{}
DestroyCalled bool
}
@ -37,6 +38,11 @@ func (*MockArtifact) String() string {
return "string"
}
func (a *MockArtifact) State(name string) interface{} {
value, _ := a.StateValues[name]
return value
}
func (a *MockArtifact) Destroy() error {
a.DestroyCalled = true
return nil

View File

@ -2,6 +2,7 @@ package packer
type TestArtifact struct {
id string
state map[string]interface{}
destroyCalled bool
}
@ -26,6 +27,11 @@ func (*TestArtifact) String() string {
return "string"
}
func (a *TestArtifact) State(name string) interface{} {
value, _ := a.state[name]
return value
}
func (a *TestArtifact) Destroy() error {
a.destroyCalled = true
return nil

View File

@ -1,23 +0,0 @@
package packer
// A command is a runnable sub-command of the `packer` application.
// When `packer` is called with the proper subcommand, this will be
// called.
//
// The mapping of command names to command interfaces is in the
// Environment struct.
type Command interface {
// Help should return long-form help text that includes the command-line
// usage, a brief few sentences explaining the function of the command,
// and the complete list of flags the command accepts.
Help() string
// Run should run the actual command with the given environmet and
// command-line arguments. It should return the exit status when it is
// finished.
Run(env Environment, args []string) int
// Synopsis should return a one-line, short synopsis of the command.
// This should be less than 50 characters ideally.
Synopsis() string
}

View File

@ -1,22 +0,0 @@
package packer
type TestCommand struct {
runArgs []string
runCalled bool
runEnv Environment
}
func (tc *TestCommand) Help() string {
return "bar"
}
func (tc *TestCommand) Run(env Environment, args []string) int {
tc.runCalled = true
tc.runArgs = args
tc.runEnv = env
return 0
}
func (tc *TestCommand) Synopsis() string {
return "foo"
}

View File

@ -4,19 +4,12 @@ package packer
import (
"errors"
"fmt"
"log"
"os"
"sort"
"strings"
"sync"
)
// The function type used to lookup Builder implementations.
type BuilderFunc func(name string) (Builder, error)
// The function type used to lookup Command implementations.
type CommandFunc func(name string) (Command, error)
// The function type used to lookup Hook implementations.
type HookFunc func(name string) (Hook, error)
@ -31,7 +24,6 @@ type ProvisionerFunc func(name string) (Provisioner, error)
// commands, etc.
type ComponentFinder struct {
Builder BuilderFunc
Command CommandFunc
Hook HookFunc
PostProcessor PostProcessorFunc
Provisioner ProvisionerFunc
@ -45,7 +37,6 @@ type ComponentFinder struct {
type Environment interface {
Builder(string) (Builder, error)
Cache() Cache
Cli([]string) (int, error)
Hook(string) (Hook, error)
PostProcessor(string) (PostProcessor, error)
Provisioner(string) (Provisioner, error)
@ -56,7 +47,6 @@ type Environment interface {
// environment.
type coreEnvironment struct {
cache Cache
commands []string
components ComponentFinder
ui Ui
}
@ -64,22 +54,14 @@ type coreEnvironment struct {
// This struct configures new environments.
type EnvironmentConfig struct {
Cache Cache
Commands []string
Components ComponentFinder
Ui Ui
}
type helpCommandEntry struct {
i int
key string
synopsis string
}
// DefaultEnvironmentConfig returns a default EnvironmentConfig that can
// be used to create a new enviroment with NewEnvironment with sane defaults.
func DefaultEnvironmentConfig() *EnvironmentConfig {
config := &EnvironmentConfig{}
config.Commands = make([]string, 0)
config.Ui = &BasicUi{
Reader: os.Stdin,
Writer: os.Stdout,
@ -98,7 +80,6 @@ func NewEnvironment(config *EnvironmentConfig) (resultEnv Environment, err error
env := &coreEnvironment{}
env.cache = config.Cache
env.commands = config.Commands
env.components = config.Components
env.ui = config.Ui
@ -109,10 +90,6 @@ func NewEnvironment(config *EnvironmentConfig) (resultEnv Environment, err error
env.components.Builder = func(string) (Builder, error) { return nil, nil }
}
if env.components.Command == nil {
env.components.Command = func(string) (Command, error) { return nil, nil }
}
if env.components.Hook == nil {
env.components.Hook = func(string) (Hook, error) { return nil, nil }
}
@ -199,159 +176,6 @@ func (e *coreEnvironment) Provisioner(name string) (p Provisioner, err error) {
return
}
// Executes a command as if it was typed on the command-line interface.
// The return value is the exit code of the command.
func (e *coreEnvironment) Cli(args []string) (result int, err error) {
log.Printf("Environment.Cli: %#v\n", args)
// If we have no arguments, just short-circuit here and print the help
if len(args) == 0 {
e.printHelp()
return 1, nil
}
// This variable will track whether or not we're supposed to print
// the help or not.
isHelp := false
for _, arg := range args {
if arg == "-h" || arg == "--help" {
isHelp = true
break
}
}
// Trim up to the command name
for i, v := range args {
if len(v) > 0 && v[0] != '-' {
args = args[i:]
break
}
}
log.Printf("command + args: %#v", args)
version := args[0] == "version"
if !version {
for _, arg := range args {
if arg == "--version" || arg == "-v" {
version = true
break
}
}
}
var command Command
if version {
command = new(versionCommand)
}
if command == nil {
command, err = e.components.Command(args[0])
if err != nil {
return
}
// If we still don't have a command, show the help.
if command == nil {
e.ui.Error(fmt.Sprintf("Unknown command: %s\n", args[0]))
e.printHelp()
return 1, nil
}
}
// If we're supposed to print help, then print the help of the
// command rather than running it.
if isHelp {
e.ui.Say(command.Help())
return 0, nil
}
log.Printf("Executing command: %s\n", args[0])
return command.Run(e, args[1:]), nil
}
// Prints the CLI help to the UI.
func (e *coreEnvironment) printHelp() {
// Created a sorted slice of the map keys and record the longest
// command name so we can better format the output later.
maxKeyLen := 0
for _, command := range e.commands {
if len(command) > maxKeyLen {
maxKeyLen = len(command)
}
}
// Sort the keys
sort.Strings(e.commands)
// Create the communication/sync mechanisms to get the synopsis' of
// the various commands. We do this in parallel since the overhead
// of the subprocess underneath is very expensive and this speeds things
// up an incredible amount.
var wg sync.WaitGroup
ch := make(chan *helpCommandEntry)
for i, key := range e.commands {
wg.Add(1)
// Get the synopsis in a goroutine since it may take awhile
// to subprocess out.
go func(i int, key string) {
defer wg.Done()
var synopsis string
command, err := e.components.Command(key)
if err != nil {
synopsis = fmt.Sprintf("Error loading command: %s", err.Error())
} else if command == nil {
return
} else {
synopsis = command.Synopsis()
}
// Pad the key with spaces so that they're all the same width
key = fmt.Sprintf("%s%s", key, strings.Repeat(" ", maxKeyLen-len(key)))
// Output the command and the synopsis
ch <- &helpCommandEntry{
i: i,
key: key,
synopsis: synopsis,
}
}(i, key)
}
e.ui.Say("usage: packer [--version] [--help] <command> [<args>]\n")
e.ui.Say("Available commands are:")
// Make a goroutine that just waits for all the synopsis gathering
// to complete, and then output it.
synopsisDone := make(chan struct{})
go func() {
defer close(synopsisDone)
entries := make([]string, len(e.commands))
for entry := range ch {
e.ui.Machine("command", entry.key, entry.synopsis)
message := fmt.Sprintf(" %s %s", entry.key, entry.synopsis)
entries[entry.i] = message
}
for _, message := range entries {
if message != "" {
e.ui.Say(message)
}
}
}()
// Wait to complete getting the synopsis' then close the channel
wg.Wait()
close(ch)
<-synopsisDone
e.ui.Say("\nGlobally recognized options:")
e.ui.Say(" -machine-readable Machine-readable output format.")
}
// Returns the UI for the environment. The UI is the interface that should
// be used for all communication with the outside world.
func (e *coreEnvironment) Ui() Ui {

View File

@ -6,8 +6,6 @@ import (
"io/ioutil"
"log"
"os"
"reflect"
"strings"
"testing"
)
@ -43,13 +41,6 @@ func testEnvironment() Environment {
return env
}
func TestEnvironment_DefaultConfig_Commands(t *testing.T) {
config := DefaultEnvironmentConfig()
if len(config.Commands) != 0 {
t.Fatalf("bad: %#v", config.Commands)
}
}
func TestEnvironment_DefaultConfig_Ui(t *testing.T) {
config := DefaultEnvironmentConfig()
if config.Ui == nil {
@ -91,7 +82,6 @@ func TestEnvironment_NilComponents(t *testing.T) {
// anything but if there is a panic in the test then yeah, something
// went wrong.
env.Builder("foo")
env.Cli([]string{"foo"})
env.Hook("foo")
env.PostProcessor("foo")
env.Provisioner("foo")
@ -154,117 +144,6 @@ func TestEnvironment_Cache(t *testing.T) {
}
}
func TestEnvironment_Cli_Error(t *testing.T) {
config := DefaultEnvironmentConfig()
config.Components.Command = func(n string) (Command, error) { return nil, errors.New("foo") }
env, _ := NewEnvironment(config)
_, err := env.Cli([]string{"foo"})
if err == nil {
t.Fatal("should have error")
}
if err.Error() != "foo" {
t.Fatalf("bad: %s", err)
}
}
func TestEnvironment_Cli_CallsRun(t *testing.T) {
command := &TestCommand{}
commands := make(map[string]Command)
commands["foo"] = command
config := &EnvironmentConfig{}
config.Commands = []string{"foo"}
config.Components.Command = func(n string) (Command, error) { return commands[n], nil }
env, _ := NewEnvironment(config)
exitCode, err := env.Cli([]string{"foo", "bar", "baz"})
if err != nil {
t.Fatalf("err: %s", err)
}
if exitCode != 0 {
t.Fatalf("bad: %d", exitCode)
}
if !command.runCalled {
t.Fatal("command should be run")
}
if command.runEnv != env {
t.Fatalf("bad env: %#v", command.runEnv)
}
if !reflect.DeepEqual(command.runArgs, []string{"bar", "baz"}) {
t.Fatalf("bad: %#v", command.runArgs)
}
}
func TestEnvironment_DefaultCli_Empty(t *testing.T) {
defaultEnv := testEnvironment()
// Test with no args
exitCode, _ := defaultEnv.Cli([]string{})
if exitCode != 1 {
t.Fatalf("bad: %d", exitCode)
}
// Test with only blank args
exitCode, _ = defaultEnv.Cli([]string{""})
if exitCode != 1 {
t.Fatalf("bad: %d", exitCode)
}
}
func TestEnvironment_DefaultCli_Help(t *testing.T) {
defaultEnv := testEnvironment()
// A little lambda to help us test the output actually contains help
testOutput := func() {
buffer := defaultEnv.Ui().(*BasicUi).Writer.(*bytes.Buffer)
output := buffer.String()
buffer.Reset()
if !strings.Contains(output, "usage: packer") {
t.Fatalf("should contain help: %#v", output)
}
}
// Test "--help"
exitCode, _ := defaultEnv.Cli([]string{"--help"})
if exitCode != 1 {
t.Fatalf("bad: %d", exitCode)
}
testOutput()
// Test "-h"
exitCode, _ = defaultEnv.Cli([]string{"--help"})
if exitCode != 1 {
t.Fatalf("bad: %d", exitCode)
}
testOutput()
}
func TestEnvironment_DefaultCli_Version(t *testing.T) {
defaultEnv := testEnvironment()
versionCommands := []string{"version", "--version", "-v"}
for _, command := range versionCommands {
exitCode, _ := defaultEnv.Cli([]string{command})
if exitCode != 0 {
t.Fatalf("bad: %d", exitCode)
}
// Test the --version and -v can appear anywhere
exitCode, _ = defaultEnv.Cli([]string{"bad", command})
if command != "version" {
if exitCode != 0 {
t.Fatalf("bad: %d", exitCode)
}
} else {
if exitCode != 1 {
t.Fatalf("bad: %d", exitCode)
}
}
}
}
func TestEnvironment_Hook(t *testing.T) {
hook := &MockHook{}
hooks := make(map[string]Hook)

Some files were not shown because too many files have changed in this diff Show More