Merge branch 'master' of https://github.com/mitchellh/packer
Conflicts: provisioner/salt-masterless/provisioner.go
This commit is contained in:
commit
e5c6f1a753
|
@ -7,7 +7,7 @@ go:
|
||||||
|
|
||||||
install: make updatedeps
|
install: make updatedeps
|
||||||
script:
|
script:
|
||||||
- make test
|
- GOMAXPROCS=2 make test
|
||||||
#- go test -race ./...
|
#- go test -race ./...
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
|
|
44
CHANGELOG.md
44
CHANGELOG.md
|
@ -1,15 +1,57 @@
|
||||||
## 0.7.2 (unreleased)
|
## 0.8.0 (unreleased)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 0.7.2 (October 28, 2014)
|
||||||
|
|
||||||
FEATURES:
|
FEATURES:
|
||||||
|
|
||||||
|
* builder/digitalocean: API V2 support. [GH-1463]
|
||||||
* builder/parallels: Don't depend on _prl-utils_ [GH-1499]
|
* 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:
|
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/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: Ignore 'The fdd0 device does not exist' [GH-1501]
|
||||||
* builder/parallels: Rely on Cleanup functions to detach devices [GH-1502]
|
* 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: 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)
|
## 0.7.1 (September 10, 2014)
|
||||||
|
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -15,6 +15,6 @@ testrace:
|
||||||
go test -race $(TEST) $(TESTARGS)
|
go test -race $(TEST) $(TESTARGS)
|
||||||
|
|
||||||
updatedeps:
|
updatedeps:
|
||||||
go get -u -v -p 2 ./...
|
go get -d -v -p 2 ./...
|
||||||
|
|
||||||
.PHONY: bin default test updatedeps
|
.PHONY: bin default test updatedeps
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
# -*- mode: ruby -*-
|
# -*- mode: ruby -*-
|
||||||
# vi: set ft=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
|
$script = <<SCRIPT
|
||||||
SRCROOT="/opt/go"
|
SRCROOT="/opt/go"
|
||||||
|
|
||||||
|
@ -31,7 +28,7 @@ sudo chown -R vagrant:vagrant /opt/gopath
|
||||||
sudo apt-get install -y curl git-core zip
|
sudo apt-get install -y curl git-core zip
|
||||||
SCRIPT
|
SCRIPT
|
||||||
|
|
||||||
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
Vagrant.configure(2) do |config|
|
||||||
config.vm.box = "chef/ubuntu-12.04"
|
config.vm.box = "chef/ubuntu-12.04"
|
||||||
|
|
||||||
config.vm.provision "shell", inline: $script
|
config.vm.provision "shell", inline: $script
|
||||||
|
|
|
@ -26,7 +26,7 @@ func (c *AccessConfig) Auth() (aws.Auth, error) {
|
||||||
c.SecretKey = auth.SecretKey
|
c.SecretKey = auth.SecretKey
|
||||||
c.Token = auth.Token
|
c.Token = auth.Token
|
||||||
}
|
}
|
||||||
if auth.Token == "" && c.Token != "" {
|
if c.Token != "" {
|
||||||
auth.Token = c.Token
|
auth.Token = c.Token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,10 @@ func (a *Artifact) String() string {
|
||||||
return fmt.Sprintf("AMIs were created:\n\n%s", strings.Join(amiStrings, "\n"))
|
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 {
|
func (a *Artifact) Destroy() error {
|
||||||
errors := make([]error, 0)
|
errors := make([]error, 0)
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,9 @@ import (
|
||||||
"github.com/mitchellh/goamz/ec2"
|
"github.com/mitchellh/goamz/ec2"
|
||||||
"github.com/mitchellh/multistep"
|
"github.com/mitchellh/multistep"
|
||||||
"log"
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -38,6 +41,9 @@ func AMIStateRefreshFunc(conn *ec2.EC2, imageId string) StateRefreshFunc {
|
||||||
if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidAMIID.NotFound" {
|
if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidAMIID.NotFound" {
|
||||||
// Set this to nil as if we didn't find anything.
|
// Set this to nil as if we didn't find anything.
|
||||||
resp = nil
|
resp = nil
|
||||||
|
} else if isTransientNetworkError(err) {
|
||||||
|
// Transient network error, treat it as if we didn't find anything
|
||||||
|
resp = nil
|
||||||
} else {
|
} else {
|
||||||
log.Printf("Error on AMIStateRefresh: %s", err)
|
log.Printf("Error on AMIStateRefresh: %s", err)
|
||||||
return nil, "", 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" {
|
if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidInstanceID.NotFound" {
|
||||||
// Set this to nil as if we didn't find anything.
|
// Set this to nil as if we didn't find anything.
|
||||||
resp = nil
|
resp = nil
|
||||||
|
} else if isTransientNetworkError(err) {
|
||||||
|
// Transient network error, treat it as if we didn't find anything
|
||||||
|
resp = nil
|
||||||
} else {
|
} else {
|
||||||
log.Printf("Error on InstanceStateRefresh: %s", err)
|
log.Printf("Error on InstanceStateRefresh: %s", err)
|
||||||
return nil, "", 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" {
|
if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidSpotInstanceRequestID.NotFound" {
|
||||||
// Set this to nil as if we didn't find anything.
|
// Set this to nil as if we didn't find anything.
|
||||||
resp = nil
|
resp = nil
|
||||||
|
} else if isTransientNetworkError(err) {
|
||||||
|
// Transient network error, treat it as if we didn't find anything
|
||||||
|
resp = nil
|
||||||
} else {
|
} else {
|
||||||
log.Printf("Error on SpotRequestStateRefresh: %s", err)
|
log.Printf("Error on SpotRequestStateRefresh: %s", err)
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
|
@ -112,6 +124,8 @@ func SpotRequestStateRefreshFunc(conn *ec2.EC2, spotRequestId string) StateRefre
|
||||||
func WaitForState(conf *StateChangeConf) (i interface{}, err error) {
|
func WaitForState(conf *StateChangeConf) (i interface{}, err error) {
|
||||||
log.Printf("Waiting for state to become: %s", conf.Target)
|
log.Printf("Waiting for state to become: %s", conf.Target)
|
||||||
|
|
||||||
|
sleepSeconds := 2
|
||||||
|
maxTicks := int(TimeoutSeconds()/sleepSeconds) + 1
|
||||||
notfoundTick := 0
|
notfoundTick := 0
|
||||||
|
|
||||||
for {
|
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
|
// If we didn't find the resource, check if we have been
|
||||||
// not finding it for awhile, and if so, report an error.
|
// not finding it for awhile, and if so, report an error.
|
||||||
notfoundTick += 1
|
notfoundTick += 1
|
||||||
if notfoundTick > 20 {
|
if notfoundTick > maxTicks {
|
||||||
return nil, errors.New("couldn't find resource")
|
return nil, errors.New("couldn't find resource")
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -4,36 +4,13 @@
|
||||||
|
|
||||||
package digitalocean
|
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 {
|
type Region struct {
|
||||||
Id uint
|
Id uint `json:"id,omitempty"` //only in v1 api
|
||||||
Name string
|
Slug string `json:"slug"` //presen in both api
|
||||||
Slug string
|
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 {
|
type RegionsResp struct {
|
||||||
|
@ -41,378 +18,51 @@ type RegionsResp struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Size struct {
|
type Size struct {
|
||||||
Id uint
|
Id uint `json:"id,omitempty"` //only in v1 api
|
||||||
Name string
|
Name string `json:"name,omitempty"` //only in v1 api
|
||||||
Slug string
|
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 {
|
type SizesResp struct {
|
||||||
Sizes []Size
|
Sizes []Size
|
||||||
}
|
}
|
||||||
|
|
||||||
type DigitalOceanClient struct {
|
type Image struct {
|
||||||
// The http client for communicating
|
Id uint `json:"id"` //presen in both api
|
||||||
client *http.Client
|
Name string `json:"name"` //presen in both api
|
||||||
|
Slug string `json:"slug"` //presen in both api
|
||||||
// Credentials
|
Distribution string `json:"distribution"` //presen in both api
|
||||||
ClientID string
|
Public bool `json:"public,omitempty"` //only in v2 api
|
||||||
APIKey string
|
Regions []string `json:"regions,omitempty"` //only in v2 api
|
||||||
|
ActionIds []string `json:"action_ids,omitempty"` //only in v2 api
|
||||||
// The base URL of the API
|
CreatedAt string `json:"created_at,omitempty"` //only in v2 api
|
||||||
APIURL string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new client for communicating with DO
|
type ImagesResp struct {
|
||||||
func (d DigitalOceanClient) New(client string, key string, url string) *DigitalOceanClient {
|
Images []Image
|
||||||
c := &DigitalOceanClient{
|
|
||||||
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
|
type DigitalOceanClient interface {
|
||||||
func (d DigitalOceanClient) CreateKey(name string, pub string) (uint, error) {
|
CreateKey(string, string) (uint, error)
|
||||||
params := url.Values{}
|
DestroyKey(uint) error
|
||||||
params.Set("name", name)
|
CreateDroplet(string, string, string, string, uint, bool) (uint, error)
|
||||||
params.Set("ssh_pub_key", pub)
|
DestroyDroplet(uint) error
|
||||||
|
PowerOffDroplet(uint) error
|
||||||
body, err := NewRequest(d, "ssh_keys/new", params)
|
ShutdownDroplet(uint) error
|
||||||
if err != nil {
|
CreateSnapshot(uint, string) error
|
||||||
return 0, err
|
Images() ([]Image, error)
|
||||||
}
|
DestroyImage(uint) error
|
||||||
|
DropletStatus(uint) (string, string, error)
|
||||||
// Read the SSH key's ID we just created
|
Image(string) (Image, error)
|
||||||
key := body["ssh_key"].(map[string]interface{})
|
Regions() ([]Region, error)
|
||||||
keyId := key["id"].(float64)
|
Region(string) (Region, error)
|
||||||
return uint(keyId), nil
|
Sizes() ([]Size, error)
|
||||||
}
|
Size(string) (Size, error)
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -16,7 +16,7 @@ type Artifact struct {
|
||||||
regionName string
|
regionName string
|
||||||
|
|
||||||
// The client for making API calls
|
// The client for making API calls
|
||||||
client *DigitalOceanClient
|
client DigitalOceanClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*Artifact) BuilderId() string {
|
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)
|
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 {
|
func (a *Artifact) Destroy() error {
|
||||||
log.Printf("Destroying image: %d (%s)", a.snapshotId, a.snapshotName)
|
log.Printf("Destroying image: %d (%s)", a.snapshotId, a.snapshotName)
|
||||||
return a.client.DestroyImage(a.snapshotId)
|
return a.client.DestroyImage(a.snapshotId)
|
||||||
|
|
|
@ -40,6 +40,7 @@ type config struct {
|
||||||
ClientID string `mapstructure:"client_id"`
|
ClientID string `mapstructure:"client_id"`
|
||||||
APIKey string `mapstructure:"api_key"`
|
APIKey string `mapstructure:"api_key"`
|
||||||
APIURL string `mapstructure:"api_url"`
|
APIURL string `mapstructure:"api_url"`
|
||||||
|
APIToken string `mapstructure:"api_token"`
|
||||||
RegionID uint `mapstructure:"region_id"`
|
RegionID uint `mapstructure:"region_id"`
|
||||||
SizeID uint `mapstructure:"size_id"`
|
SizeID uint `mapstructure:"size_id"`
|
||||||
ImageID uint `mapstructure:"image_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")
|
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.Region == "" {
|
||||||
if b.config.RegionID != 0 {
|
if b.config.RegionID != 0 {
|
||||||
b.config.Region = fmt.Sprintf("%v", b.config.RegionID)
|
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,
|
"client_id": &b.config.ClientID,
|
||||||
"api_key": &b.config.APIKey,
|
"api_key": &b.config.APIKey,
|
||||||
"api_url": &b.config.APIURL,
|
"api_url": &b.config.APIURL,
|
||||||
|
"api_token": &b.config.APIToken,
|
||||||
"snapshot_name": &b.config.SnapshotName,
|
"snapshot_name": &b.config.SnapshotName,
|
||||||
"droplet_name": &b.config.DropletName,
|
"droplet_name": &b.config.DropletName,
|
||||||
"ssh_username": &b.config.SSHUsername,
|
"ssh_username": &b.config.SSHUsername,
|
||||||
|
@ -180,19 +187,21 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if b.config.APIToken == "" {
|
||||||
// Required configurations that will display errors if not set
|
// Required configurations that will display errors if not set
|
||||||
if b.config.ClientID == "" {
|
if b.config.ClientID == "" {
|
||||||
errs = packer.MultiErrorAppend(
|
errs = packer.MultiErrorAppend(
|
||||||
errs, errors.New("a client_id must be specified"))
|
errs, errors.New("a client_id 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 == "" {
|
if b.config.APIKey == "" {
|
||||||
errs = packer.MultiErrorAppend(
|
errs = packer.MultiErrorAppend(
|
||||||
errs, errors.New("an api_key must be specified"))
|
errs, errors.New("a api_key for v1 auth or api_token for v2 auth must be specified"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.APIURL == "" {
|
||||||
|
b.config.APIURL = "https://api.digitalocean.com"
|
||||||
}
|
}
|
||||||
|
|
||||||
sshTimeout, err := time.ParseDuration(b.config.RawSSHTimeout)
|
sshTimeout, err := time.ParseDuration(b.config.RawSSHTimeout)
|
||||||
|
@ -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) {
|
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
|
||||||
|
var client DigitalOceanClient
|
||||||
// Initialize the DO API client
|
// 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
|
// Set up the state
|
||||||
state := new(multistep.BasicStateBag)
|
state := new(multistep.BasicStateBag)
|
||||||
|
|
|
@ -12,7 +12,7 @@ type stepCreateDroplet struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *stepCreateDroplet) Run(state multistep.StateBag) multistep.StepAction {
|
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)
|
ui := state.Get("ui").(packer.Ui)
|
||||||
c := state.Get("config").(config)
|
c := state.Get("config").(config)
|
||||||
sshKeyId := state.Get("ssh_key_id").(uint)
|
sshKeyId := state.Get("ssh_key_id").(uint)
|
||||||
|
@ -44,7 +44,7 @@ func (s *stepCreateDroplet) Cleanup(state multistep.StateBag) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
client := state.Get("client").(*DigitalOceanClient)
|
client := state.Get("client").(DigitalOceanClient)
|
||||||
ui := state.Get("ui").(packer.Ui)
|
ui := state.Get("ui").(packer.Ui)
|
||||||
c := state.Get("config").(config)
|
c := state.Get("config").(config)
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ type stepCreateSSHKey struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction {
|
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 := state.Get("ui").(packer.Ui)
|
||||||
|
|
||||||
ui.Say("Creating temporary ssh key for droplet...")
|
ui.Say("Creating temporary ssh key for droplet...")
|
||||||
|
@ -71,15 +71,14 @@ func (s *stepCreateSSHKey) Cleanup(state multistep.StateBag) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
client := state.Get("client").(*DigitalOceanClient)
|
client := state.Get("client").(DigitalOceanClient)
|
||||||
ui := state.Get("ui").(packer.Ui)
|
ui := state.Get("ui").(packer.Ui)
|
||||||
c := state.Get("config").(config)
|
c := state.Get("config").(config)
|
||||||
|
|
||||||
ui.Say("Deleting temporary ssh key...")
|
ui.Say("Deleting temporary ssh key...")
|
||||||
err := client.DestroyKey(s.keyId)
|
err := client.DestroyKey(s.keyId)
|
||||||
|
|
||||||
curlstr := fmt.Sprintf("curl '%v/ssh_keys/%v/destroy?client_id=%v&api_key=%v'",
|
curlstr := fmt.Sprintf("curl -H 'Authorization: Bearer #TOKEN#' -X DELETE '%v/v2/account/keys/%v'", c.APIURL, s.keyId)
|
||||||
c.APIURL, s.keyId, c.ClientID, c.APIKey)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error cleaning up ssh key: %v", err.Error())
|
log.Printf("Error cleaning up ssh key: %v", err.Error())
|
||||||
|
|
|
@ -2,6 +2,7 @@ package digitalocean
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/mitchellh/multistep"
|
"github.com/mitchellh/multistep"
|
||||||
"github.com/mitchellh/packer/packer"
|
"github.com/mitchellh/packer/packer"
|
||||||
)
|
)
|
||||||
|
@ -9,7 +10,7 @@ import (
|
||||||
type stepDropletInfo struct{}
|
type stepDropletInfo struct{}
|
||||||
|
|
||||||
func (s *stepDropletInfo) Run(state multistep.StateBag) multistep.StepAction {
|
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)
|
ui := state.Get("ui").(packer.Ui)
|
||||||
c := state.Get("config").(config)
|
c := state.Get("config").(config)
|
||||||
dropletId := state.Get("droplet_id").(uint)
|
dropletId := state.Get("droplet_id").(uint)
|
||||||
|
|
|
@ -2,15 +2,16 @@ package digitalocean
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
"github.com/mitchellh/multistep"
|
"github.com/mitchellh/multistep"
|
||||||
"github.com/mitchellh/packer/packer"
|
"github.com/mitchellh/packer/packer"
|
||||||
"log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type stepPowerOff struct{}
|
type stepPowerOff struct{}
|
||||||
|
|
||||||
func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction {
|
func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
client := state.Get("client").(*DigitalOceanClient)
|
client := state.Get("client").(DigitalOceanClient)
|
||||||
c := state.Get("config").(config)
|
c := state.Get("config").(config)
|
||||||
ui := state.Get("ui").(packer.Ui)
|
ui := state.Get("ui").(packer.Ui)
|
||||||
dropletId := state.Get("droplet_id").(uint)
|
dropletId := state.Get("droplet_id").(uint)
|
||||||
|
|
|
@ -2,16 +2,17 @@ package digitalocean
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/mitchellh/multistep"
|
|
||||||
"github.com/mitchellh/packer/packer"
|
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/mitchellh/multistep"
|
||||||
|
"github.com/mitchellh/packer/packer"
|
||||||
)
|
)
|
||||||
|
|
||||||
type stepShutdown struct{}
|
type stepShutdown struct{}
|
||||||
|
|
||||||
func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction {
|
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)
|
ui := state.Get("ui").(packer.Ui)
|
||||||
dropletId := state.Get("droplet_id").(uint)
|
dropletId := state.Get("droplet_id").(uint)
|
||||||
|
|
||||||
|
|
|
@ -3,15 +3,16 @@ package digitalocean
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
"github.com/mitchellh/multistep"
|
"github.com/mitchellh/multistep"
|
||||||
"github.com/mitchellh/packer/packer"
|
"github.com/mitchellh/packer/packer"
|
||||||
"log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type stepSnapshot struct{}
|
type stepSnapshot struct{}
|
||||||
|
|
||||||
func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
|
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)
|
ui := state.Get("ui").(packer.Ui)
|
||||||
c := state.Get("config").(config)
|
c := state.Get("config").(config)
|
||||||
dropletId := state.Get("droplet_id").(uint)
|
dropletId := state.Get("droplet_id").(uint)
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
|
|
||||||
// waitForState simply blocks until the droplet is in
|
// waitForState simply blocks until the droplet is in
|
||||||
// a state we expect, while eventually timing out.
|
// 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{})
|
done := make(chan struct{})
|
||||||
defer close(done)
|
defer close(done)
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,10 @@ func (a *ExportArtifact) String() string {
|
||||||
return fmt.Sprintf("Exported Docker file: %s", a.path)
|
return fmt.Sprintf("Exported Docker file: %s", a.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *ExportArtifact) State(name string) interface{} {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *ExportArtifact) Destroy() error {
|
func (a *ExportArtifact) Destroy() error {
|
||||||
return os.Remove(a.path)
|
return os.Remove(a.path)
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,10 @@ func (a *ImportArtifact) String() string {
|
||||||
return fmt.Sprintf("Imported Docker image: %s", a.Id())
|
return fmt.Sprintf("Imported Docker image: %s", a.Id())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (*ImportArtifact) State(name string) interface{} {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *ImportArtifact) Destroy() error {
|
func (a *ImportArtifact) Destroy() error {
|
||||||
return a.Driver.DeleteImage(a.Id())
|
return a.Driver.DeleteImage(a.Id())
|
||||||
}
|
}
|
||||||
|
|
|
@ -258,10 +258,5 @@ func (d *DockerDriver) Verify() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if v := os.Getenv("DOCKER_HOST"); v != "" {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"DOCKER_HOST cannot be set with the Packer Docker builder.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,3 +37,7 @@ func (a *Artifact) Id() string {
|
||||||
func (a *Artifact) String() string {
|
func (a *Artifact) String() string {
|
||||||
return fmt.Sprintf("A disk image was created: %v", a.imageName)
|
return fmt.Sprintf("A disk image was created: %v", a.imageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Artifact) State(name string) interface{} {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -24,6 +24,25 @@ func (config *Config) getImage() Image {
|
||||||
return Image{Name: config.SourceImage, ProjectId: project}
|
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.
|
// Run executes the Packer build step that creates a GCE instance.
|
||||||
func (s *StepCreateInstance) Run(state multistep.StateBag) multistep.StepAction {
|
func (s *StepCreateInstance) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
config := state.Get("config").(*Config)
|
config := state.Get("config").(*Config)
|
||||||
|
@ -39,9 +58,7 @@ func (s *StepCreateInstance) Run(state multistep.StateBag) multistep.StepAction
|
||||||
DiskSizeGb: config.DiskSizeGb,
|
DiskSizeGb: config.DiskSizeGb,
|
||||||
Image: config.getImage(),
|
Image: config.getImage(),
|
||||||
MachineType: config.MachineType,
|
MachineType: config.MachineType,
|
||||||
Metadata: map[string]string{
|
Metadata: config.getInstanceMetadata(sshPublicKey),
|
||||||
"sshKeys": fmt.Sprintf("%s:%s", config.SSHUsername, sshPublicKey),
|
|
||||||
},
|
|
||||||
Name: name,
|
Name: name,
|
||||||
Network: config.Network,
|
Network: config.Network,
|
||||||
Tags: config.Tags,
|
Tags: config.Tags,
|
||||||
|
|
|
@ -24,6 +24,10 @@ func (a *NullArtifact) String() string {
|
||||||
return fmt.Sprintf("Did not export anything. This is the null builder")
|
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 {
|
func (a *NullArtifact) Destroy() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,12 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/mitchellh/packer/common"
|
"github.com/mitchellh/packer/common"
|
||||||
"github.com/mitchellh/packer/packer"
|
"github.com/mitchellh/packer/packer"
|
||||||
"github.com/rackspace/gophercloud"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AccessConfig is for common configuration related to openstack access
|
// AccessConfig is for common configuration related to openstack access
|
||||||
|
|
|
@ -2,8 +2,9 @@ package openstack
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/rackspace/gophercloud"
|
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
|
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Artifact is an artifact implementation that contains built images.
|
// 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)
|
return fmt.Sprintf("An image was created: %v", a.ImageId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Artifact) State(name string) interface{} {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Artifact) Destroy() error {
|
func (a *Artifact) Destroy() error {
|
||||||
log.Printf("Destroying image: %d", a.ImageId)
|
log.Printf("Destroying image: %d", a.ImageId)
|
||||||
return a.Conn.DeleteImageById(a.ImageId)
|
return a.Conn.DeleteImageById(a.ImageId)
|
||||||
|
|
|
@ -8,8 +8,9 @@ import (
|
||||||
"github.com/mitchellh/multistep"
|
"github.com/mitchellh/multistep"
|
||||||
"github.com/mitchellh/packer/common"
|
"github.com/mitchellh/packer/common"
|
||||||
"github.com/mitchellh/packer/packer"
|
"github.com/mitchellh/packer/packer"
|
||||||
"github.com/rackspace/gophercloud"
|
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
|
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The unique ID for this builder
|
// The unique ID for this builder
|
||||||
|
|
|
@ -5,9 +5,10 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/mitchellh/multistep"
|
"github.com/mitchellh/multistep"
|
||||||
"github.com/racker/perigee"
|
"github.com/racker/perigee"
|
||||||
"github.com/rackspace/gophercloud"
|
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||||
)
|
)
|
||||||
|
|
||||||
// StateRefreshFunc is a function type used for StateChangeConf that is
|
// StateRefreshFunc is a function type used for StateChangeConf that is
|
||||||
|
|
|
@ -5,8 +5,9 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/mitchellh/multistep"
|
"github.com/mitchellh/multistep"
|
||||||
"github.com/rackspace/gophercloud"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SSHAddress returns a function that can be given to the SSH communicator
|
// SSHAddress returns a function that can be given to the SSH communicator
|
||||||
|
|
|
@ -4,7 +4,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/mitchellh/multistep"
|
"github.com/mitchellh/multistep"
|
||||||
"github.com/mitchellh/packer/packer"
|
"github.com/mitchellh/packer/packer"
|
||||||
"github.com/rackspace/gophercloud"
|
|
||||||
|
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StepAllocateIp struct {
|
type StepAllocateIp struct {
|
||||||
|
|
|
@ -4,9 +4,10 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/mitchellh/multistep"
|
"github.com/mitchellh/multistep"
|
||||||
"github.com/mitchellh/packer/packer"
|
"github.com/mitchellh/packer/packer"
|
||||||
"github.com/rackspace/gophercloud"
|
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||||
)
|
)
|
||||||
|
|
||||||
type stepCreateImage struct{}
|
type stepCreateImage struct{}
|
||||||
|
|
|
@ -5,10 +5,11 @@ import (
|
||||||
"github.com/mitchellh/multistep"
|
"github.com/mitchellh/multistep"
|
||||||
"github.com/mitchellh/packer/common/uuid"
|
"github.com/mitchellh/packer/common/uuid"
|
||||||
"github.com/mitchellh/packer/packer"
|
"github.com/mitchellh/packer/packer"
|
||||||
"github.com/rackspace/gophercloud"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StepKeyPair struct {
|
type StepKeyPair struct {
|
||||||
|
|
|
@ -4,8 +4,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/mitchellh/multistep"
|
"github.com/mitchellh/multistep"
|
||||||
"github.com/mitchellh/packer/packer"
|
"github.com/mitchellh/packer/packer"
|
||||||
"github.com/rackspace/gophercloud"
|
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
|
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StepRunSourceServer struct {
|
type StepRunSourceServer struct {
|
||||||
|
|
|
@ -66,6 +66,10 @@ func (a *artifact) String() string {
|
||||||
return fmt.Sprintf("VM files in directory: %s", a.dir)
|
return fmt.Sprintf("VM files in directory: %s", a.dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *artifact) State(name string) interface{} {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *artifact) Destroy() error {
|
func (a *artifact) Destroy() error {
|
||||||
return os.RemoveAll(a.dir)
|
return os.RemoveAll(a.dir)
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,7 +81,7 @@ func (s *stepAttachISO) Cleanup(state multistep.StateBag) {
|
||||||
log.Println("Enabling default CD/DVD drive...")
|
log.Println("Enabling default CD/DVD drive...")
|
||||||
command := []string{
|
command := []string{
|
||||||
"set", vmName,
|
"set", vmName,
|
||||||
"--device-set", "cdrom0", "--enable",
|
"--device-set", "cdrom0", "--enable", "--disconnect",
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := driver.Prlctl(command...); err != nil {
|
if err := driver.Prlctl(command...); err != nil {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
type Artifact struct {
|
type Artifact struct {
|
||||||
dir string
|
dir string
|
||||||
f []string
|
f []string
|
||||||
|
state map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*Artifact) BuilderId() string {
|
func (*Artifact) BuilderId() string {
|
||||||
|
@ -28,6 +29,10 @@ func (a *Artifact) String() string {
|
||||||
return fmt.Sprintf("VM files in directory: %s", a.dir)
|
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 {
|
func (a *Artifact) Destroy() error {
|
||||||
return os.RemoveAll(a.dir)
|
return os.RemoveAll(a.dir)
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,14 @@ var diskInterface = map[string]bool{
|
||||||
"virtio": true,
|
"virtio": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var diskCache = map[string]bool{
|
||||||
|
"writethrough": true,
|
||||||
|
"writeback": true,
|
||||||
|
"none": true,
|
||||||
|
"unsafe": true,
|
||||||
|
"directsync": true,
|
||||||
|
}
|
||||||
|
|
||||||
type Builder struct {
|
type Builder struct {
|
||||||
config config
|
config config
|
||||||
runner multistep.Runner
|
runner multistep.Runner
|
||||||
|
@ -68,9 +76,11 @@ type config struct {
|
||||||
BootCommand []string `mapstructure:"boot_command"`
|
BootCommand []string `mapstructure:"boot_command"`
|
||||||
DiskInterface string `mapstructure:"disk_interface"`
|
DiskInterface string `mapstructure:"disk_interface"`
|
||||||
DiskSize uint `mapstructure:"disk_size"`
|
DiskSize uint `mapstructure:"disk_size"`
|
||||||
|
DiskCache string `mapstructure:"disk_cache"`
|
||||||
FloppyFiles []string `mapstructure:"floppy_files"`
|
FloppyFiles []string `mapstructure:"floppy_files"`
|
||||||
Format string `mapstructure:"format"`
|
Format string `mapstructure:"format"`
|
||||||
Headless bool `mapstructure:"headless"`
|
Headless bool `mapstructure:"headless"`
|
||||||
|
DiskImage bool `mapstructure:"disk_image"`
|
||||||
HTTPDir string `mapstructure:"http_directory"`
|
HTTPDir string `mapstructure:"http_directory"`
|
||||||
HTTPPortMin uint `mapstructure:"http_port_min"`
|
HTTPPortMin uint `mapstructure:"http_port_min"`
|
||||||
HTTPPortMax uint `mapstructure:"http_port_max"`
|
HTTPPortMax uint `mapstructure:"http_port_max"`
|
||||||
|
@ -126,6 +136,10 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
||||||
b.config.DiskSize = 40000
|
b.config.DiskSize = 40000
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if b.config.DiskCache == "" {
|
||||||
|
b.config.DiskCache = "writeback"
|
||||||
|
}
|
||||||
|
|
||||||
if b.config.Accelerator == "" {
|
if b.config.Accelerator == "" {
|
||||||
b.config.Accelerator = "kvm"
|
b.config.Accelerator = "kvm"
|
||||||
}
|
}
|
||||||
|
@ -139,7 +153,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.config.MachineType == "" {
|
if b.config.MachineType == "" {
|
||||||
b.config.MachineType = "pc-1.0"
|
b.config.MachineType = "pc"
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.config.OutputDir == "" {
|
if b.config.OutputDir == "" {
|
||||||
|
@ -280,6 +294,11 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
||||||
errs, errors.New("unrecognized disk interface type"))
|
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 {
|
if b.config.HTTPPortMin > b.config.HTTPPortMax {
|
||||||
errs = packer.MultiErrorAppend(
|
errs = packer.MultiErrorAppend(
|
||||||
errs, errors.New("http_port_min must be less than http_port_max"))
|
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,
|
Files: b.config.FloppyFiles,
|
||||||
},
|
},
|
||||||
new(stepCreateDisk),
|
new(stepCreateDisk),
|
||||||
|
new(stepCopyDisk),
|
||||||
|
new(stepResizeDisk),
|
||||||
new(stepHTTPServer),
|
new(stepHTTPServer),
|
||||||
new(stepForwardSSH),
|
new(stepForwardSSH),
|
||||||
new(stepConfigureVNC),
|
new(stepConfigureVNC),
|
||||||
|
@ -481,8 +502,14 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||||
artifact := &Artifact{
|
artifact := &Artifact{
|
||||||
dir: b.config.OutputDir,
|
dir: b.config.OutputDir,
|
||||||
f: files,
|
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
|
return artifact, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {}
|
|
@ -16,8 +16,8 @@ func (s *stepCreateDisk) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
config := state.Get("config").(*config)
|
config := state.Get("config").(*config)
|
||||||
driver := state.Get("driver").(Driver)
|
driver := state.Get("driver").(Driver)
|
||||||
ui := state.Get("ui").(packer.Ui)
|
ui := state.Get("ui").(packer.Ui)
|
||||||
path := filepath.Join(config.OutputDir, fmt.Sprintf("%s.%s", config.VMName,
|
name := config.VMName + "." + strings.ToLower(config.Format)
|
||||||
strings.ToLower(config.Format)))
|
path := filepath.Join(config.OutputDir, name)
|
||||||
|
|
||||||
command := []string{
|
command := []string{
|
||||||
"create",
|
"create",
|
||||||
|
@ -26,6 +26,10 @@ func (s *stepCreateDisk) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
fmt.Sprintf("%vM", config.DiskSize),
|
fmt.Sprintf("%vM", config.DiskSize),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.DiskImage == true {
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
ui.Say("Creating hard drive...")
|
ui.Say("Creating hard drive...")
|
||||||
if err := driver.QemuImg(command...); err != nil {
|
if err := driver.QemuImg(command...); err != nil {
|
||||||
err := fmt.Errorf("Error creating hard drive: %s", err)
|
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
|
return multistep.ActionHalt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state.Put("disk_filename", name)
|
||||||
|
|
||||||
return multistep.ActionContinue
|
return multistep.ActionContinue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,12 @@ package qemu
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/mitchellh/multistep"
|
|
||||||
"github.com/mitchellh/packer/packer"
|
|
||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
|
"github.com/mitchellh/multistep"
|
||||||
|
"github.com/mitchellh/packer/packer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This step adds a NAT port forwarding definition so that SSH is available
|
// 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)
|
log.Printf("Looking for available SSH port between %d and %d", config.SSHHostPortMin, config.SSHHostPortMax)
|
||||||
var sshHostPort uint
|
var sshHostPort uint
|
||||||
|
var offset uint = 0
|
||||||
|
|
||||||
portRange := int(config.SSHHostPortMax - config.SSHHostPortMin)
|
portRange := int(config.SSHHostPortMax - config.SSHHostPortMin)
|
||||||
|
if portRange > 0 {
|
||||||
|
// Have to check if > 0 to avoid a panic
|
||||||
|
offset = uint(rand.Intn(portRange))
|
||||||
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
sshHostPort = uint(rand.Intn(portRange)) + config.SSHHostPortMin
|
sshHostPort = offset + config.SSHHostPortMin
|
||||||
log.Printf("Trying port: %d", sshHostPort)
|
log.Printf("Trying port: %d", sshHostPort)
|
||||||
l, err := net.Listen("tcp", fmt.Sprintf(":%d", sshHostPort))
|
l, err := net.Listen("tcp", fmt.Sprintf(":%d", sshHostPort))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
|
@ -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) {}
|
|
@ -2,11 +2,12 @@ package qemu
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/mitchellh/multistep"
|
|
||||||
"github.com/mitchellh/packer/packer"
|
|
||||||
"log"
|
"log"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mitchellh/multistep"
|
||||||
|
"github.com/mitchellh/packer/packer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// stepRun runs the virtual machine
|
// stepRun runs the virtual machine
|
||||||
|
@ -78,13 +79,12 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error
|
||||||
|
|
||||||
defaultArgs["-name"] = vmName
|
defaultArgs["-name"] = vmName
|
||||||
defaultArgs["-machine"] = fmt.Sprintf("type=%s", config.MachineType)
|
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["-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["-cdrom"] = isoPath
|
||||||
defaultArgs["-boot"] = bootDrive
|
defaultArgs["-boot"] = bootDrive
|
||||||
defaultArgs["-m"] = "512M"
|
defaultArgs["-m"] = "512M"
|
||||||
defaultArgs["-redir"] = fmt.Sprintf("tcp:%v::22", sshHostPort)
|
|
||||||
defaultArgs["-vnc"] = vnc
|
defaultArgs["-vnc"] = vnc
|
||||||
|
|
||||||
// Append the accelerator to the machine type if it is specified
|
// Append the accelerator to the machine type if it is specified
|
||||||
|
|
|
@ -56,6 +56,10 @@ func (a *artifact) String() string {
|
||||||
return fmt.Sprintf("VM files in directory: %s", a.dir)
|
return fmt.Sprintf("VM files in directory: %s", a.dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *artifact) State(name string) interface{} {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *artifact) Destroy() error {
|
func (a *artifact) Destroy() error {
|
||||||
return os.RemoveAll(a.dir)
|
return os.RemoveAll(a.dir)
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,11 +49,12 @@ func (d *VBox42Driver) Iso() (string, error) {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
DefaultGuestAdditionsRe := regexp.MustCompile("Default Guest Additions ISO:(.*)")
|
DefaultGuestAdditionsRe := regexp.MustCompile("Default Guest Additions ISO:(.+)")
|
||||||
|
|
||||||
for _, line := range strings.Split(stdout.String(), "\n") {
|
for _, line := range strings.Split(stdout.String(), "\n") {
|
||||||
// Need to trim off CR character when running in windows
|
// 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)
|
matches := DefaultGuestAdditionsRe.FindStringSubmatch(line)
|
||||||
if matches == nil {
|
if matches == nil {
|
||||||
|
@ -66,7 +67,7 @@ func (d *VBox42Driver) Iso() (string, error) {
|
||||||
return isoname, nil
|
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 {
|
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)
|
return "", fmt.Errorf("VirtualBox is not properly setup: %s", versionOutput)
|
||||||
}
|
}
|
||||||
|
|
||||||
versionRe := regexp.MustCompile("^[.0-9]+(?:_RC[0-9]+)?")
|
versionRe := regexp.MustCompile("^([.0-9]+)(?:_(?:RC|OSEr)[0-9]+)?")
|
||||||
matches := versionRe.FindAllString(versionOutput, 1)
|
matches := versionRe.FindAllStringSubmatch(versionOutput, 1)
|
||||||
if matches == nil {
|
if matches == nil || len(matches[0]) != 2 {
|
||||||
return "", fmt.Errorf("No version found: %s", versionOutput)
|
return "", fmt.Errorf("No version found: %s", versionOutput)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("VirtualBox version: %s", matches[0])
|
log.Printf("VirtualBox version: %s", matches[0][1])
|
||||||
return matches[0], nil
|
return matches[0][1], nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,10 @@ func (a *localArtifact) String() string {
|
||||||
return fmt.Sprintf("VM files in directory: %s", a.dir)
|
return fmt.Sprintf("VM files in directory: %s", a.dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *localArtifact) State(name string) interface{} {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *localArtifact) Destroy() error {
|
func (a *localArtifact) Destroy() error {
|
||||||
return os.RemoveAll(a.dir)
|
return os.RemoveAll(a.dir)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/mitchellh/packer/packer"
|
"github.com/mitchellh/packer/packer"
|
||||||
)
|
)
|
||||||
|
@ -11,6 +12,9 @@ type DriverConfig struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DriverConfig) Prepare(t *packer.ConfigTemplate) []error {
|
func (c *DriverConfig) Prepare(t *packer.ConfigTemplate) []error {
|
||||||
|
if c.FusionAppPath == "" {
|
||||||
|
c.FusionAppPath = os.Getenv("FUSION_APP_PATH")
|
||||||
|
}
|
||||||
if c.FusionAppPath == "" {
|
if c.FusionAppPath == "" {
|
||||||
c.FusionAppPath = "/Applications/VMware Fusion.app"
|
c.FusionAppPath = "/Applications/VMware Fusion.app"
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ func (d *Fusion6Driver) Clone(dst, src string) error {
|
||||||
if _, _, err := runAndLog(cmd); err != nil {
|
if _, _, err := runAndLog(cmd); err != nil {
|
||||||
if strings.Contains(err.Error(), "parameters was invalid") {
|
if strings.Contains(err.Error(), "parameters was invalid") {
|
||||||
return fmt.Errorf(
|
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)
|
"only works with Fusion %s Professional or above. Please verify your version.", VMWARE_FUSION_VERSION)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,6 @@ func (s StepCleanVMX) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
return multistep.ActionHalt
|
return multistep.ActionHalt
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := state.GetOk("floppy_path"); ok {
|
|
||||||
// Delete the floppy0 entries so the floppy is no longer mounted
|
// Delete the floppy0 entries so the floppy is no longer mounted
|
||||||
ui.Message("Unmounting floppy from VMX...")
|
ui.Message("Unmounting floppy from VMX...")
|
||||||
for k, _ := range vmxData {
|
for k, _ := range vmxData {
|
||||||
|
@ -42,28 +41,18 @@ func (s StepCleanVMX) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
vmxData["floppy0.present"] = "FALSE"
|
vmxData["floppy0.present"] = "FALSE"
|
||||||
}
|
|
||||||
|
|
||||||
if isoPathRaw, ok := state.GetOk("iso_path"); ok {
|
|
||||||
isoPath := isoPathRaw.(string)
|
|
||||||
|
|
||||||
ui.Message("Detaching ISO from CD-ROM device...")
|
|
||||||
devRe := regexp.MustCompile(`^ide\d:\d\.`)
|
devRe := regexp.MustCompile(`^ide\d:\d\.`)
|
||||||
for k, _ := range vmxData {
|
for k, v := range vmxData {
|
||||||
match := devRe.FindString(k)
|
ide := devRe.FindString(k)
|
||||||
if match == "" {
|
if ide == "" || v != "cdrom-image" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
filenameKey := match + "filename"
|
ui.Message("Detaching ISO from CD-ROM device...")
|
||||||
if filename, ok := vmxData[filenameKey]; ok {
|
|
||||||
if filename == isoPath {
|
vmxData[ide+"devicetype"] = "cdrom-raw"
|
||||||
// Change the CD-ROM device back to auto-detect to eject
|
vmxData[ide+"filename"] = "auto detect"
|
||||||
vmxData[filenameKey] = "auto detect"
|
|
||||||
vmxData[match+"devicetype"] = "cdrom-raw"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rewrite the VMX
|
// Rewrite the VMX
|
||||||
|
|
|
@ -39,7 +39,6 @@ func TestStepCleanVMX_floppyPath(t *testing.T) {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
state.Put("floppy_path", "foo")
|
|
||||||
state.Put("vmx_path", vmxPath)
|
state.Put("vmx_path", vmxPath)
|
||||||
|
|
||||||
// Test the run
|
// Test the run
|
||||||
|
@ -89,7 +88,6 @@ func TestStepCleanVMX_isoPath(t *testing.T) {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
state.Put("iso_path", "foo")
|
|
||||||
state.Put("vmx_path", vmxPath)
|
state.Put("vmx_path", vmxPath)
|
||||||
|
|
||||||
// Test the run
|
// Test the run
|
||||||
|
@ -136,6 +134,7 @@ floppy0.filetype = "file"
|
||||||
`
|
`
|
||||||
|
|
||||||
const testVMXISOPath = `
|
const testVMXISOPath = `
|
||||||
|
ide0:0.devicetype = "cdrom-image"
|
||||||
ide0:0.filename = "foo"
|
ide0:0.filename = "foo"
|
||||||
ide0:1.filename = "bar"
|
ide0:1.filename = "bar"
|
||||||
foo = "bar"
|
foo = "bar"
|
||||||
|
|
|
@ -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
|
// 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
|
// process is exiting. Ubuntu will yield control of the files but
|
||||||
// nice to find a better solution to this.
|
// 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)
|
time.Sleep(5 * time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,10 @@ func (a *Artifact) String() string {
|
||||||
return fmt.Sprintf("VM files in directory: %s", a.dir)
|
return fmt.Sprintf("VM files in directory: %s", a.dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Artifact) State(name string) interface{} {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Artifact) Destroy() error {
|
func (a *Artifact) Destroy() error {
|
||||||
return a.dir.RemoveAll()
|
return a.dir.RemoveAll()
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,7 @@ type config struct {
|
||||||
ISOChecksum string `mapstructure:"iso_checksum"`
|
ISOChecksum string `mapstructure:"iso_checksum"`
|
||||||
ISOChecksumType string `mapstructure:"iso_checksum_type"`
|
ISOChecksumType string `mapstructure:"iso_checksum_type"`
|
||||||
ISOUrls []string `mapstructure:"iso_urls"`
|
ISOUrls []string `mapstructure:"iso_urls"`
|
||||||
|
Version string `mapstructure:"version"`
|
||||||
VMName string `mapstructure:"vm_name"`
|
VMName string `mapstructure:"vm_name"`
|
||||||
BootCommand []string `mapstructure:"boot_command"`
|
BootCommand []string `mapstructure:"boot_command"`
|
||||||
SkipCompaction bool `mapstructure:"skip_compaction"`
|
SkipCompaction bool `mapstructure:"skip_compaction"`
|
||||||
|
@ -47,6 +48,8 @@ type config struct {
|
||||||
|
|
||||||
RemoteType string `mapstructure:"remote_type"`
|
RemoteType string `mapstructure:"remote_type"`
|
||||||
RemoteDatastore string `mapstructure:"remote_datastore"`
|
RemoteDatastore string `mapstructure:"remote_datastore"`
|
||||||
|
RemoteCacheDatastore string `mapstructure:"remote_cache_datastore"`
|
||||||
|
RemoteCacheDirectory string `mapstructure:"remote_cache_directory"`
|
||||||
RemoteHost string `mapstructure:"remote_host"`
|
RemoteHost string `mapstructure:"remote_host"`
|
||||||
RemotePort uint `mapstructure:"remote_port"`
|
RemotePort uint `mapstructure:"remote_port"`
|
||||||
RemoteUser string `mapstructure:"remote_username"`
|
RemoteUser string `mapstructure:"remote_username"`
|
||||||
|
@ -110,6 +113,10 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
||||||
b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName)
|
b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if b.config.Version == "" {
|
||||||
|
b.config.Version = "9"
|
||||||
|
}
|
||||||
|
|
||||||
if b.config.RemoteUser == "" {
|
if b.config.RemoteUser == "" {
|
||||||
b.config.RemoteUser = "root"
|
b.config.RemoteUser = "root"
|
||||||
}
|
}
|
||||||
|
@ -118,6 +125,14 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
||||||
b.config.RemoteDatastore = "datastore1"
|
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 {
|
if b.config.RemotePort == 0 {
|
||||||
b.config.RemotePort = 22
|
b.config.RemotePort = 22
|
||||||
}
|
}
|
||||||
|
@ -134,6 +149,8 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
||||||
"remote_type": &b.config.RemoteType,
|
"remote_type": &b.config.RemoteType,
|
||||||
"remote_host": &b.config.RemoteHost,
|
"remote_host": &b.config.RemoteHost,
|
||||||
"remote_datastore": &b.config.RemoteDatastore,
|
"remote_datastore": &b.config.RemoteDatastore,
|
||||||
|
"remote_cache_datastore": &b.config.RemoteCacheDatastore,
|
||||||
|
"remote_cache_directory": &b.config.RemoteCacheDirectory,
|
||||||
"remote_user": &b.config.RemoteUser,
|
"remote_user": &b.config.RemoteUser,
|
||||||
"remote_password": &b.config.RemotePassword,
|
"remote_password": &b.config.RemotePassword,
|
||||||
}
|
}
|
||||||
|
|
|
@ -134,6 +134,10 @@ func TestBuilderPrepare_Defaults(t *testing.T) {
|
||||||
t.Errorf("bad output dir: %s", b.config.OutputDir)
|
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) {
|
if b.config.SSHWaitTimeout != (20 * time.Minute) {
|
||||||
t.Errorf("bad wait timeout: %s", b.config.SSHWaitTimeout)
|
t.Errorf("bad wait timeout: %s", b.config.SSHWaitTimeout)
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,8 @@ func NewDriver(config *config) (vmwcommon.Driver, error) {
|
||||||
Username: config.RemoteUser,
|
Username: config.RemoteUser,
|
||||||
Password: config.RemotePassword,
|
Password: config.RemotePassword,
|
||||||
Datastore: config.RemoteDatastore,
|
Datastore: config.RemoteDatastore,
|
||||||
|
CacheDatastore: config.RemoteCacheDatastore,
|
||||||
|
CacheDirectory: config.RemoteCacheDirectory,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,8 @@ type ESX5Driver struct {
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
Datastore string
|
Datastore string
|
||||||
|
CacheDatastore string
|
||||||
|
CacheDirectory string
|
||||||
|
|
||||||
comm packer.Communicator
|
comm packer.Communicator
|
||||||
outputDir string
|
outputDir string
|
||||||
|
@ -55,7 +57,21 @@ func (d *ESX5Driver) IsRunning(string) (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *ESX5Driver) Start(vmxPathLocal string, headless 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 {
|
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) {
|
func (d *ESX5Driver) UploadISO(localPath string, checksum string, checksumType string) (string, error) {
|
||||||
cacheRoot, _ := filepath.Abs(".")
|
finalPath := d.cachePath(localPath)
|
||||||
targetFile, err := filepath.Rel(cacheRoot, localPath)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
finalPath := d.datastorePath(targetFile)
|
|
||||||
if err := d.mkdir(filepath.ToSlash(filepath.Dir(finalPath))); err != nil {
|
if err := d.mkdir(filepath.ToSlash(filepath.Dir(finalPath))); err != nil {
|
||||||
return "", err
|
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)))
|
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 {
|
func (d *ESX5Driver) connect() error {
|
||||||
address := fmt.Sprintf("%s:%d", d.Host, d.Port)
|
address := fmt.Sprintf("%s:%d", d.Host, d.Port)
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ type vmxTemplateData struct {
|
||||||
GuestOS string
|
GuestOS string
|
||||||
DiskName string
|
DiskName string
|
||||||
ISOPath string
|
ISOPath string
|
||||||
|
Version string
|
||||||
}
|
}
|
||||||
|
|
||||||
// This step creates the VMX file for the VM.
|
// 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,
|
Name: config.VMName,
|
||||||
GuestOS: config.GuestOSType,
|
GuestOS: config.GuestOSType,
|
||||||
DiskName: config.DiskName,
|
DiskName: config.DiskName,
|
||||||
|
Version: config.Version,
|
||||||
ISOPath: isoPath,
|
ISOPath: isoPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,7 +182,7 @@ tools.upgrade.policy = "upgradeAtPowerCycle"
|
||||||
usb.pciSlotNumber = "32"
|
usb.pciSlotNumber = "32"
|
||||||
usb.present = "FALSE"
|
usb.present = "FALSE"
|
||||||
virtualHW.productCompatibility = "hosted"
|
virtualHW.productCompatibility = "hosted"
|
||||||
virtualHW.version = "9"
|
virtualHW.version = "{{ .Version }}"
|
||||||
vmci0.id = "1861462627"
|
vmci0.id = "1861462627"
|
||||||
vmci0.pciSlotNumber = "35"
|
vmci0.pciSlotNumber = "35"
|
||||||
vmci0.present = "TRUE"
|
vmci0.present = "TRUE"
|
||||||
|
|
|
@ -37,8 +37,17 @@ func (s *StepCloneVMX) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
return multistep.ActionHalt
|
return multistep.ActionHalt
|
||||||
}
|
}
|
||||||
|
|
||||||
diskName, ok := vmxData["scsi0:0.filename"]
|
var diskName string
|
||||||
if !ok {
|
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!")
|
err := fmt.Errorf("Root disk filename could not be found!")
|
||||||
state.Put("error", err)
|
state.Put("error", err)
|
||||||
return multistep.ActionHalt
|
return multistep.ActionHalt
|
||||||
|
|
|
@ -6,11 +6,10 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/hashicorp/go-checkpoint"
|
"github.com/hashicorp/go-checkpoint"
|
||||||
"github.com/mitchellh/packer/packer"
|
"github.com/mitchellh/packer/command"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
packer.VersionChecker = packerVersionCheck
|
|
||||||
checkpointResult = make(chan *checkpoint.CheckResponse, 1)
|
checkpointResult = make(chan *checkpoint.CheckResponse, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,9 +32,9 @@ func runCheckpoint(c *config) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
version := packer.Version
|
version := Version
|
||||||
if packer.VersionPrerelease != "" {
|
if VersionPrerelease != "" {
|
||||||
version += fmt.Sprintf(".%s", packer.VersionPrerelease)
|
version += fmt.Sprintf(".%s", VersionPrerelease)
|
||||||
}
|
}
|
||||||
|
|
||||||
signaturePath := filepath.Join(configDir, "checkpoint_signature")
|
signaturePath := filepath.Join(configDir, "checkpoint_signature")
|
||||||
|
@ -58,21 +57,23 @@ func runCheckpoint(c *config) {
|
||||||
checkpointResult <- resp
|
checkpointResult <- resp
|
||||||
}
|
}
|
||||||
|
|
||||||
// packerVersionCheck implements packer.VersionCheckFunc and is used
|
// commandVersionCheck implements command.VersionCheckFunc and is used
|
||||||
// as the version checker.
|
// 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
|
info := <-checkpointResult
|
||||||
if info == nil {
|
if info == nil {
|
||||||
var zero packer.VersionCheckInfo
|
var zero command.VersionCheckInfo
|
||||||
return zero, nil
|
return zero, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build the alerts that we may have received about our version
|
||||||
alerts := make([]string, len(info.Alerts))
|
alerts := make([]string, len(info.Alerts))
|
||||||
for i, a := range info.Alerts {
|
for i, a := range info.Alerts {
|
||||||
alerts[i] = a.Message
|
alerts[i] = a.Message
|
||||||
}
|
}
|
||||||
|
|
||||||
return packer.VersionCheckInfo{
|
return command.VersionCheckInfo{
|
||||||
Outdated: info.Outdated,
|
Outdated: info.Outdated,
|
||||||
Latest: info.CurrentVersion,
|
Latest: info.CurrentVersion,
|
||||||
Alerts: alerts,
|
Alerts: alerts,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package build
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
@ -14,16 +14,20 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Command byte
|
type BuildCommand struct {
|
||||||
|
Meta
|
||||||
func (Command) Help() string {
|
|
||||||
return strings.TrimSpace(helpText)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Command) Run(env packer.Environment, args []string) int {
|
func (c BuildCommand) Run(args []string) int {
|
||||||
var cfgColor, cfgDebug, cfgForce, cfgParallel bool
|
var cfgColor, cfgDebug, cfgForce, cfgParallel bool
|
||||||
buildOptions := new(cmdcommon.BuildOptions)
|
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 := flag.NewFlagSet("build", flag.ContinueOnError)
|
||||||
cmdFlags.Usage = func() { env.Ui().Say(c.Help()) }
|
cmdFlags.Usage = func() { env.Ui().Say(c.Help()) }
|
||||||
cmdFlags.BoolVar(&cfgColor, "color", true, "enable or disable color")
|
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
|
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"
|
return "build image(s) from template"
|
||||||
}
|
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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.
|
|
||||||
`
|
|
|
@ -1,23 +1,28 @@
|
||||||
package fix
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/mitchellh/packer/packer"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mitchellh/packer/fix"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Command byte
|
type FixCommand struct {
|
||||||
|
Meta
|
||||||
func (Command) Help() string {
|
|
||||||
return strings.TrimSpace(helpString)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 := flag.NewFlagSet("fix", flag.ContinueOnError)
|
||||||
cmdFlags.Usage = func() { env.Ui().Say(c.Help()) }
|
cmdFlags.Usage = func() { env.Ui().Say(c.Help()) }
|
||||||
if err := cmdFlags.Parse(args); err != nil {
|
if err := cmdFlags.Parse(args); err != nil {
|
||||||
|
@ -50,9 +55,9 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
||||||
tplF.Close()
|
tplF.Close()
|
||||||
|
|
||||||
input := templateData
|
input := templateData
|
||||||
for _, name := range FixerOrder {
|
for _, name := range fix.FixerOrder {
|
||||||
var err error
|
var err error
|
||||||
fixer, ok := Fixers[name]
|
fixer, ok := fix.Fixers[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
panic("fixer not found: " + name)
|
panic("fixer not found: " + name)
|
||||||
}
|
}
|
||||||
|
@ -85,6 +90,30 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
||||||
return 0
|
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"
|
return "fixes templates from old versions of packer"
|
||||||
}
|
}
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
|
|
||||||
`
|
|
|
@ -1,4 +1,4 @@
|
||||||
package inspect
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
|
@ -9,17 +9,17 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Command struct{}
|
type InspectCommand struct{
|
||||||
|
Meta
|
||||||
func (Command) Help() string {
|
|
||||||
return strings.TrimSpace(helpText)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Command) Synopsis() string {
|
func (c *InspectCommand) Run(args []string) int {
|
||||||
return "see components of a template"
|
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 := flag.NewFlagSet("inspect", flag.ContinueOnError)
|
||||||
flags.Usage = func() { env.Ui().Say(c.Help()) }
|
flags.Usage = func() { env.Ui().Say(c.Help()) }
|
||||||
if err := flags.Parse(args); err != nil {
|
if err := flags.Parse(args); err != nil {
|
||||||
|
@ -148,3 +148,23 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
||||||
|
|
||||||
return 0
|
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"
|
||||||
|
}
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
`
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package validate
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
|
@ -9,16 +9,20 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Command byte
|
type ValidateCommand struct {
|
||||||
|
Meta
|
||||||
func (Command) Help() string {
|
|
||||||
return strings.TrimSpace(helpString)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Command) Run(env packer.Environment, args []string) int {
|
func (c *ValidateCommand) Run(args []string) int {
|
||||||
var cfgSyntaxOnly bool
|
var cfgSyntaxOnly bool
|
||||||
buildOptions := new(cmdcommon.BuildOptions)
|
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 := flag.NewFlagSet("validate", flag.ContinueOnError)
|
||||||
cmdFlags.Usage = func() { env.Ui().Say(c.Help()) }
|
cmdFlags.Usage = func() { env.Ui().Say(c.Help()) }
|
||||||
cmdFlags.BoolVar(&cfgSyntaxOnly, "syntax-only", false, "check syntax only")
|
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
|
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"
|
return "check that a template is valid"
|
||||||
}
|
}
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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.
|
|
||||||
`
|
|
|
@ -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"
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVersionCommand_implements(t *testing.T) {
|
||||||
|
var _ cli.Command = &VersionCommand{}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -3,10 +3,11 @@ package common
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/mitchellh/multistep"
|
|
||||||
"github.com/mitchellh/packer/packer"
|
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/mitchellh/multistep"
|
||||||
|
"github.com/mitchellh/packer/packer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// StepDownload downloads a remote file using the download client within
|
// 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,
|
CopyFile: false,
|
||||||
Hash: HashForType(s.ChecksumType),
|
Hash: HashForType(s.ChecksumType),
|
||||||
Checksum: checksum,
|
Checksum: checksum,
|
||||||
UserAgent: packer.VersionString(),
|
UserAgent: "Packer",
|
||||||
}
|
}
|
||||||
|
|
||||||
path, err, retry := s.download(config, state)
|
path, err, retry := s.download(config, state)
|
||||||
|
|
34
config.go
34
config.go
|
@ -13,6 +13,9 @@ import (
|
||||||
"github.com/mitchellh/packer/packer/plugin"
|
"github.com/mitchellh/packer/packer/plugin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// EnvConfig is the global EnvironmentConfig we use to initialize the CLI.
|
||||||
|
var EnvConfig packer.EnvironmentConfig
|
||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
DisableCheckpoint bool `json:"disable_checkpoint"`
|
DisableCheckpoint bool `json:"disable_checkpoint"`
|
||||||
DisableCheckpointSignature bool `json:"disable_checkpoint_signature"`
|
DisableCheckpointSignature bool `json:"disable_checkpoint_signature"`
|
||||||
|
@ -20,7 +23,6 @@ type config struct {
|
||||||
PluginMaxPort uint
|
PluginMaxPort uint
|
||||||
|
|
||||||
Builders map[string]string
|
Builders map[string]string
|
||||||
Commands map[string]string
|
|
||||||
PostProcessors map[string]string `json:"post-processors"`
|
PostProcessors map[string]string `json:"post-processors"`
|
||||||
Provisioners map[string]string
|
Provisioners map[string]string
|
||||||
}
|
}
|
||||||
|
@ -79,15 +81,6 @@ func (c *config) Discover() error {
|
||||||
return nil
|
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
|
// This is a proper packer.BuilderFunc that can be used to load packer.Builder
|
||||||
// implementations from the defined plugins.
|
// implementations from the defined plugins.
|
||||||
func (c *config) LoadBuilder(name string) (packer.Builder, error) {
|
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()
|
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
|
// This is a proper implementation of packer.HookFunc that can be used
|
||||||
// to load packer.Hook implementations from the defined plugins.
|
// to load packer.Hook implementations from the defined plugins.
|
||||||
func (c *config) LoadHook(name string) (packer.Hook, error) {
|
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 {
|
func (c *config) discover(path string) error {
|
||||||
var err error
|
var err error
|
||||||
err = c.discoverSingle(
|
|
||||||
filepath.Join(path, "packer-builder-*"), &c.Builders)
|
if !filepath.IsAbs(path) {
|
||||||
|
path, err = filepath.Abs(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err = c.discoverSingle(
|
err = c.discoverSingle(
|
||||||
filepath.Join(path, "packer-command-*"), &c.Commands)
|
filepath.Join(path, "packer-builder-*"), &c.Builders)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -9,10 +9,13 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
"github.com/mitchellh/packer/packer"
|
"github.com/mitchellh/packer/packer"
|
||||||
"github.com/mitchellh/packer/packer/plugin"
|
"github.com/mitchellh/packer/packer/plugin"
|
||||||
"github.com/mitchellh/panicwrap"
|
"github.com/mitchellh/panicwrap"
|
||||||
|
"github.com/mitchellh/prefixedio"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -24,19 +27,20 @@ func main() {
|
||||||
|
|
||||||
// realMain is executed from main and returns the exit status to exit with.
|
// realMain is executed from main and returns the exit status to exit with.
|
||||||
func realMain() int {
|
func realMain() int {
|
||||||
// If there is no explicit number of Go threads to use, then set it
|
var wrapConfig panicwrap.WrapConfig
|
||||||
if os.Getenv("GOMAXPROCS") == "" {
|
|
||||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if !panicwrap.Wrapped(&wrapConfig) {
|
||||||
// Determine where logs should go in general (requested by the user)
|
// Determine where logs should go in general (requested by the user)
|
||||||
logWriter, err := logOutput()
|
logWriter, err := logOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Couldn't setup log output: %s", err)
|
fmt.Fprintf(os.Stderr, "Couldn't setup log output: %s", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
if logWriter == nil {
|
||||||
|
logWriter = ioutil.Discard
|
||||||
|
}
|
||||||
|
|
||||||
// We also always send logs to a temporary file that we use in case
|
// We always send logs to a temporary file that we use in case
|
||||||
// there is a panic. Otherwise, we delete it.
|
// there is a panic. Otherwise, we delete it.
|
||||||
logTempFile, err := ioutil.TempFile("", "packer-log")
|
logTempFile, err := ioutil.TempFile("", "packer-log")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -46,41 +50,59 @@ func realMain() int {
|
||||||
defer os.Remove(logTempFile.Name())
|
defer os.Remove(logTempFile.Name())
|
||||||
defer logTempFile.Close()
|
defer logTempFile.Close()
|
||||||
|
|
||||||
// Reset the log variables to minimize work in the subprocess
|
// Tell the logger to log to this file
|
||||||
os.Setenv("PACKER_LOG", "")
|
os.Setenv(EnvLog, "")
|
||||||
os.Setenv("PACKER_LOG_FILE", "")
|
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
|
// Create the configuration for panicwrap and wrap our executable
|
||||||
wrapConfig := &panicwrap.WrapConfig{
|
wrapConfig.Handler = panicHandler(logTempFile)
|
||||||
Handler: panicHandler(logTempFile),
|
wrapConfig.Writer = io.MultiWriter(logTempFile, logWriter)
|
||||||
Writer: io.MultiWriter(logTempFile, logWriter),
|
wrapConfig.Stdout = outW
|
||||||
}
|
exitStatus, err := panicwrap.Wrap(&wrapConfig)
|
||||||
|
|
||||||
exitStatus, err := panicwrap.Wrap(wrapConfig)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Couldn't start Packer: %s", err)
|
fmt.Fprintf(os.Stderr, "Couldn't start Packer: %s", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If >= 0, we're the parent, so just exit
|
||||||
if exitStatus >= 0 {
|
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
|
return exitStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
// We're the child, so just close the tempfile we made in order to
|
// 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.
|
// save file handles since the tempfile is only used by the parent.
|
||||||
logTempFile.Close()
|
logTempFile.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the real main
|
||||||
return wrappedMain()
|
return wrappedMain()
|
||||||
}
|
}
|
||||||
|
|
||||||
// wrappedMain is called only when we're wrapped by panicwrap and
|
// wrappedMain is called only when we're wrapped by panicwrap and
|
||||||
// returns the exit status to exit with.
|
// returns the exit status to exit with.
|
||||||
func wrappedMain() int {
|
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.SetOutput(os.Stderr)
|
||||||
|
|
||||||
log.Printf(
|
log.Printf(
|
||||||
"Packer Version: %s %s %s",
|
"[INFO] Packer version: %s %s %s",
|
||||||
packer.Version, packer.VersionPrerelease, packer.GitCommit)
|
Version, VersionPrerelease, GitCommit)
|
||||||
log.Printf("Packer Target OS/Arch: %s %s", runtime.GOOS, runtime.GOARCH)
|
log.Printf("Packer Target OS/Arch: %s %s", runtime.GOOS, runtime.GOARCH)
|
||||||
log.Printf("Built with Go Version: %s", runtime.Version())
|
log.Printf("Built with Go Version: %s", runtime.Version())
|
||||||
|
|
||||||
|
@ -118,16 +140,14 @@ func wrappedMain() int {
|
||||||
defer plugin.CleanupClients()
|
defer plugin.CleanupClients()
|
||||||
|
|
||||||
// Create the environment configuration
|
// Create the environment configuration
|
||||||
envConfig := packer.DefaultEnvironmentConfig()
|
EnvConfig = *packer.DefaultEnvironmentConfig()
|
||||||
envConfig.Cache = cache
|
EnvConfig.Cache = cache
|
||||||
envConfig.Commands = config.CommandNames()
|
EnvConfig.Components.Builder = config.LoadBuilder
|
||||||
envConfig.Components.Builder = config.LoadBuilder
|
EnvConfig.Components.Hook = config.LoadHook
|
||||||
envConfig.Components.Command = config.LoadCommand
|
EnvConfig.Components.PostProcessor = config.LoadPostProcessor
|
||||||
envConfig.Components.Hook = config.LoadHook
|
EnvConfig.Components.Provisioner = config.LoadProvisioner
|
||||||
envConfig.Components.PostProcessor = config.LoadPostProcessor
|
|
||||||
envConfig.Components.Provisioner = config.LoadProvisioner
|
|
||||||
if machineReadable {
|
if machineReadable {
|
||||||
envConfig.Ui = &packer.MachineReadableUi{
|
EnvConfig.Ui = &packer.MachineReadableUi{
|
||||||
Writer: os.Stdout,
|
Writer: os.Stdout,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,17 +159,18 @@ func wrappedMain() int {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
env, err := packer.NewEnvironment(envConfig)
|
//setupSignalHandlers(env)
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Packer initialization error: \n\n%s\n", err)
|
cli := &cli.CLI{
|
||||||
return 1
|
Args: args,
|
||||||
|
Commands: Commands,
|
||||||
|
HelpFunc: cli.BasicHelpFunc("packer"),
|
||||||
|
HelpWriter: os.Stdout,
|
||||||
}
|
}
|
||||||
|
|
||||||
setupSignalHandlers(env)
|
exitCode, err := cli.Run()
|
||||||
|
|
||||||
exitCode, err := env.Cli(args)
|
|
||||||
if err != nil {
|
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
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -220,20 +241,44 @@ func loadConfig() (*config, error) {
|
||||||
return &config, nil
|
return &config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// logOutput determines where we should send logs (if anywhere).
|
// copyOutput uses output prefixes to determine whether data on stdout
|
||||||
func logOutput() (logOutput io.Writer, err error) {
|
// should go to stdout or stderr. This is due to panicwrap using stderr
|
||||||
logOutput = ioutil.Discard
|
// as the log and error channel.
|
||||||
if os.Getenv("PACKER_LOG") != "" {
|
func copyOutput(r io.Reader, doneCh chan<- struct{}) {
|
||||||
logOutput = os.Stderr
|
defer close(doneCh)
|
||||||
|
|
||||||
if logPath := os.Getenv("PACKER_LOG_PATH"); logPath != "" {
|
pr, err := prefixedio.NewReader(r)
|
||||||
var err error
|
|
||||||
logOutput, err = os.Create(logPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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()
|
||||||
}
|
}
|
|
@ -25,6 +25,10 @@ type Artifact interface {
|
||||||
// This is used for UI output. It can be multiple lines.
|
// This is used for UI output. It can be multiple lines.
|
||||||
String() string
|
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,
|
// Destroy deletes the artifact. Packer calls this for various reasons,
|
||||||
// such as if a post-processor has processed this artifact and it is
|
// such as if a post-processor has processed this artifact and it is
|
||||||
// no longer needed.
|
// no longer needed.
|
||||||
|
|
|
@ -5,6 +5,7 @@ type MockArtifact struct {
|
||||||
BuilderIdValue string
|
BuilderIdValue string
|
||||||
FilesValue []string
|
FilesValue []string
|
||||||
IdValue string
|
IdValue string
|
||||||
|
StateValues map[string]interface{}
|
||||||
DestroyCalled bool
|
DestroyCalled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,6 +38,11 @@ func (*MockArtifact) String() string {
|
||||||
return "string"
|
return "string"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *MockArtifact) State(name string) interface{} {
|
||||||
|
value, _ := a.StateValues[name]
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
func (a *MockArtifact) Destroy() error {
|
func (a *MockArtifact) Destroy() error {
|
||||||
a.DestroyCalled = true
|
a.DestroyCalled = true
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -2,6 +2,7 @@ package packer
|
||||||
|
|
||||||
type TestArtifact struct {
|
type TestArtifact struct {
|
||||||
id string
|
id string
|
||||||
|
state map[string]interface{}
|
||||||
destroyCalled bool
|
destroyCalled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,6 +27,11 @@ func (*TestArtifact) String() string {
|
||||||
return "string"
|
return "string"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *TestArtifact) State(name string) interface{} {
|
||||||
|
value, _ := a.state[name]
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
func (a *TestArtifact) Destroy() error {
|
func (a *TestArtifact) Destroy() error {
|
||||||
a.destroyCalled = true
|
a.destroyCalled = true
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
|
@ -4,19 +4,12 @@ package packer
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// The function type used to lookup Builder implementations.
|
// The function type used to lookup Builder implementations.
|
||||||
type BuilderFunc func(name string) (Builder, error)
|
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.
|
// The function type used to lookup Hook implementations.
|
||||||
type HookFunc func(name string) (Hook, error)
|
type HookFunc func(name string) (Hook, error)
|
||||||
|
|
||||||
|
@ -31,7 +24,6 @@ type ProvisionerFunc func(name string) (Provisioner, error)
|
||||||
// commands, etc.
|
// commands, etc.
|
||||||
type ComponentFinder struct {
|
type ComponentFinder struct {
|
||||||
Builder BuilderFunc
|
Builder BuilderFunc
|
||||||
Command CommandFunc
|
|
||||||
Hook HookFunc
|
Hook HookFunc
|
||||||
PostProcessor PostProcessorFunc
|
PostProcessor PostProcessorFunc
|
||||||
Provisioner ProvisionerFunc
|
Provisioner ProvisionerFunc
|
||||||
|
@ -45,7 +37,6 @@ type ComponentFinder struct {
|
||||||
type Environment interface {
|
type Environment interface {
|
||||||
Builder(string) (Builder, error)
|
Builder(string) (Builder, error)
|
||||||
Cache() Cache
|
Cache() Cache
|
||||||
Cli([]string) (int, error)
|
|
||||||
Hook(string) (Hook, error)
|
Hook(string) (Hook, error)
|
||||||
PostProcessor(string) (PostProcessor, error)
|
PostProcessor(string) (PostProcessor, error)
|
||||||
Provisioner(string) (Provisioner, error)
|
Provisioner(string) (Provisioner, error)
|
||||||
|
@ -56,7 +47,6 @@ type Environment interface {
|
||||||
// environment.
|
// environment.
|
||||||
type coreEnvironment struct {
|
type coreEnvironment struct {
|
||||||
cache Cache
|
cache Cache
|
||||||
commands []string
|
|
||||||
components ComponentFinder
|
components ComponentFinder
|
||||||
ui Ui
|
ui Ui
|
||||||
}
|
}
|
||||||
|
@ -64,22 +54,14 @@ type coreEnvironment struct {
|
||||||
// This struct configures new environments.
|
// This struct configures new environments.
|
||||||
type EnvironmentConfig struct {
|
type EnvironmentConfig struct {
|
||||||
Cache Cache
|
Cache Cache
|
||||||
Commands []string
|
|
||||||
Components ComponentFinder
|
Components ComponentFinder
|
||||||
Ui Ui
|
Ui Ui
|
||||||
}
|
}
|
||||||
|
|
||||||
type helpCommandEntry struct {
|
|
||||||
i int
|
|
||||||
key string
|
|
||||||
synopsis string
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultEnvironmentConfig returns a default EnvironmentConfig that can
|
// DefaultEnvironmentConfig returns a default EnvironmentConfig that can
|
||||||
// be used to create a new enviroment with NewEnvironment with sane defaults.
|
// be used to create a new enviroment with NewEnvironment with sane defaults.
|
||||||
func DefaultEnvironmentConfig() *EnvironmentConfig {
|
func DefaultEnvironmentConfig() *EnvironmentConfig {
|
||||||
config := &EnvironmentConfig{}
|
config := &EnvironmentConfig{}
|
||||||
config.Commands = make([]string, 0)
|
|
||||||
config.Ui = &BasicUi{
|
config.Ui = &BasicUi{
|
||||||
Reader: os.Stdin,
|
Reader: os.Stdin,
|
||||||
Writer: os.Stdout,
|
Writer: os.Stdout,
|
||||||
|
@ -98,7 +80,6 @@ func NewEnvironment(config *EnvironmentConfig) (resultEnv Environment, err error
|
||||||
|
|
||||||
env := &coreEnvironment{}
|
env := &coreEnvironment{}
|
||||||
env.cache = config.Cache
|
env.cache = config.Cache
|
||||||
env.commands = config.Commands
|
|
||||||
env.components = config.Components
|
env.components = config.Components
|
||||||
env.ui = config.Ui
|
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 }
|
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 {
|
if env.components.Hook == nil {
|
||||||
env.components.Hook = func(string) (Hook, error) { return nil, 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
|
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
|
// Returns the UI for the environment. The UI is the interface that should
|
||||||
// be used for all communication with the outside world.
|
// be used for all communication with the outside world.
|
||||||
func (e *coreEnvironment) Ui() Ui {
|
func (e *coreEnvironment) Ui() Ui {
|
||||||
|
|
|
@ -6,8 +6,6 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -43,13 +41,6 @@ func testEnvironment() Environment {
|
||||||
return env
|
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) {
|
func TestEnvironment_DefaultConfig_Ui(t *testing.T) {
|
||||||
config := DefaultEnvironmentConfig()
|
config := DefaultEnvironmentConfig()
|
||||||
if config.Ui == nil {
|
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
|
// anything but if there is a panic in the test then yeah, something
|
||||||
// went wrong.
|
// went wrong.
|
||||||
env.Builder("foo")
|
env.Builder("foo")
|
||||||
env.Cli([]string{"foo"})
|
|
||||||
env.Hook("foo")
|
env.Hook("foo")
|
||||||
env.PostProcessor("foo")
|
env.PostProcessor("foo")
|
||||||
env.Provisioner("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) {
|
func TestEnvironment_Hook(t *testing.T) {
|
||||||
hook := &MockHook{}
|
hook := &MockHook{}
|
||||||
hooks := make(map[string]Hook)
|
hooks := make(map[string]Hook)
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue