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
|
||||
script:
|
||||
- make test
|
||||
- GOMAXPROCS=2 make test
|
||||
#- go test -race ./...
|
||||
|
||||
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:
|
||||
|
||||
* builder/digitalocean: API V2 support. [GH-1463]
|
||||
* builder/parallels: Don't depend on _prl-utils_ [GH-1499]
|
||||
|
||||
IMPROVEMENTS:
|
||||
|
||||
* builder/amazon/all: Support new AWS Frankfurt region.
|
||||
* builder/docker: Allow remote `DOCKER_HOST`, which works as long as
|
||||
volumes work. [GH-1594]
|
||||
* builder/qemu: Can set cache mode for main disk. [GH-1558]
|
||||
* builder/qemu: Can build from pre-existing disk. [GH-1342]
|
||||
* builder/vmware: Can specify path to Fusion installation with environmental
|
||||
variable `FUSION_APP_PATH`. [GH-1552]
|
||||
* builder/vmware: Can specify the HW version for the VMX. [GH-1530]
|
||||
* builder/vmware/esxi: Will now cache ISOs/floppies remotely. [GH-1479]
|
||||
* builder/vmware/vmx: Source VMX can have a disk connected via SATA. [GH-1604]
|
||||
* post-processors/vagrant: Support Qemu (libvirt) boxes. [GH-1330]
|
||||
* post-processors/vagrantcloud: Support self-hosted box URLs.
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
* core: Fix loading plugins from pwd. [GH-1521]
|
||||
* builder/amazon: Prefer token in config if given. [GH-1544]
|
||||
* builder/amazon/all: Extended timeout for waiting for AMI. [GH-1533]
|
||||
* builder/virtualbox: Can read VirtualBox version on FreeBSD. [GH-1570]
|
||||
* builder/virtualbox: More robust reading of guest additions URL. [GH-1509]
|
||||
* builder/vmware: Always remove floppies/drives. [GH-1504]
|
||||
* builder/vmware: Wait some time so that post-VMX update aren't
|
||||
overwritten. [GH-1504]
|
||||
* builder/vmware/esxi: Retry power on if it fails. [GH-1334]
|
||||
* builder/vmware-vmx: Fix issue with order of boot command support [GH-1492]
|
||||
* builder/amazon: Extend timeout and allow user override [GH-1533]
|
||||
* builder/parallels: Ignore 'The fdd0 device does not exist' [GH-1501]
|
||||
* builder/parallels: Rely on Cleanup functions to detach devices [GH-1502]
|
||||
* builder/parallels: Create VM without hdd and then add it later [GH-1548]
|
||||
* builder/parallels: Disconnect cdrom0 [GH-1605]
|
||||
* builder/qemu: Don't use `-redir` flag anymore, replace with
|
||||
`hostfwd` options. [GH-1561]
|
||||
* builder/qmeu: Use `pc` as default machine type instead of `pc-1.0`.
|
||||
* providers/aws: Ignore transient network errors. [GH-1579]
|
||||
* provisioner/ansible: Don't buffer output so output streams in. [GH-1585]
|
||||
* provisioner/ansible: Use inventory file always to avoid potentially
|
||||
deprecated feature. [GH-1562]
|
||||
* provisioner/shell: Quote environmental variables. [GH-1568]
|
||||
* provisioner/salt: Bootstrap over SSL. [GH-1608]
|
||||
* post-processors/docker-push: Work with docker-tag artifacts. [GH-1526]
|
||||
* post-processors/vsphere: Append "/" to object address. [GH-1615]
|
||||
|
||||
## 0.7.1 (September 10, 2014)
|
||||
|
||||
|
|
2
Makefile
2
Makefile
|
@ -15,6 +15,6 @@ testrace:
|
|||
go test -race $(TEST) $(TESTARGS)
|
||||
|
||||
updatedeps:
|
||||
go get -u -v -p 2 ./...
|
||||
go get -d -v -p 2 ./...
|
||||
|
||||
.PHONY: bin default test updatedeps
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
# -*- mode: ruby -*-
|
||||
# vi: set ft=ruby :
|
||||
|
||||
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
|
||||
VAGRANTFILE_API_VERSION = "2"
|
||||
|
||||
$script = <<SCRIPT
|
||||
SRCROOT="/opt/go"
|
||||
|
||||
|
@ -31,7 +28,7 @@ sudo chown -R vagrant:vagrant /opt/gopath
|
|||
sudo apt-get install -y curl git-core zip
|
||||
SCRIPT
|
||||
|
||||
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
||||
Vagrant.configure(2) do |config|
|
||||
config.vm.box = "chef/ubuntu-12.04"
|
||||
|
||||
config.vm.provision "shell", inline: $script
|
||||
|
|
|
@ -26,7 +26,7 @@ func (c *AccessConfig) Auth() (aws.Auth, error) {
|
|||
c.SecretKey = auth.SecretKey
|
||||
c.Token = auth.Token
|
||||
}
|
||||
if auth.Token == "" && c.Token != "" {
|
||||
if c.Token != "" {
|
||||
auth.Token = c.Token
|
||||
}
|
||||
|
||||
|
|
|
@ -52,6 +52,10 @@ func (a *Artifact) String() string {
|
|||
return fmt.Sprintf("AMIs were created:\n\n%s", strings.Join(amiStrings, "\n"))
|
||||
}
|
||||
|
||||
func (a *Artifact) State(name string) interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Artifact) Destroy() error {
|
||||
errors := make([]error, 0)
|
||||
|
||||
|
|
|
@ -6,6 +6,9 @@ import (
|
|||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -38,6 +41,9 @@ func AMIStateRefreshFunc(conn *ec2.EC2, imageId string) StateRefreshFunc {
|
|||
if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidAMIID.NotFound" {
|
||||
// Set this to nil as if we didn't find anything.
|
||||
resp = nil
|
||||
} else if isTransientNetworkError(err) {
|
||||
// Transient network error, treat it as if we didn't find anything
|
||||
resp = nil
|
||||
} else {
|
||||
log.Printf("Error on AMIStateRefresh: %s", err)
|
||||
return nil, "", err
|
||||
|
@ -64,6 +70,9 @@ func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) StateRefreshFunc {
|
|||
if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidInstanceID.NotFound" {
|
||||
// Set this to nil as if we didn't find anything.
|
||||
resp = nil
|
||||
} else if isTransientNetworkError(err) {
|
||||
// Transient network error, treat it as if we didn't find anything
|
||||
resp = nil
|
||||
} else {
|
||||
log.Printf("Error on InstanceStateRefresh: %s", err)
|
||||
return nil, "", err
|
||||
|
@ -90,6 +99,9 @@ func SpotRequestStateRefreshFunc(conn *ec2.EC2, spotRequestId string) StateRefre
|
|||
if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidSpotInstanceRequestID.NotFound" {
|
||||
// Set this to nil as if we didn't find anything.
|
||||
resp = nil
|
||||
} else if isTransientNetworkError(err) {
|
||||
// Transient network error, treat it as if we didn't find anything
|
||||
resp = nil
|
||||
} else {
|
||||
log.Printf("Error on SpotRequestStateRefresh: %s", err)
|
||||
return nil, "", err
|
||||
|
@ -112,6 +124,8 @@ func SpotRequestStateRefreshFunc(conn *ec2.EC2, spotRequestId string) StateRefre
|
|||
func WaitForState(conf *StateChangeConf) (i interface{}, err error) {
|
||||
log.Printf("Waiting for state to become: %s", conf.Target)
|
||||
|
||||
sleepSeconds := 2
|
||||
maxTicks := int(TimeoutSeconds()/sleepSeconds) + 1
|
||||
notfoundTick := 0
|
||||
|
||||
for {
|
||||
|
@ -125,7 +139,7 @@ func WaitForState(conf *StateChangeConf) (i interface{}, err error) {
|
|||
// If we didn't find the resource, check if we have been
|
||||
// not finding it for awhile, and if so, report an error.
|
||||
notfoundTick += 1
|
||||
if notfoundTick > 20 {
|
||||
if notfoundTick > maxTicks {
|
||||
return nil, errors.New("couldn't find resource")
|
||||
}
|
||||
} else {
|
||||
|
@ -156,8 +170,36 @@ func WaitForState(conf *StateChangeConf) (i interface{}, err error) {
|
|||
}
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
time.Sleep(time.Duration(sleepSeconds) * time.Second)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func isTransientNetworkError(err error) bool {
|
||||
if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Returns 300 seconds (5 minutes) by default
|
||||
// Some AWS operations, like copying an AMI to a distant region, take a very long time
|
||||
// Allow user to override with AWS_TIMEOUT_SECONDS environment variable
|
||||
func TimeoutSeconds() (seconds int) {
|
||||
seconds = 300
|
||||
|
||||
override := os.Getenv("AWS_TIMEOUT_SECONDS")
|
||||
if override != "" {
|
||||
n, err := strconv.Atoi(override)
|
||||
if err != nil {
|
||||
log.Printf("Invalid timeout seconds '%s', using default", override)
|
||||
} else {
|
||||
seconds = n
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Allowing %ds to complete (change with AWS_TIMEOUT_SECONDS)", seconds)
|
||||
return seconds
|
||||
}
|
||||
|
|
|
@ -4,36 +4,13 @@
|
|||
|
||||
package digitalocean
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
type Image struct {
|
||||
Id uint
|
||||
Name string
|
||||
Slug string
|
||||
Distribution string
|
||||
}
|
||||
|
||||
type ImagesResp struct {
|
||||
Images []Image
|
||||
}
|
||||
|
||||
type Region struct {
|
||||
Id uint
|
||||
Name string
|
||||
Slug string
|
||||
Id uint `json:"id,omitempty"` //only in v1 api
|
||||
Slug string `json:"slug"` //presen in both api
|
||||
Name string `json:"name"` //presen in both api
|
||||
Sizes []string `json:"sizes,omitempty"` //only in v2 api
|
||||
Available bool `json:"available,omitempty"` //only in v2 api
|
||||
Features []string `json:"features,omitempty"` //only in v2 api
|
||||
}
|
||||
|
||||
type RegionsResp struct {
|
||||
|
@ -41,378 +18,51 @@ type RegionsResp struct {
|
|||
}
|
||||
|
||||
type Size struct {
|
||||
Id uint
|
||||
Name string
|
||||
Slug string
|
||||
Id uint `json:"id,omitempty"` //only in v1 api
|
||||
Name string `json:"name,omitempty"` //only in v1 api
|
||||
Slug string `json:"slug"` //presen in both api
|
||||
Memory uint `json:"memory,omitempty"` //only in v2 api
|
||||
VCPUS uint `json:"vcpus,omitempty"` //only in v2 api
|
||||
Disk uint `json:"disk,omitempty"` //only in v2 api
|
||||
Transfer float64 `json:"transfer,omitempty"` //only in v2 api
|
||||
PriceMonthly float64 `json:"price_monthly,omitempty"` //only in v2 api
|
||||
PriceHourly float64 `json:"price_hourly,omitempty"` //only in v2 api
|
||||
Regions []string `json:"regions,omitempty"` //only in v2 api
|
||||
}
|
||||
|
||||
type SizesResp struct {
|
||||
Sizes []Size
|
||||
}
|
||||
|
||||
type DigitalOceanClient struct {
|
||||
// The http client for communicating
|
||||
client *http.Client
|
||||
|
||||
// Credentials
|
||||
ClientID string
|
||||
APIKey string
|
||||
|
||||
// The base URL of the API
|
||||
APIURL string
|
||||
type Image struct {
|
||||
Id uint `json:"id"` //presen in both api
|
||||
Name string `json:"name"` //presen in both api
|
||||
Slug string `json:"slug"` //presen in both api
|
||||
Distribution string `json:"distribution"` //presen in both api
|
||||
Public bool `json:"public,omitempty"` //only in v2 api
|
||||
Regions []string `json:"regions,omitempty"` //only in v2 api
|
||||
ActionIds []string `json:"action_ids,omitempty"` //only in v2 api
|
||||
CreatedAt string `json:"created_at,omitempty"` //only in v2 api
|
||||
}
|
||||
|
||||
// Creates a new client for communicating with DO
|
||||
func (d DigitalOceanClient) New(client string, key string, url string) *DigitalOceanClient {
|
||||
c := &DigitalOceanClient{
|
||||
client: &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
},
|
||||
},
|
||||
APIURL: url,
|
||||
ClientID: client,
|
||||
APIKey: key,
|
||||
}
|
||||
return c
|
||||
type ImagesResp struct {
|
||||
Images []Image
|
||||
}
|
||||
|
||||
// Creates an SSH Key and returns it's id
|
||||
func (d DigitalOceanClient) CreateKey(name string, pub string) (uint, error) {
|
||||
params := url.Values{}
|
||||
params.Set("name", name)
|
||||
params.Set("ssh_pub_key", pub)
|
||||
|
||||
body, err := NewRequest(d, "ssh_keys/new", params)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Read the SSH key's ID we just created
|
||||
key := body["ssh_key"].(map[string]interface{})
|
||||
keyId := key["id"].(float64)
|
||||
return uint(keyId), nil
|
||||
}
|
||||
|
||||
// Destroys an SSH key
|
||||
func (d DigitalOceanClient) DestroyKey(id uint) error {
|
||||
path := fmt.Sprintf("ssh_keys/%v/destroy", id)
|
||||
_, err := NewRequest(d, path, url.Values{})
|
||||
return err
|
||||
}
|
||||
|
||||
// Creates a droplet and returns it's id
|
||||
func (d DigitalOceanClient) CreateDroplet(name string, size string, image string, region string, keyId uint, privateNetworking bool) (uint, error) {
|
||||
params := url.Values{}
|
||||
params.Set("name", name)
|
||||
|
||||
found_size, err := d.Size(size)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("Invalid size or lookup failure: '%s': %s", size, err)
|
||||
}
|
||||
|
||||
found_image, err := d.Image(image)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("Invalid image or lookup failure: '%s': %s", image, err)
|
||||
}
|
||||
|
||||
found_region, err := d.Region(region)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("Invalid region or lookup failure: '%s': %s", region, err)
|
||||
}
|
||||
|
||||
params.Set("size_slug", found_size.Slug)
|
||||
params.Set("image_slug", found_image.Slug)
|
||||
params.Set("region_slug", found_region.Slug)
|
||||
params.Set("ssh_key_ids", fmt.Sprintf("%v", keyId))
|
||||
params.Set("private_networking", fmt.Sprintf("%v", privateNetworking))
|
||||
|
||||
body, err := NewRequest(d, "droplets/new", params)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Read the Droplets ID
|
||||
droplet := body["droplet"].(map[string]interface{})
|
||||
dropletId := droplet["id"].(float64)
|
||||
return uint(dropletId), err
|
||||
}
|
||||
|
||||
// Destroys a droplet
|
||||
func (d DigitalOceanClient) DestroyDroplet(id uint) error {
|
||||
path := fmt.Sprintf("droplets/%v/destroy", id)
|
||||
_, err := NewRequest(d, path, url.Values{})
|
||||
return err
|
||||
}
|
||||
|
||||
// Powers off a droplet
|
||||
func (d DigitalOceanClient) PowerOffDroplet(id uint) error {
|
||||
path := fmt.Sprintf("droplets/%v/power_off", id)
|
||||
|
||||
_, err := NewRequest(d, path, url.Values{})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Shutsdown a droplet. This is a "soft" shutdown.
|
||||
func (d DigitalOceanClient) ShutdownDroplet(id uint) error {
|
||||
path := fmt.Sprintf("droplets/%v/shutdown", id)
|
||||
|
||||
_, err := NewRequest(d, path, url.Values{})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Creates a snaphot of a droplet by it's ID
|
||||
func (d DigitalOceanClient) CreateSnapshot(id uint, name string) error {
|
||||
path := fmt.Sprintf("droplets/%v/snapshot", id)
|
||||
|
||||
params := url.Values{}
|
||||
params.Set("name", name)
|
||||
|
||||
_, err := NewRequest(d, path, params)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Returns all available images.
|
||||
func (d DigitalOceanClient) Images() ([]Image, error) {
|
||||
resp, err := NewRequest(d, "images", url.Values{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result ImagesResp
|
||||
if err := mapstructure.Decode(resp, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result.Images, nil
|
||||
}
|
||||
|
||||
// Destroys an image by its ID.
|
||||
func (d DigitalOceanClient) DestroyImage(id uint) error {
|
||||
path := fmt.Sprintf("images/%d/destroy", id)
|
||||
_, err := NewRequest(d, path, url.Values{})
|
||||
return err
|
||||
}
|
||||
|
||||
// Returns DO's string representation of status "off" "new" "active" etc.
|
||||
func (d DigitalOceanClient) DropletStatus(id uint) (string, string, error) {
|
||||
path := fmt.Sprintf("droplets/%v", id)
|
||||
|
||||
body, err := NewRequest(d, path, url.Values{})
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
var ip string
|
||||
|
||||
// Read the droplet's "status"
|
||||
droplet := body["droplet"].(map[string]interface{})
|
||||
status := droplet["status"].(string)
|
||||
|
||||
if droplet["ip_address"] != nil {
|
||||
ip = droplet["ip_address"].(string)
|
||||
}
|
||||
|
||||
return ip, status, err
|
||||
}
|
||||
|
||||
// Sends an api request and returns a generic map[string]interface of
|
||||
// the response.
|
||||
func NewRequest(d DigitalOceanClient, path string, params url.Values) (map[string]interface{}, error) {
|
||||
client := d.client
|
||||
|
||||
// Add the authentication parameters
|
||||
params.Set("client_id", d.ClientID)
|
||||
params.Set("api_key", d.APIKey)
|
||||
|
||||
url := fmt.Sprintf("%s/%s?%s", d.APIURL, path, params.Encode())
|
||||
|
||||
// Do some basic scrubbing so sensitive information doesn't appear in logs
|
||||
scrubbedUrl := strings.Replace(url, d.ClientID, "CLIENT_ID", -1)
|
||||
scrubbedUrl = strings.Replace(scrubbedUrl, d.APIKey, "API_KEY", -1)
|
||||
log.Printf("sending new request to digitalocean: %s", scrubbedUrl)
|
||||
|
||||
var lastErr error
|
||||
for attempts := 1; attempts < 10; attempts++ {
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Printf("response from digitalocean: %s", body)
|
||||
|
||||
var decodedResponse map[string]interface{}
|
||||
err = json.Unmarshal(body, &decodedResponse)
|
||||
if err != nil {
|
||||
err = errors.New(fmt.Sprintf("Failed to decode JSON response (HTTP %v) from DigitalOcean: %s",
|
||||
resp.StatusCode, body))
|
||||
return decodedResponse, err
|
||||
}
|
||||
|
||||
// Check for errors sent by digitalocean
|
||||
status := decodedResponse["status"].(string)
|
||||
if status == "OK" {
|
||||
return decodedResponse, nil
|
||||
}
|
||||
|
||||
if status == "ERROR" {
|
||||
statusRaw, ok := decodedResponse["error_message"]
|
||||
if ok {
|
||||
status = statusRaw.(string)
|
||||
} else {
|
||||
status = fmt.Sprintf(
|
||||
"Unknown error. Full response body: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
lastErr = errors.New(fmt.Sprintf("Received error from DigitalOcean (%d): %s",
|
||||
resp.StatusCode, status))
|
||||
log.Println(lastErr)
|
||||
if strings.Contains(status, "a pending event") {
|
||||
// Retry, DigitalOcean sends these dumb "pending event"
|
||||
// errors all the time.
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
// Some other kind of error. Just return.
|
||||
return decodedResponse, lastErr
|
||||
}
|
||||
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
func (d DigitalOceanClient) Image(slug_or_name_or_id string) (Image, error) {
|
||||
images, err := d.Images()
|
||||
if err != nil {
|
||||
return Image{}, err
|
||||
}
|
||||
|
||||
for _, image := range images {
|
||||
if strings.EqualFold(image.Slug, slug_or_name_or_id) {
|
||||
return image, nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, image := range images {
|
||||
if strings.EqualFold(image.Name, slug_or_name_or_id) {
|
||||
return image, nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, image := range images {
|
||||
id, err := strconv.Atoi(slug_or_name_or_id)
|
||||
if err == nil {
|
||||
if image.Id == uint(id) {
|
||||
return image, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = errors.New(fmt.Sprintf("Unknown image '%v'", slug_or_name_or_id))
|
||||
|
||||
return Image{}, err
|
||||
}
|
||||
|
||||
// Returns all available regions.
|
||||
func (d DigitalOceanClient) Regions() ([]Region, error) {
|
||||
resp, err := NewRequest(d, "regions", url.Values{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result RegionsResp
|
||||
if err := mapstructure.Decode(resp, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result.Regions, nil
|
||||
}
|
||||
|
||||
func (d DigitalOceanClient) Region(slug_or_name_or_id string) (Region, error) {
|
||||
regions, err := d.Regions()
|
||||
if err != nil {
|
||||
return Region{}, err
|
||||
}
|
||||
|
||||
for _, region := range regions {
|
||||
if strings.EqualFold(region.Slug, slug_or_name_or_id) {
|
||||
return region, nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, region := range regions {
|
||||
if strings.EqualFold(region.Name, slug_or_name_or_id) {
|
||||
return region, nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, region := range regions {
|
||||
id, err := strconv.Atoi(slug_or_name_or_id)
|
||||
if err == nil {
|
||||
if region.Id == uint(id) {
|
||||
return region, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = errors.New(fmt.Sprintf("Unknown region '%v'", slug_or_name_or_id))
|
||||
|
||||
return Region{}, err
|
||||
}
|
||||
|
||||
// Returns all available sizes.
|
||||
func (d DigitalOceanClient) Sizes() ([]Size, error) {
|
||||
resp, err := NewRequest(d, "sizes", url.Values{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result SizesResp
|
||||
if err := mapstructure.Decode(resp, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result.Sizes, nil
|
||||
}
|
||||
|
||||
func (d DigitalOceanClient) Size(slug_or_name_or_id string) (Size, error) {
|
||||
sizes, err := d.Sizes()
|
||||
if err != nil {
|
||||
return Size{}, err
|
||||
}
|
||||
|
||||
for _, size := range sizes {
|
||||
if strings.EqualFold(size.Slug, slug_or_name_or_id) {
|
||||
return size, nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, size := range sizes {
|
||||
if strings.EqualFold(size.Name, slug_or_name_or_id) {
|
||||
return size, nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, size := range sizes {
|
||||
id, err := strconv.Atoi(slug_or_name_or_id)
|
||||
if err == nil {
|
||||
if size.Id == uint(id) {
|
||||
return size, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = errors.New(fmt.Sprintf("Unknown size '%v'", slug_or_name_or_id))
|
||||
|
||||
return Size{}, err
|
||||
type DigitalOceanClient interface {
|
||||
CreateKey(string, string) (uint, error)
|
||||
DestroyKey(uint) error
|
||||
CreateDroplet(string, string, string, string, uint, bool) (uint, error)
|
||||
DestroyDroplet(uint) error
|
||||
PowerOffDroplet(uint) error
|
||||
ShutdownDroplet(uint) error
|
||||
CreateSnapshot(uint, string) error
|
||||
Images() ([]Image, error)
|
||||
DestroyImage(uint) error
|
||||
DropletStatus(uint) (string, string, error)
|
||||
Image(string) (Image, error)
|
||||
Regions() ([]Region, error)
|
||||
Region(string) (Region, error)
|
||||
Sizes() ([]Size, error)
|
||||
Size(string) (Size, error)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
// The client for making API calls
|
||||
client *DigitalOceanClient
|
||||
client DigitalOceanClient
|
||||
}
|
||||
|
||||
func (*Artifact) BuilderId() string {
|
||||
|
@ -37,6 +37,10 @@ func (a *Artifact) String() string {
|
|||
return fmt.Sprintf("A snapshot was created: '%v' in region '%v'", a.snapshotName, a.regionName)
|
||||
}
|
||||
|
||||
func (a *Artifact) State(name string) interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Artifact) Destroy() error {
|
||||
log.Printf("Destroying image: %d (%s)", a.snapshotId, a.snapshotName)
|
||||
return a.client.DestroyImage(a.snapshotId)
|
||||
|
|
|
@ -40,6 +40,7 @@ type config struct {
|
|||
ClientID string `mapstructure:"client_id"`
|
||||
APIKey string `mapstructure:"api_key"`
|
||||
APIURL string `mapstructure:"api_url"`
|
||||
APIToken string `mapstructure:"api_token"`
|
||||
RegionID uint `mapstructure:"region_id"`
|
||||
SizeID uint `mapstructure:"size_id"`
|
||||
ImageID uint `mapstructure:"image_id"`
|
||||
|
@ -101,6 +102,11 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
b.config.APIURL = os.Getenv("DIGITALOCEAN_API_URL")
|
||||
}
|
||||
|
||||
if b.config.APIToken == "" {
|
||||
// Default to environment variable for api_token, if it exists
|
||||
b.config.APIToken = os.Getenv("DIGITALOCEAN_API_TOKEN")
|
||||
}
|
||||
|
||||
if b.config.Region == "" {
|
||||
if b.config.RegionID != 0 {
|
||||
b.config.Region = fmt.Sprintf("%v", b.config.RegionID)
|
||||
|
@ -164,6 +170,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
"client_id": &b.config.ClientID,
|
||||
"api_key": &b.config.APIKey,
|
||||
"api_url": &b.config.APIURL,
|
||||
"api_token": &b.config.APIToken,
|
||||
"snapshot_name": &b.config.SnapshotName,
|
||||
"droplet_name": &b.config.DropletName,
|
||||
"ssh_username": &b.config.SSHUsername,
|
||||
|
@ -180,21 +187,23 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// Required configurations that will display errors if not set
|
||||
if b.config.ClientID == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("a client_id must be specified"))
|
||||
if b.config.APIToken == "" {
|
||||
// Required configurations that will display errors if not set
|
||||
if b.config.ClientID == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("a client_id for v1 auth or api_token for v2 auth must be specified"))
|
||||
}
|
||||
|
||||
if b.config.APIKey == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("a api_key for v1 auth or api_token for v2 auth must be specified"))
|
||||
}
|
||||
}
|
||||
|
||||
if b.config.APIURL == "" {
|
||||
b.config.APIURL = "https://api.digitalocean.com"
|
||||
}
|
||||
|
||||
if b.config.APIKey == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("an api_key must be specified"))
|
||||
}
|
||||
|
||||
sshTimeout, err := time.ParseDuration(b.config.RawSSHTimeout)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
|
@ -218,8 +227,13 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
}
|
||||
|
||||
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
|
||||
var client DigitalOceanClient
|
||||
// Initialize the DO API client
|
||||
client := DigitalOceanClient{}.New(b.config.ClientID, b.config.APIKey, b.config.APIURL)
|
||||
if b.config.APIToken == "" {
|
||||
client = DigitalOceanClientNewV1(b.config.ClientID, b.config.APIKey, b.config.APIURL)
|
||||
} else {
|
||||
client = DigitalOceanClientNewV2(b.config.APIToken, b.config.APIURL)
|
||||
}
|
||||
|
||||
// Set up the state
|
||||
state := new(multistep.BasicStateBag)
|
||||
|
|
|
@ -12,7 +12,7 @@ type stepCreateDroplet struct {
|
|||
}
|
||||
|
||||
func (s *stepCreateDroplet) Run(state multistep.StateBag) multistep.StepAction {
|
||||
client := state.Get("client").(*DigitalOceanClient)
|
||||
client := state.Get("client").(DigitalOceanClient)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
c := state.Get("config").(config)
|
||||
sshKeyId := state.Get("ssh_key_id").(uint)
|
||||
|
@ -44,7 +44,7 @@ func (s *stepCreateDroplet) Cleanup(state multistep.StateBag) {
|
|||
return
|
||||
}
|
||||
|
||||
client := state.Get("client").(*DigitalOceanClient)
|
||||
client := state.Get("client").(DigitalOceanClient)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
c := state.Get("config").(config)
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ type stepCreateSSHKey struct {
|
|||
}
|
||||
|
||||
func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction {
|
||||
client := state.Get("client").(*DigitalOceanClient)
|
||||
client := state.Get("client").(DigitalOceanClient)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
ui.Say("Creating temporary ssh key for droplet...")
|
||||
|
@ -71,15 +71,14 @@ func (s *stepCreateSSHKey) Cleanup(state multistep.StateBag) {
|
|||
return
|
||||
}
|
||||
|
||||
client := state.Get("client").(*DigitalOceanClient)
|
||||
client := state.Get("client").(DigitalOceanClient)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
c := state.Get("config").(config)
|
||||
|
||||
ui.Say("Deleting temporary ssh key...")
|
||||
err := client.DestroyKey(s.keyId)
|
||||
|
||||
curlstr := fmt.Sprintf("curl '%v/ssh_keys/%v/destroy?client_id=%v&api_key=%v'",
|
||||
c.APIURL, s.keyId, c.ClientID, c.APIKey)
|
||||
curlstr := fmt.Sprintf("curl -H 'Authorization: Bearer #TOKEN#' -X DELETE '%v/v2/account/keys/%v'", c.APIURL, s.keyId)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Error cleaning up ssh key: %v", err.Error())
|
||||
|
|
|
@ -2,6 +2,7 @@ package digitalocean
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
@ -9,7 +10,7 @@ import (
|
|||
type stepDropletInfo struct{}
|
||||
|
||||
func (s *stepDropletInfo) Run(state multistep.StateBag) multistep.StepAction {
|
||||
client := state.Get("client").(*DigitalOceanClient)
|
||||
client := state.Get("client").(DigitalOceanClient)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
c := state.Get("config").(config)
|
||||
dropletId := state.Get("droplet_id").(uint)
|
||||
|
|
|
@ -2,15 +2,16 @@ package digitalocean
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
)
|
||||
|
||||
type stepPowerOff struct{}
|
||||
|
||||
func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction {
|
||||
client := state.Get("client").(*DigitalOceanClient)
|
||||
client := state.Get("client").(DigitalOceanClient)
|
||||
c := state.Get("config").(config)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
dropletId := state.Get("droplet_id").(uint)
|
||||
|
|
|
@ -2,16 +2,17 @@ package digitalocean
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type stepShutdown struct{}
|
||||
|
||||
func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction {
|
||||
client := state.Get("client").(*DigitalOceanClient)
|
||||
client := state.Get("client").(DigitalOceanClient)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
dropletId := state.Get("droplet_id").(uint)
|
||||
|
||||
|
|
|
@ -3,15 +3,16 @@ package digitalocean
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
)
|
||||
|
||||
type stepSnapshot struct{}
|
||||
|
||||
func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
|
||||
client := state.Get("client").(*DigitalOceanClient)
|
||||
client := state.Get("client").(DigitalOceanClient)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
c := state.Get("config").(config)
|
||||
dropletId := state.Get("droplet_id").(uint)
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
|
||||
// waitForState simply blocks until the droplet is in
|
||||
// a state we expect, while eventually timing out.
|
||||
func waitForDropletState(desiredState string, dropletId uint, client *DigitalOceanClient, timeout time.Duration) error {
|
||||
func waitForDropletState(desiredState string, dropletId uint, client DigitalOceanClient, timeout time.Duration) error {
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
|
||||
|
|
|
@ -27,6 +27,10 @@ func (a *ExportArtifact) String() string {
|
|||
return fmt.Sprintf("Exported Docker file: %s", a.path)
|
||||
}
|
||||
|
||||
func (a *ExportArtifact) State(name string) interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *ExportArtifact) Destroy() error {
|
||||
return os.Remove(a.path)
|
||||
}
|
||||
|
|
|
@ -28,6 +28,10 @@ func (a *ImportArtifact) String() string {
|
|||
return fmt.Sprintf("Imported Docker image: %s", a.Id())
|
||||
}
|
||||
|
||||
func (*ImportArtifact) State(name string) interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *ImportArtifact) Destroy() error {
|
||||
return a.Driver.DeleteImage(a.Id())
|
||||
}
|
||||
|
|
|
@ -258,10 +258,5 @@ func (d *DockerDriver) Verify() error {
|
|||
return err
|
||||
}
|
||||
|
||||
if v := os.Getenv("DOCKER_HOST"); v != "" {
|
||||
return fmt.Errorf(
|
||||
"DOCKER_HOST cannot be set with the Packer Docker builder.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -37,3 +37,7 @@ func (a *Artifact) Id() string {
|
|||
func (a *Artifact) String() string {
|
||||
return fmt.Sprintf("A disk image was created: %v", a.imageName)
|
||||
}
|
||||
|
||||
func (a *Artifact) State(name string) interface{} {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -24,6 +24,25 @@ func (config *Config) getImage() Image {
|
|||
return Image{Name: config.SourceImage, ProjectId: project}
|
||||
}
|
||||
|
||||
func (config *Config) getInstanceMetadata(sshPublicKey string) map[string]string {
|
||||
instanceMetadata := make(map[string]string)
|
||||
|
||||
// Copy metadata from config
|
||||
for k, v := range config.Metadata {
|
||||
instanceMetadata[k] = v
|
||||
}
|
||||
|
||||
// Merge any existing ssh keys with our public key
|
||||
sshMetaKey := "sshKeys"
|
||||
sshKeys := fmt.Sprintf("%s:%s", config.SSHUsername, sshPublicKey)
|
||||
if confSshKeys, exists := instanceMetadata[sshMetaKey]; exists {
|
||||
sshKeys = fmt.Sprintf("%s\n%s", sshKeys, confSshKeys)
|
||||
}
|
||||
instanceMetadata[sshMetaKey] = sshKeys
|
||||
|
||||
return instanceMetadata
|
||||
}
|
||||
|
||||
// Run executes the Packer build step that creates a GCE instance.
|
||||
func (s *StepCreateInstance) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
|
@ -39,13 +58,11 @@ func (s *StepCreateInstance) Run(state multistep.StateBag) multistep.StepAction
|
|||
DiskSizeGb: config.DiskSizeGb,
|
||||
Image: config.getImage(),
|
||||
MachineType: config.MachineType,
|
||||
Metadata: map[string]string{
|
||||
"sshKeys": fmt.Sprintf("%s:%s", config.SSHUsername, sshPublicKey),
|
||||
},
|
||||
Name: name,
|
||||
Network: config.Network,
|
||||
Tags: config.Tags,
|
||||
Zone: config.Zone,
|
||||
Metadata: config.getInstanceMetadata(sshPublicKey),
|
||||
Name: name,
|
||||
Network: config.Network,
|
||||
Tags: config.Tags,
|
||||
Zone: config.Zone,
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
|
|
|
@ -24,6 +24,10 @@ func (a *NullArtifact) String() string {
|
|||
return fmt.Sprintf("Did not export anything. This is the null builder")
|
||||
}
|
||||
|
||||
func (a *NullArtifact) State(name string) interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *NullArtifact) Destroy() error {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -5,11 +5,12 @@ import (
|
|||
"fmt"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/rackspace/gophercloud"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||
)
|
||||
|
||||
// AccessConfig is for common configuration related to openstack access
|
||||
|
|
|
@ -2,8 +2,9 @@ package openstack
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/rackspace/gophercloud"
|
||||
"log"
|
||||
|
||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||
)
|
||||
|
||||
// Artifact is an artifact implementation that contains built images.
|
||||
|
@ -35,6 +36,10 @@ func (a *Artifact) String() string {
|
|||
return fmt.Sprintf("An image was created: %v", a.ImageId)
|
||||
}
|
||||
|
||||
func (a *Artifact) State(name string) interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Artifact) Destroy() error {
|
||||
log.Printf("Destroying image: %d", a.ImageId)
|
||||
return a.Conn.DeleteImageById(a.ImageId)
|
||||
|
|
|
@ -8,8 +8,9 @@ import (
|
|||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/rackspace/gophercloud"
|
||||
"log"
|
||||
|
||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||
)
|
||||
|
||||
// The unique ID for this builder
|
||||
|
|
|
@ -5,9 +5,10 @@ import (
|
|||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/racker/perigee"
|
||||
"github.com/rackspace/gophercloud"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||
)
|
||||
|
||||
// StateRefreshFunc is a function type used for StateChangeConf that is
|
||||
|
|
|
@ -5,8 +5,9 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/rackspace/gophercloud"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||
)
|
||||
|
||||
// SSHAddress returns a function that can be given to the SSH communicator
|
||||
|
|
|
@ -4,7 +4,8 @@ import (
|
|||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/rackspace/gophercloud"
|
||||
|
||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||
)
|
||||
|
||||
type StepAllocateIp struct {
|
||||
|
|
|
@ -4,9 +4,10 @@ import (
|
|||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/rackspace/gophercloud"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||
)
|
||||
|
||||
type stepCreateImage struct{}
|
||||
|
|
|
@ -5,10 +5,11 @@ import (
|
|||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/common/uuid"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/rackspace/gophercloud"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||
)
|
||||
|
||||
type StepKeyPair struct {
|
||||
|
|
|
@ -4,8 +4,9 @@ import (
|
|||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/rackspace/gophercloud"
|
||||
"log"
|
||||
|
||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||
)
|
||||
|
||||
type StepRunSourceServer struct {
|
||||
|
|
|
@ -66,6 +66,10 @@ func (a *artifact) String() string {
|
|||
return fmt.Sprintf("VM files in directory: %s", a.dir)
|
||||
}
|
||||
|
||||
func (a *artifact) State(name string) interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *artifact) Destroy() error {
|
||||
return os.RemoveAll(a.dir)
|
||||
}
|
||||
|
|
|
@ -81,7 +81,7 @@ func (s *stepAttachISO) Cleanup(state multistep.StateBag) {
|
|||
log.Println("Enabling default CD/DVD drive...")
|
||||
command := []string{
|
||||
"set", vmName,
|
||||
"--device-set", "cdrom0", "--enable",
|
||||
"--device-set", "cdrom0", "--enable", "--disconnect",
|
||||
}
|
||||
|
||||
if err := driver.Prlctl(command...); err != nil {
|
||||
|
|
|
@ -8,8 +8,9 @@ import (
|
|||
// Artifact is the result of running the Qemu builder, namely a set
|
||||
// of files associated with the resulting machine.
|
||||
type Artifact struct {
|
||||
dir string
|
||||
f []string
|
||||
dir string
|
||||
f []string
|
||||
state map[string]interface{}
|
||||
}
|
||||
|
||||
func (*Artifact) BuilderId() string {
|
||||
|
@ -28,6 +29,10 @@ func (a *Artifact) String() string {
|
|||
return fmt.Sprintf("VM files in directory: %s", a.dir)
|
||||
}
|
||||
|
||||
func (a *Artifact) State(name string) interface{} {
|
||||
return a.state[name]
|
||||
}
|
||||
|
||||
func (a *Artifact) Destroy() error {
|
||||
return os.RemoveAll(a.dir)
|
||||
}
|
||||
|
|
|
@ -56,6 +56,14 @@ var diskInterface = map[string]bool{
|
|||
"virtio": true,
|
||||
}
|
||||
|
||||
var diskCache = map[string]bool{
|
||||
"writethrough": true,
|
||||
"writeback": true,
|
||||
"none": true,
|
||||
"unsafe": true,
|
||||
"directsync": true,
|
||||
}
|
||||
|
||||
type Builder struct {
|
||||
config config
|
||||
runner multistep.Runner
|
||||
|
@ -68,9 +76,11 @@ type config struct {
|
|||
BootCommand []string `mapstructure:"boot_command"`
|
||||
DiskInterface string `mapstructure:"disk_interface"`
|
||||
DiskSize uint `mapstructure:"disk_size"`
|
||||
DiskCache string `mapstructure:"disk_cache"`
|
||||
FloppyFiles []string `mapstructure:"floppy_files"`
|
||||
Format string `mapstructure:"format"`
|
||||
Headless bool `mapstructure:"headless"`
|
||||
DiskImage bool `mapstructure:"disk_image"`
|
||||
HTTPDir string `mapstructure:"http_directory"`
|
||||
HTTPPortMin uint `mapstructure:"http_port_min"`
|
||||
HTTPPortMax uint `mapstructure:"http_port_max"`
|
||||
|
@ -126,6 +136,10 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
b.config.DiskSize = 40000
|
||||
}
|
||||
|
||||
if b.config.DiskCache == "" {
|
||||
b.config.DiskCache = "writeback"
|
||||
}
|
||||
|
||||
if b.config.Accelerator == "" {
|
||||
b.config.Accelerator = "kvm"
|
||||
}
|
||||
|
@ -139,7 +153,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
}
|
||||
|
||||
if b.config.MachineType == "" {
|
||||
b.config.MachineType = "pc-1.0"
|
||||
b.config.MachineType = "pc"
|
||||
}
|
||||
|
||||
if b.config.OutputDir == "" {
|
||||
|
@ -280,6 +294,11 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
errs, errors.New("unrecognized disk interface type"))
|
||||
}
|
||||
|
||||
if _, ok := diskCache[b.config.DiskCache]; !ok {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("unrecognized disk cache type"))
|
||||
}
|
||||
|
||||
if b.config.HTTPPortMin > b.config.HTTPPortMax {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("http_port_min must be less than http_port_max"))
|
||||
|
@ -412,6 +431,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
Files: b.config.FloppyFiles,
|
||||
},
|
||||
new(stepCreateDisk),
|
||||
new(stepCopyDisk),
|
||||
new(stepResizeDisk),
|
||||
new(stepHTTPServer),
|
||||
new(stepForwardSSH),
|
||||
new(stepConfigureVNC),
|
||||
|
@ -479,10 +500,16 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
}
|
||||
|
||||
artifact := &Artifact{
|
||||
dir: b.config.OutputDir,
|
||||
f: files,
|
||||
dir: b.config.OutputDir,
|
||||
f: files,
|
||||
state: make(map[string]interface{}),
|
||||
}
|
||||
|
||||
artifact.state["diskName"] = state.Get("disk_filename").(string)
|
||||
artifact.state["diskType"] = b.config.Format
|
||||
artifact.state["diskSize"] = uint64(b.config.DiskSize)
|
||||
artifact.state["domainType"] = b.config.Accelerator
|
||||
|
||||
return artifact, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
driver := state.Get("driver").(Driver)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
path := filepath.Join(config.OutputDir, fmt.Sprintf("%s.%s", config.VMName,
|
||||
strings.ToLower(config.Format)))
|
||||
name := config.VMName + "." + strings.ToLower(config.Format)
|
||||
path := filepath.Join(config.OutputDir, name)
|
||||
|
||||
command := []string{
|
||||
"create",
|
||||
|
@ -26,6 +26,10 @@ func (s *stepCreateDisk) Run(state multistep.StateBag) multistep.StepAction {
|
|||
fmt.Sprintf("%vM", config.DiskSize),
|
||||
}
|
||||
|
||||
if config.DiskImage == true {
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
ui.Say("Creating hard drive...")
|
||||
if err := driver.QemuImg(command...); err != nil {
|
||||
err := fmt.Errorf("Error creating hard drive: %s", err)
|
||||
|
@ -34,6 +38,8 @@ func (s *stepCreateDisk) Run(state multistep.StateBag) multistep.StepAction {
|
|||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
state.Put("disk_filename", name)
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
|
|
|
@ -2,11 +2,12 @@ package qemu
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// This step adds a NAT port forwarding definition so that SSH is available
|
||||
|
@ -23,9 +24,16 @@ func (s *stepForwardSSH) Run(state multistep.StateBag) multistep.StepAction {
|
|||
|
||||
log.Printf("Looking for available SSH port between %d and %d", config.SSHHostPortMin, config.SSHHostPortMax)
|
||||
var sshHostPort uint
|
||||
var offset uint = 0
|
||||
|
||||
portRange := int(config.SSHHostPortMax - config.SSHHostPortMin)
|
||||
if portRange > 0 {
|
||||
// Have to check if > 0 to avoid a panic
|
||||
offset = uint(rand.Intn(portRange))
|
||||
}
|
||||
|
||||
for {
|
||||
sshHostPort = uint(rand.Intn(portRange)) + config.SSHHostPortMin
|
||||
sshHostPort = offset + config.SSHHostPortMin
|
||||
log.Printf("Trying port: %d", sshHostPort)
|
||||
l, err := net.Listen("tcp", fmt.Sprintf(":%d", sshHostPort))
|
||||
if err == nil {
|
||||
|
|
|
@ -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 (
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// stepRun runs the virtual machine
|
||||
|
@ -78,13 +79,12 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error
|
|||
|
||||
defaultArgs["-name"] = vmName
|
||||
defaultArgs["-machine"] = fmt.Sprintf("type=%s", config.MachineType)
|
||||
defaultArgs["-netdev"] = "user,id=user.0"
|
||||
defaultArgs["-netdev"] = fmt.Sprintf("user,id=user.0,hostfwd=tcp::%v-:22", sshHostPort)
|
||||
defaultArgs["-device"] = fmt.Sprintf("%s,netdev=user.0", config.NetDevice)
|
||||
defaultArgs["-drive"] = fmt.Sprintf("file=%s,if=%s", imgPath, config.DiskInterface)
|
||||
defaultArgs["-drive"] = fmt.Sprintf("file=%s,if=%s,cache=%s", imgPath, config.DiskInterface, config.DiskCache)
|
||||
defaultArgs["-cdrom"] = isoPath
|
||||
defaultArgs["-boot"] = bootDrive
|
||||
defaultArgs["-m"] = "512M"
|
||||
defaultArgs["-redir"] = fmt.Sprintf("tcp:%v::22", sshHostPort)
|
||||
defaultArgs["-vnc"] = vnc
|
||||
|
||||
// Append the accelerator to the machine type if it is specified
|
||||
|
|
|
@ -56,6 +56,10 @@ func (a *artifact) String() string {
|
|||
return fmt.Sprintf("VM files in directory: %s", a.dir)
|
||||
}
|
||||
|
||||
func (a *artifact) State(name string) interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *artifact) Destroy() error {
|
||||
return os.RemoveAll(a.dir)
|
||||
}
|
||||
|
|
|
@ -49,11 +49,12 @@ func (d *VBox42Driver) Iso() (string, error) {
|
|||
return "", err
|
||||
}
|
||||
|
||||
DefaultGuestAdditionsRe := regexp.MustCompile("Default Guest Additions ISO:(.*)")
|
||||
DefaultGuestAdditionsRe := regexp.MustCompile("Default Guest Additions ISO:(.+)")
|
||||
|
||||
for _, line := range strings.Split(stdout.String(), "\n") {
|
||||
// Need to trim off CR character when running in windows
|
||||
line = strings.TrimRight(line, "\r")
|
||||
// Trimming whitespaces at this point helps to filter out empty value
|
||||
line = strings.TrimRight(line, " \r")
|
||||
|
||||
matches := DefaultGuestAdditionsRe.FindStringSubmatch(line)
|
||||
if matches == nil {
|
||||
|
@ -66,7 +67,7 @@ func (d *VBox42Driver) Iso() (string, error) {
|
|||
return isoname, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("Cannot find \"Default Guest Additions ISO\" in vboxmanage output")
|
||||
return "", fmt.Errorf("Cannot find \"Default Guest Additions ISO\" in vboxmanage output (or it is empty)")
|
||||
}
|
||||
|
||||
func (d *VBox42Driver) Import(name string, path string, flags []string) error {
|
||||
|
@ -195,12 +196,12 @@ func (d *VBox42Driver) Version() (string, error) {
|
|||
return "", fmt.Errorf("VirtualBox is not properly setup: %s", versionOutput)
|
||||
}
|
||||
|
||||
versionRe := regexp.MustCompile("^[.0-9]+(?:_RC[0-9]+)?")
|
||||
matches := versionRe.FindAllString(versionOutput, 1)
|
||||
if matches == nil {
|
||||
versionRe := regexp.MustCompile("^([.0-9]+)(?:_(?:RC|OSEr)[0-9]+)?")
|
||||
matches := versionRe.FindAllStringSubmatch(versionOutput, 1)
|
||||
if matches == nil || len(matches[0]) != 2 {
|
||||
return "", fmt.Errorf("No version found: %s", versionOutput)
|
||||
}
|
||||
|
||||
log.Printf("VirtualBox version: %s", matches[0])
|
||||
return matches[0], nil
|
||||
log.Printf("VirtualBox version: %s", matches[0][1])
|
||||
return matches[0][1], nil
|
||||
}
|
||||
|
|
|
@ -56,6 +56,10 @@ func (a *localArtifact) String() string {
|
|||
return fmt.Sprintf("VM files in directory: %s", a.dir)
|
||||
}
|
||||
|
||||
func (a *localArtifact) State(name string) interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *localArtifact) Destroy() error {
|
||||
return os.RemoveAll(a.dir)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package common
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
@ -11,6 +12,9 @@ type DriverConfig struct {
|
|||
}
|
||||
|
||||
func (c *DriverConfig) Prepare(t *packer.ConfigTemplate) []error {
|
||||
if c.FusionAppPath == "" {
|
||||
c.FusionAppPath = os.Getenv("FUSION_APP_PATH")
|
||||
}
|
||||
if c.FusionAppPath == "" {
|
||||
c.FusionAppPath = "/Applications/VMware Fusion.app"
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ func (d *Fusion6Driver) Clone(dst, src string) error {
|
|||
if _, _, err := runAndLog(cmd); err != nil {
|
||||
if strings.Contains(err.Error(), "parameters was invalid") {
|
||||
return fmt.Errorf(
|
||||
"Clone is not supported with your version of Fusion. Packer " +
|
||||
"Clone is not supported with your version of Fusion. Packer "+
|
||||
"only works with Fusion %s Professional or above. Please verify your version.", VMWARE_FUSION_VERSION)
|
||||
}
|
||||
|
||||
|
|
|
@ -32,38 +32,27 @@ func (s StepCleanVMX) Run(state multistep.StateBag) multistep.StepAction {
|
|||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
if _, ok := state.GetOk("floppy_path"); ok {
|
||||
// Delete the floppy0 entries so the floppy is no longer mounted
|
||||
ui.Message("Unmounting floppy from VMX...")
|
||||
for k, _ := range vmxData {
|
||||
if strings.HasPrefix(k, "floppy0.") {
|
||||
log.Printf("Deleting key: %s", k)
|
||||
delete(vmxData, k)
|
||||
}
|
||||
// Delete the floppy0 entries so the floppy is no longer mounted
|
||||
ui.Message("Unmounting floppy from VMX...")
|
||||
for k, _ := range vmxData {
|
||||
if strings.HasPrefix(k, "floppy0.") {
|
||||
log.Printf("Deleting key: %s", k)
|
||||
delete(vmxData, k)
|
||||
}
|
||||
vmxData["floppy0.present"] = "FALSE"
|
||||
}
|
||||
vmxData["floppy0.present"] = "FALSE"
|
||||
|
||||
if isoPathRaw, ok := state.GetOk("iso_path"); ok {
|
||||
isoPath := isoPathRaw.(string)
|
||||
devRe := regexp.MustCompile(`^ide\d:\d\.`)
|
||||
for k, v := range vmxData {
|
||||
ide := devRe.FindString(k)
|
||||
if ide == "" || v != "cdrom-image" {
|
||||
continue
|
||||
}
|
||||
|
||||
ui.Message("Detaching ISO from CD-ROM device...")
|
||||
devRe := regexp.MustCompile(`^ide\d:\d\.`)
|
||||
for k, _ := range vmxData {
|
||||
match := devRe.FindString(k)
|
||||
if match == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
filenameKey := match + "filename"
|
||||
if filename, ok := vmxData[filenameKey]; ok {
|
||||
if filename == isoPath {
|
||||
// Change the CD-ROM device back to auto-detect to eject
|
||||
vmxData[filenameKey] = "auto detect"
|
||||
vmxData[match+"devicetype"] = "cdrom-raw"
|
||||
}
|
||||
}
|
||||
}
|
||||
vmxData[ide+"devicetype"] = "cdrom-raw"
|
||||
vmxData[ide+"filename"] = "auto detect"
|
||||
}
|
||||
|
||||
// Rewrite the VMX
|
||||
|
|
|
@ -39,7 +39,6 @@ func TestStepCleanVMX_floppyPath(t *testing.T) {
|
|||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
state.Put("floppy_path", "foo")
|
||||
state.Put("vmx_path", vmxPath)
|
||||
|
||||
// Test the run
|
||||
|
@ -89,7 +88,6 @@ func TestStepCleanVMX_isoPath(t *testing.T) {
|
|||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
state.Put("iso_path", "foo")
|
||||
state.Put("vmx_path", vmxPath)
|
||||
|
||||
// Test the run
|
||||
|
@ -136,6 +134,7 @@ floppy0.filetype = "file"
|
|||
`
|
||||
|
||||
const testVMXISOPath = `
|
||||
ide0:0.devicetype = "cdrom-image"
|
||||
ide0:0.filename = "foo"
|
||||
ide0:1.filename = "bar"
|
||||
foo = "bar"
|
||||
|
|
|
@ -137,10 +137,14 @@ LockWaitLoop:
|
|||
}
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" && !s.Testing {
|
||||
if runtime.GOOS != "darwin" && !s.Testing {
|
||||
// Windows takes a while to yield control of the files when the
|
||||
// process is exiting. We just sleep here. In the future, it'd be
|
||||
// nice to find a better solution to this.
|
||||
// process is exiting. Ubuntu will yield control of the files but
|
||||
// VMWare may overwrite the VMX cleanup steps that run after this,
|
||||
// so we wait to ensure VMWare has exited and flushed the VMX.
|
||||
|
||||
// We just sleep here. In the future, it'd be nice to find a better
|
||||
// solution to this.
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,10 @@ func (a *Artifact) String() string {
|
|||
return fmt.Sprintf("VM files in directory: %s", a.dir)
|
||||
}
|
||||
|
||||
func (a *Artifact) State(name string) interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Artifact) Destroy() error {
|
||||
return a.dir.RemoveAll()
|
||||
}
|
||||
|
|
|
@ -40,17 +40,20 @@ type config struct {
|
|||
ISOChecksum string `mapstructure:"iso_checksum"`
|
||||
ISOChecksumType string `mapstructure:"iso_checksum_type"`
|
||||
ISOUrls []string `mapstructure:"iso_urls"`
|
||||
Version string `mapstructure:"version"`
|
||||
VMName string `mapstructure:"vm_name"`
|
||||
BootCommand []string `mapstructure:"boot_command"`
|
||||
SkipCompaction bool `mapstructure:"skip_compaction"`
|
||||
VMXTemplatePath string `mapstructure:"vmx_template_path"`
|
||||
|
||||
RemoteType string `mapstructure:"remote_type"`
|
||||
RemoteDatastore string `mapstructure:"remote_datastore"`
|
||||
RemoteHost string `mapstructure:"remote_host"`
|
||||
RemotePort uint `mapstructure:"remote_port"`
|
||||
RemoteUser string `mapstructure:"remote_username"`
|
||||
RemotePassword string `mapstructure:"remote_password"`
|
||||
RemoteType string `mapstructure:"remote_type"`
|
||||
RemoteDatastore string `mapstructure:"remote_datastore"`
|
||||
RemoteCacheDatastore string `mapstructure:"remote_cache_datastore"`
|
||||
RemoteCacheDirectory string `mapstructure:"remote_cache_directory"`
|
||||
RemoteHost string `mapstructure:"remote_host"`
|
||||
RemotePort uint `mapstructure:"remote_port"`
|
||||
RemoteUser string `mapstructure:"remote_username"`
|
||||
RemotePassword string `mapstructure:"remote_password"`
|
||||
|
||||
RawSingleISOUrl string `mapstructure:"iso_url"`
|
||||
|
||||
|
@ -110,6 +113,10 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName)
|
||||
}
|
||||
|
||||
if b.config.Version == "" {
|
||||
b.config.Version = "9"
|
||||
}
|
||||
|
||||
if b.config.RemoteUser == "" {
|
||||
b.config.RemoteUser = "root"
|
||||
}
|
||||
|
@ -118,24 +125,34 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
b.config.RemoteDatastore = "datastore1"
|
||||
}
|
||||
|
||||
if b.config.RemoteCacheDatastore == "" {
|
||||
b.config.RemoteCacheDatastore = b.config.RemoteDatastore
|
||||
}
|
||||
|
||||
if b.config.RemoteCacheDirectory == "" {
|
||||
b.config.RemoteCacheDirectory = "packer_cache"
|
||||
}
|
||||
|
||||
if b.config.RemotePort == 0 {
|
||||
b.config.RemotePort = 22
|
||||
}
|
||||
|
||||
// Errors
|
||||
templates := map[string]*string{
|
||||
"disk_name": &b.config.DiskName,
|
||||
"guest_os_type": &b.config.GuestOSType,
|
||||
"iso_checksum": &b.config.ISOChecksum,
|
||||
"iso_checksum_type": &b.config.ISOChecksumType,
|
||||
"iso_url": &b.config.RawSingleISOUrl,
|
||||
"vm_name": &b.config.VMName,
|
||||
"vmx_template_path": &b.config.VMXTemplatePath,
|
||||
"remote_type": &b.config.RemoteType,
|
||||
"remote_host": &b.config.RemoteHost,
|
||||
"remote_datastore": &b.config.RemoteDatastore,
|
||||
"remote_user": &b.config.RemoteUser,
|
||||
"remote_password": &b.config.RemotePassword,
|
||||
"disk_name": &b.config.DiskName,
|
||||
"guest_os_type": &b.config.GuestOSType,
|
||||
"iso_checksum": &b.config.ISOChecksum,
|
||||
"iso_checksum_type": &b.config.ISOChecksumType,
|
||||
"iso_url": &b.config.RawSingleISOUrl,
|
||||
"vm_name": &b.config.VMName,
|
||||
"vmx_template_path": &b.config.VMXTemplatePath,
|
||||
"remote_type": &b.config.RemoteType,
|
||||
"remote_host": &b.config.RemoteHost,
|
||||
"remote_datastore": &b.config.RemoteDatastore,
|
||||
"remote_cache_datastore": &b.config.RemoteCacheDatastore,
|
||||
"remote_cache_directory": &b.config.RemoteCacheDirectory,
|
||||
"remote_user": &b.config.RemoteUser,
|
||||
"remote_password": &b.config.RemotePassword,
|
||||
}
|
||||
|
||||
for n, ptr := range templates {
|
||||
|
|
|
@ -134,6 +134,10 @@ func TestBuilderPrepare_Defaults(t *testing.T) {
|
|||
t.Errorf("bad output dir: %s", b.config.OutputDir)
|
||||
}
|
||||
|
||||
if b.config.Version != "9" {
|
||||
t.Errorf("bad Version: %s", b.config.Version)
|
||||
}
|
||||
|
||||
if b.config.SSHWaitTimeout != (20 * time.Minute) {
|
||||
t.Errorf("bad wait timeout: %s", b.config.SSHWaitTimeout)
|
||||
}
|
||||
|
|
|
@ -17,11 +17,13 @@ func NewDriver(config *config) (vmwcommon.Driver, error) {
|
|||
|
||||
drivers = []vmwcommon.Driver{
|
||||
&ESX5Driver{
|
||||
Host: config.RemoteHost,
|
||||
Port: config.RemotePort,
|
||||
Username: config.RemoteUser,
|
||||
Password: config.RemotePassword,
|
||||
Datastore: config.RemoteDatastore,
|
||||
Host: config.RemoteHost,
|
||||
Port: config.RemotePort,
|
||||
Username: config.RemoteUser,
|
||||
Password: config.RemotePassword,
|
||||
Datastore: config.RemoteDatastore,
|
||||
CacheDatastore: config.RemoteCacheDatastore,
|
||||
CacheDirectory: config.RemoteCacheDirectory,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -22,11 +22,13 @@ import (
|
|||
// ESX5 driver talks to an ESXi5 hypervisor remotely over SSH to build
|
||||
// virtual machines. This driver can only manage one machine at a time.
|
||||
type ESX5Driver struct {
|
||||
Host string
|
||||
Port uint
|
||||
Username string
|
||||
Password string
|
||||
Datastore string
|
||||
Host string
|
||||
Port uint
|
||||
Username string
|
||||
Password string
|
||||
Datastore string
|
||||
CacheDatastore string
|
||||
CacheDirectory string
|
||||
|
||||
comm packer.Communicator
|
||||
outputDir string
|
||||
|
@ -55,7 +57,21 @@ func (d *ESX5Driver) IsRunning(string) (bool, error) {
|
|||
}
|
||||
|
||||
func (d *ESX5Driver) Start(vmxPathLocal string, headless bool) error {
|
||||
return d.sh("vim-cmd", "vmsvc/power.on", d.vmId)
|
||||
for i := 0; i < 20; i++ {
|
||||
err := d.sh("vim-cmd", "vmsvc/power.on", d.vmId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep((time.Duration(i) * time.Second) + 1)
|
||||
running, err := d.IsRunning(vmxPathLocal)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if running {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("Retry limit exceeded")
|
||||
}
|
||||
|
||||
func (d *ESX5Driver) Stop(vmxPathLocal string) error {
|
||||
|
@ -84,13 +100,7 @@ func (d *ESX5Driver) Unregister(vmxPathLocal string) error {
|
|||
}
|
||||
|
||||
func (d *ESX5Driver) UploadISO(localPath string, checksum string, checksumType string) (string, error) {
|
||||
cacheRoot, _ := filepath.Abs(".")
|
||||
targetFile, err := filepath.Rel(cacheRoot, localPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
finalPath := d.datastorePath(targetFile)
|
||||
finalPath := d.cachePath(localPath)
|
||||
if err := d.mkdir(filepath.ToSlash(filepath.Dir(finalPath))); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -300,6 +310,10 @@ func (d *ESX5Driver) datastorePath(path string) string {
|
|||
return filepath.ToSlash(filepath.Join("/vmfs/volumes", d.Datastore, baseDir, filepath.Base(path)))
|
||||
}
|
||||
|
||||
func (d *ESX5Driver) cachePath(path string) string {
|
||||
return filepath.ToSlash(filepath.Join("/vmfs/volumes", d.CacheDatastore, d.CacheDirectory, filepath.Base(path)))
|
||||
}
|
||||
|
||||
func (d *ESX5Driver) connect() error {
|
||||
address := fmt.Sprintf("%s:%d", d.Host, d.Port)
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ type vmxTemplateData struct {
|
|||
GuestOS string
|
||||
DiskName string
|
||||
ISOPath string
|
||||
Version string
|
||||
}
|
||||
|
||||
// This step creates the VMX file for the VM.
|
||||
|
@ -41,6 +42,7 @@ func (s *stepCreateVMX) Run(state multistep.StateBag) multistep.StepAction {
|
|||
Name: config.VMName,
|
||||
GuestOS: config.GuestOSType,
|
||||
DiskName: config.DiskName,
|
||||
Version: config.Version,
|
||||
ISOPath: isoPath,
|
||||
}
|
||||
|
||||
|
@ -180,7 +182,7 @@ tools.upgrade.policy = "upgradeAtPowerCycle"
|
|||
usb.pciSlotNumber = "32"
|
||||
usb.present = "FALSE"
|
||||
virtualHW.productCompatibility = "hosted"
|
||||
virtualHW.version = "9"
|
||||
virtualHW.version = "{{ .Version }}"
|
||||
vmci0.id = "1861462627"
|
||||
vmci0.pciSlotNumber = "35"
|
||||
vmci0.present = "TRUE"
|
||||
|
|
|
@ -37,8 +37,17 @@ func (s *StepCloneVMX) Run(state multistep.StateBag) multistep.StepAction {
|
|||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
diskName, ok := vmxData["scsi0:0.filename"]
|
||||
if !ok {
|
||||
var diskName string
|
||||
if _, ok := vmxData["scsi0:0.filename"]; ok {
|
||||
diskName = vmxData["scsi0:0.filename"]
|
||||
}
|
||||
if _, ok := vmxData["sata0:0.filename"]; ok {
|
||||
diskName = vmxData["sata0:0.filename"]
|
||||
}
|
||||
if _, ok := vmxData["ide0:0.filename"]; ok {
|
||||
diskName = vmxData["ide0:0.filename"]
|
||||
}
|
||||
if diskName == "" {
|
||||
err := fmt.Errorf("Root disk filename could not be found!")
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
|
|
|
@ -6,11 +6,10 @@ import (
|
|||
"path/filepath"
|
||||
|
||||
"github.com/hashicorp/go-checkpoint"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/mitchellh/packer/command"
|
||||
)
|
||||
|
||||
func init() {
|
||||
packer.VersionChecker = packerVersionCheck
|
||||
checkpointResult = make(chan *checkpoint.CheckResponse, 1)
|
||||
}
|
||||
|
||||
|
@ -33,9 +32,9 @@ func runCheckpoint(c *config) {
|
|||
return
|
||||
}
|
||||
|
||||
version := packer.Version
|
||||
if packer.VersionPrerelease != "" {
|
||||
version += fmt.Sprintf(".%s", packer.VersionPrerelease)
|
||||
version := Version
|
||||
if VersionPrerelease != "" {
|
||||
version += fmt.Sprintf(".%s", VersionPrerelease)
|
||||
}
|
||||
|
||||
signaturePath := filepath.Join(configDir, "checkpoint_signature")
|
||||
|
@ -58,21 +57,23 @@ func runCheckpoint(c *config) {
|
|||
checkpointResult <- resp
|
||||
}
|
||||
|
||||
// packerVersionCheck implements packer.VersionCheckFunc and is used
|
||||
// commandVersionCheck implements command.VersionCheckFunc and is used
|
||||
// as the version checker.
|
||||
func packerVersionCheck(current string) (packer.VersionCheckInfo, error) {
|
||||
func commandVersionCheck() (command.VersionCheckInfo, error) {
|
||||
// Wait for the result to come through
|
||||
info := <-checkpointResult
|
||||
if info == nil {
|
||||
var zero packer.VersionCheckInfo
|
||||
var zero command.VersionCheckInfo
|
||||
return zero, nil
|
||||
}
|
||||
|
||||
// Build the alerts that we may have received about our version
|
||||
alerts := make([]string, len(info.Alerts))
|
||||
for i, a := range info.Alerts {
|
||||
alerts[i] = a.Message
|
||||
}
|
||||
|
||||
return packer.VersionCheckInfo{
|
||||
return command.VersionCheckInfo{
|
||||
Outdated: info.Outdated,
|
||||
Latest: info.CurrentVersion,
|
||||
Alerts: alerts,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package build
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
@ -14,16 +14,20 @@ import (
|
|||
"sync"
|
||||
)
|
||||
|
||||
type Command byte
|
||||
|
||||
func (Command) Help() string {
|
||||
return strings.TrimSpace(helpText)
|
||||
type BuildCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c Command) Run(env packer.Environment, args []string) int {
|
||||
func (c BuildCommand) Run(args []string) int {
|
||||
var cfgColor, cfgDebug, cfgForce, cfgParallel bool
|
||||
buildOptions := new(cmdcommon.BuildOptions)
|
||||
|
||||
env, err := c.Meta.Environment()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error initializing environment: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
cmdFlags := flag.NewFlagSet("build", flag.ContinueOnError)
|
||||
cmdFlags.Usage = func() { env.Ui().Say(c.Help()) }
|
||||
cmdFlags.BoolVar(&cfgColor, "color", true, "enable or disable color")
|
||||
|
@ -278,6 +282,28 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
|||
return 0
|
||||
}
|
||||
|
||||
func (Command) Synopsis() string {
|
||||
func (BuildCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: packer build [options] TEMPLATE
|
||||
|
||||
Will execute multiple builds in parallel as defined in the template.
|
||||
The various artifacts created by the template will be outputted.
|
||||
|
||||
Options:
|
||||
|
||||
-debug Debug mode enabled for builds
|
||||
-force Force a build to continue if artifacts exist, deletes existing artifacts
|
||||
-machine-readable Machine-readable output
|
||||
-except=foo,bar,baz Build all builds other than these
|
||||
-only=foo,bar,baz Only build the given builds by name
|
||||
-parallel=false Disable parallelization (on by default)
|
||||
-var 'key=value' Variable for templates, can be used multiple times.
|
||||
-var-file=path JSON file containing user variables.
|
||||
`
|
||||
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (BuildCommand) Synopsis() string {
|
||||
return "build image(s) from template"
|
||||
}
|
|
@ -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 (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/packer/fix"
|
||||
)
|
||||
|
||||
type Command byte
|
||||
|
||||
func (Command) Help() string {
|
||||
return strings.TrimSpace(helpString)
|
||||
type FixCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c Command) Run(env packer.Environment, args []string) int {
|
||||
func (c *FixCommand) Run(args []string) int {
|
||||
env, err := c.Meta.Environment()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error initializing environment: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
cmdFlags := flag.NewFlagSet("fix", flag.ContinueOnError)
|
||||
cmdFlags.Usage = func() { env.Ui().Say(c.Help()) }
|
||||
if err := cmdFlags.Parse(args); err != nil {
|
||||
|
@ -50,9 +55,9 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
|||
tplF.Close()
|
||||
|
||||
input := templateData
|
||||
for _, name := range FixerOrder {
|
||||
for _, name := range fix.FixerOrder {
|
||||
var err error
|
||||
fixer, ok := Fixers[name]
|
||||
fixer, ok := fix.Fixers[name]
|
||||
if !ok {
|
||||
panic("fixer not found: " + name)
|
||||
}
|
||||
|
@ -85,6 +90,30 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
|||
return 0
|
||||
}
|
||||
|
||||
func (c Command) Synopsis() string {
|
||||
func (*FixCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: packer fix [options] TEMPLATE
|
||||
|
||||
Reads the JSON template and attempts to fix known backwards
|
||||
incompatibilities. The fixed template will be outputted to standard out.
|
||||
|
||||
If the template cannot be fixed due to an error, the command will exit
|
||||
with a non-zero exit status. Error messages will appear on standard error.
|
||||
|
||||
Fixes that are run:
|
||||
|
||||
iso-md5 Replaces "iso_md5" in builders with newer "iso_checksum"
|
||||
createtime Replaces ".CreateTime" in builder configs with "{{timestamp}}"
|
||||
virtualbox-gaattach Updates VirtualBox builders using "guest_additions_attach"
|
||||
to use "guest_additions_mode"
|
||||
pp-vagrant-override Replaces old-style provider overrides for the Vagrant
|
||||
post-processor to new-style as of Packer 0.5.0.
|
||||
virtualbox-rename Updates "virtualbox" builders to "virtualbox-iso"
|
||||
`
|
||||
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *FixCommand) Synopsis() string {
|
||||
return "fixes templates from old versions of packer"
|
||||
}
|
|
@ -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 (
|
||||
"flag"
|
||||
|
@ -9,17 +9,17 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
type Command struct{}
|
||||
|
||||
func (Command) Help() string {
|
||||
return strings.TrimSpace(helpText)
|
||||
type InspectCommand struct{
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c Command) Synopsis() string {
|
||||
return "see components of a template"
|
||||
}
|
||||
func (c *InspectCommand) Run(args []string) int {
|
||||
env, err := c.Meta.Environment()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error initializing environment: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
func (c Command) Run(env packer.Environment, args []string) int {
|
||||
flags := flag.NewFlagSet("inspect", flag.ContinueOnError)
|
||||
flags.Usage = func() { env.Ui().Say(c.Help()) }
|
||||
if err := flags.Parse(args); err != nil {
|
||||
|
@ -148,3 +148,23 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
|||
|
||||
return 0
|
||||
}
|
||||
|
||||
func (*InspectCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: packer inspect TEMPLATE
|
||||
|
||||
Inspects a template, parsing and outputting the components a template
|
||||
defines. This does not validate the contents of a template (other than
|
||||
basic syntax by necessity).
|
||||
|
||||
Options:
|
||||
|
||||
-machine-readable Machine-readable output
|
||||
`
|
||||
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *InspectCommand) Synopsis() string {
|
||||
return "see components of a template"
|
||||
}
|
|
@ -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 (
|
||||
"flag"
|
||||
|
@ -9,16 +9,20 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
type Command byte
|
||||
|
||||
func (Command) Help() string {
|
||||
return strings.TrimSpace(helpString)
|
||||
type ValidateCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c Command) Run(env packer.Environment, args []string) int {
|
||||
func (c *ValidateCommand) Run(args []string) int {
|
||||
var cfgSyntaxOnly bool
|
||||
buildOptions := new(cmdcommon.BuildOptions)
|
||||
|
||||
env, err := c.Meta.Environment()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error initializing environment: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
cmdFlags := flag.NewFlagSet("validate", flag.ContinueOnError)
|
||||
cmdFlags.Usage = func() { env.Ui().Say(c.Help()) }
|
||||
cmdFlags.BoolVar(&cfgSyntaxOnly, "syntax-only", false, "check syntax only")
|
||||
|
@ -123,6 +127,29 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
|||
return 0
|
||||
}
|
||||
|
||||
func (Command) Synopsis() string {
|
||||
func (*ValidateCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: packer validate [options] TEMPLATE
|
||||
|
||||
Checks the template is valid by parsing the template and also
|
||||
checking the configuration with the various builders, provisioners, etc.
|
||||
|
||||
If it is not valid, the errors will be shown and the command will exit
|
||||
with a non-zero exit status. If it is valid, it will exit with a zero
|
||||
exit status.
|
||||
|
||||
Options:
|
||||
|
||||
-syntax-only Only check syntax. Do not verify config of the template.
|
||||
-except=foo,bar,baz Validate all builds other than these
|
||||
-only=foo,bar,baz Validate only these builds
|
||||
-var 'key=value' Variable for templates, can be used multiple times.
|
||||
-var-file=path JSON file containing user variables.
|
||||
`
|
||||
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (*ValidateCommand) Synopsis() string {
|
||||
return "check that a template is valid"
|
||||
}
|
|
@ -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 (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// StepDownload downloads a remote file using the download client within
|
||||
|
@ -70,7 +71,7 @@ func (s *StepDownload) Run(state multistep.StateBag) multistep.StepAction {
|
|||
CopyFile: false,
|
||||
Hash: HashForType(s.ChecksumType),
|
||||
Checksum: checksum,
|
||||
UserAgent: packer.VersionString(),
|
||||
UserAgent: "Packer",
|
||||
}
|
||||
|
||||
path, err, retry := s.download(config, state)
|
||||
|
|
38
config.go
38
config.go
|
@ -13,6 +13,9 @@ import (
|
|||
"github.com/mitchellh/packer/packer/plugin"
|
||||
)
|
||||
|
||||
// EnvConfig is the global EnvironmentConfig we use to initialize the CLI.
|
||||
var EnvConfig packer.EnvironmentConfig
|
||||
|
||||
type config struct {
|
||||
DisableCheckpoint bool `json:"disable_checkpoint"`
|
||||
DisableCheckpointSignature bool `json:"disable_checkpoint_signature"`
|
||||
|
@ -20,7 +23,6 @@ type config struct {
|
|||
PluginMaxPort uint
|
||||
|
||||
Builders map[string]string
|
||||
Commands map[string]string
|
||||
PostProcessors map[string]string `json:"post-processors"`
|
||||
Provisioners map[string]string
|
||||
}
|
||||
|
@ -79,15 +81,6 @@ func (c *config) Discover() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Returns an array of defined command names.
|
||||
func (c *config) CommandNames() (result []string) {
|
||||
result = make([]string, 0, len(c.Commands))
|
||||
for name := range c.Commands {
|
||||
result = append(result, name)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// This is a proper packer.BuilderFunc that can be used to load packer.Builder
|
||||
// implementations from the defined plugins.
|
||||
func (c *config) LoadBuilder(name string) (packer.Builder, error) {
|
||||
|
@ -101,19 +94,6 @@ func (c *config) LoadBuilder(name string) (packer.Builder, error) {
|
|||
return c.pluginClient(bin).Builder()
|
||||
}
|
||||
|
||||
// This is a proper packer.CommandFunc that can be used to load packer.Command
|
||||
// implementations from the defined plugins.
|
||||
func (c *config) LoadCommand(name string) (packer.Command, error) {
|
||||
log.Printf("Loading command: %s\n", name)
|
||||
bin, ok := c.Commands[name]
|
||||
if !ok {
|
||||
log.Printf("Command not found: %s\n", name)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return c.pluginClient(bin).Command()
|
||||
}
|
||||
|
||||
// This is a proper implementation of packer.HookFunc that can be used
|
||||
// to load packer.Hook implementations from the defined plugins.
|
||||
func (c *config) LoadHook(name string) (packer.Hook, error) {
|
||||
|
@ -149,14 +129,16 @@ func (c *config) LoadProvisioner(name string) (packer.Provisioner, error) {
|
|||
|
||||
func (c *config) discover(path string) error {
|
||||
var err error
|
||||
err = c.discoverSingle(
|
||||
filepath.Join(path, "packer-builder-*"), &c.Builders)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
if !filepath.IsAbs(path) {
|
||||
path, err = filepath.Abs(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = c.discoverSingle(
|
||||
filepath.Join(path, "packer-command-*"), &c.Commands)
|
||||
filepath.Join(path, "packer-builder-*"), &c.Builders)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/mitchellh/packer/packer/plugin"
|
||||
"github.com/mitchellh/panicwrap"
|
||||
"github.com/mitchellh/prefixedio"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
@ -24,63 +27,82 @@ func main() {
|
|||
|
||||
// realMain is executed from main and returns the exit status to exit with.
|
||||
func realMain() int {
|
||||
// If there is no explicit number of Go threads to use, then set it
|
||||
if os.Getenv("GOMAXPROCS") == "" {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
var wrapConfig panicwrap.WrapConfig
|
||||
|
||||
if !panicwrap.Wrapped(&wrapConfig) {
|
||||
// Determine where logs should go in general (requested by the user)
|
||||
logWriter, err := logOutput()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Couldn't setup log output: %s", err)
|
||||
return 1
|
||||
}
|
||||
if logWriter == nil {
|
||||
logWriter = ioutil.Discard
|
||||
}
|
||||
|
||||
// We always send logs to a temporary file that we use in case
|
||||
// there is a panic. Otherwise, we delete it.
|
||||
logTempFile, err := ioutil.TempFile("", "packer-log")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Couldn't setup logging tempfile: %s", err)
|
||||
return 1
|
||||
}
|
||||
defer os.Remove(logTempFile.Name())
|
||||
defer logTempFile.Close()
|
||||
|
||||
// Tell the logger to log to this file
|
||||
os.Setenv(EnvLog, "")
|
||||
os.Setenv(EnvLogFile, "")
|
||||
|
||||
// Setup the prefixed readers that send data properly to
|
||||
// stdout/stderr.
|
||||
doneCh := make(chan struct{})
|
||||
outR, outW := io.Pipe()
|
||||
go copyOutput(outR, doneCh)
|
||||
|
||||
// Create the configuration for panicwrap and wrap our executable
|
||||
wrapConfig.Handler = panicHandler(logTempFile)
|
||||
wrapConfig.Writer = io.MultiWriter(logTempFile, logWriter)
|
||||
wrapConfig.Stdout = outW
|
||||
exitStatus, err := panicwrap.Wrap(&wrapConfig)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Couldn't start Packer: %s", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
// If >= 0, we're the parent, so just exit
|
||||
if exitStatus >= 0 {
|
||||
// Close the stdout writer so that our copy process can finish
|
||||
outW.Close()
|
||||
|
||||
// Wait for the output copying to finish
|
||||
<-doneCh
|
||||
|
||||
return exitStatus
|
||||
}
|
||||
|
||||
// We're the child, so just close the tempfile we made in order to
|
||||
// save file handles since the tempfile is only used by the parent.
|
||||
logTempFile.Close()
|
||||
}
|
||||
|
||||
// Determine where logs should go in general (requested by the user)
|
||||
logWriter, err := logOutput()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Couldn't setup log output: %s", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
// We also always send logs to a temporary file that we use in case
|
||||
// there is a panic. Otherwise, we delete it.
|
||||
logTempFile, err := ioutil.TempFile("", "packer-log")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Couldn't setup logging tempfile: %s", err)
|
||||
return 1
|
||||
}
|
||||
defer os.Remove(logTempFile.Name())
|
||||
defer logTempFile.Close()
|
||||
|
||||
// Reset the log variables to minimize work in the subprocess
|
||||
os.Setenv("PACKER_LOG", "")
|
||||
os.Setenv("PACKER_LOG_FILE", "")
|
||||
|
||||
// Create the configuration for panicwrap and wrap our executable
|
||||
wrapConfig := &panicwrap.WrapConfig{
|
||||
Handler: panicHandler(logTempFile),
|
||||
Writer: io.MultiWriter(logTempFile, logWriter),
|
||||
}
|
||||
|
||||
exitStatus, err := panicwrap.Wrap(wrapConfig)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Couldn't start Packer: %s", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
if exitStatus >= 0 {
|
||||
return exitStatus
|
||||
}
|
||||
|
||||
// We're the child, so just close the tempfile we made in order to
|
||||
// save file handles since the tempfile is only used by the parent.
|
||||
logTempFile.Close()
|
||||
|
||||
// Call the real main
|
||||
return wrappedMain()
|
||||
}
|
||||
|
||||
// wrappedMain is called only when we're wrapped by panicwrap and
|
||||
// returns the exit status to exit with.
|
||||
func wrappedMain() int {
|
||||
// If there is no explicit number of Go threads to use, then set it
|
||||
if os.Getenv("GOMAXPROCS") == "" {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
}
|
||||
|
||||
log.SetOutput(os.Stderr)
|
||||
|
||||
log.Printf(
|
||||
"Packer Version: %s %s %s",
|
||||
packer.Version, packer.VersionPrerelease, packer.GitCommit)
|
||||
"[INFO] Packer version: %s %s %s",
|
||||
Version, VersionPrerelease, GitCommit)
|
||||
log.Printf("Packer Target OS/Arch: %s %s", runtime.GOOS, runtime.GOARCH)
|
||||
log.Printf("Built with Go Version: %s", runtime.Version())
|
||||
|
||||
|
@ -118,16 +140,14 @@ func wrappedMain() int {
|
|||
defer plugin.CleanupClients()
|
||||
|
||||
// Create the environment configuration
|
||||
envConfig := packer.DefaultEnvironmentConfig()
|
||||
envConfig.Cache = cache
|
||||
envConfig.Commands = config.CommandNames()
|
||||
envConfig.Components.Builder = config.LoadBuilder
|
||||
envConfig.Components.Command = config.LoadCommand
|
||||
envConfig.Components.Hook = config.LoadHook
|
||||
envConfig.Components.PostProcessor = config.LoadPostProcessor
|
||||
envConfig.Components.Provisioner = config.LoadProvisioner
|
||||
EnvConfig = *packer.DefaultEnvironmentConfig()
|
||||
EnvConfig.Cache = cache
|
||||
EnvConfig.Components.Builder = config.LoadBuilder
|
||||
EnvConfig.Components.Hook = config.LoadHook
|
||||
EnvConfig.Components.PostProcessor = config.LoadPostProcessor
|
||||
EnvConfig.Components.Provisioner = config.LoadProvisioner
|
||||
if machineReadable {
|
||||
envConfig.Ui = &packer.MachineReadableUi{
|
||||
EnvConfig.Ui = &packer.MachineReadableUi{
|
||||
Writer: os.Stdout,
|
||||
}
|
||||
|
||||
|
@ -139,17 +159,18 @@ func wrappedMain() int {
|
|||
}
|
||||
}
|
||||
|
||||
env, err := packer.NewEnvironment(envConfig)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Packer initialization error: \n\n%s\n", err)
|
||||
return 1
|
||||
//setupSignalHandlers(env)
|
||||
|
||||
cli := &cli.CLI{
|
||||
Args: args,
|
||||
Commands: Commands,
|
||||
HelpFunc: cli.BasicHelpFunc("packer"),
|
||||
HelpWriter: os.Stdout,
|
||||
}
|
||||
|
||||
setupSignalHandlers(env)
|
||||
|
||||
exitCode, err := env.Cli(args)
|
||||
exitCode, err := cli.Run()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error executing CLI: %s\n", err.Error())
|
||||
fmt.Fprintf(os.Stderr, "Error executing CLI: %s\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
|
@ -220,20 +241,44 @@ func loadConfig() (*config, error) {
|
|||
return &config, nil
|
||||
}
|
||||
|
||||
// logOutput determines where we should send logs (if anywhere).
|
||||
func logOutput() (logOutput io.Writer, err error) {
|
||||
logOutput = ioutil.Discard
|
||||
if os.Getenv("PACKER_LOG") != "" {
|
||||
logOutput = os.Stderr
|
||||
// copyOutput uses output prefixes to determine whether data on stdout
|
||||
// should go to stdout or stderr. This is due to panicwrap using stderr
|
||||
// as the log and error channel.
|
||||
func copyOutput(r io.Reader, doneCh chan<- struct{}) {
|
||||
defer close(doneCh)
|
||||
|
||||
if logPath := os.Getenv("PACKER_LOG_PATH"); logPath != "" {
|
||||
var err error
|
||||
logOutput, err = os.Create(logPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
pr, err := prefixedio.NewReader(r)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return
|
||||
stderrR, err := pr.Prefix(ErrorPrefix)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
stdoutR, err := pr.Prefix(OutputPrefix)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defaultR, err := pr.Prefix("")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(3)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
io.Copy(os.Stderr, stderrR)
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
io.Copy(os.Stdout, stdoutR)
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
io.Copy(os.Stdout, defaultR)
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
}
|
|
@ -25,6 +25,10 @@ type Artifact interface {
|
|||
// This is used for UI output. It can be multiple lines.
|
||||
String() string
|
||||
|
||||
// State allows the caller to ask for builder specific state information
|
||||
// relating to the artifact instance.
|
||||
State(name string) interface{}
|
||||
|
||||
// Destroy deletes the artifact. Packer calls this for various reasons,
|
||||
// such as if a post-processor has processed this artifact and it is
|
||||
// no longer needed.
|
||||
|
|
|
@ -5,6 +5,7 @@ type MockArtifact struct {
|
|||
BuilderIdValue string
|
||||
FilesValue []string
|
||||
IdValue string
|
||||
StateValues map[string]interface{}
|
||||
DestroyCalled bool
|
||||
}
|
||||
|
||||
|
@ -37,6 +38,11 @@ func (*MockArtifact) String() string {
|
|||
return "string"
|
||||
}
|
||||
|
||||
func (a *MockArtifact) State(name string) interface{} {
|
||||
value, _ := a.StateValues[name]
|
||||
return value
|
||||
}
|
||||
|
||||
func (a *MockArtifact) Destroy() error {
|
||||
a.DestroyCalled = true
|
||||
return nil
|
||||
|
|
|
@ -2,6 +2,7 @@ package packer
|
|||
|
||||
type TestArtifact struct {
|
||||
id string
|
||||
state map[string]interface{}
|
||||
destroyCalled bool
|
||||
}
|
||||
|
||||
|
@ -26,6 +27,11 @@ func (*TestArtifact) String() string {
|
|||
return "string"
|
||||
}
|
||||
|
||||
func (a *TestArtifact) State(name string) interface{} {
|
||||
value, _ := a.state[name]
|
||||
return value
|
||||
}
|
||||
|
||||
func (a *TestArtifact) Destroy() error {
|
||||
a.destroyCalled = true
|
||||
return nil
|
||||
|
|
|
@ -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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// The function type used to lookup Builder implementations.
|
||||
type BuilderFunc func(name string) (Builder, error)
|
||||
|
||||
// The function type used to lookup Command implementations.
|
||||
type CommandFunc func(name string) (Command, error)
|
||||
|
||||
// The function type used to lookup Hook implementations.
|
||||
type HookFunc func(name string) (Hook, error)
|
||||
|
||||
|
@ -31,7 +24,6 @@ type ProvisionerFunc func(name string) (Provisioner, error)
|
|||
// commands, etc.
|
||||
type ComponentFinder struct {
|
||||
Builder BuilderFunc
|
||||
Command CommandFunc
|
||||
Hook HookFunc
|
||||
PostProcessor PostProcessorFunc
|
||||
Provisioner ProvisionerFunc
|
||||
|
@ -45,7 +37,6 @@ type ComponentFinder struct {
|
|||
type Environment interface {
|
||||
Builder(string) (Builder, error)
|
||||
Cache() Cache
|
||||
Cli([]string) (int, error)
|
||||
Hook(string) (Hook, error)
|
||||
PostProcessor(string) (PostProcessor, error)
|
||||
Provisioner(string) (Provisioner, error)
|
||||
|
@ -56,7 +47,6 @@ type Environment interface {
|
|||
// environment.
|
||||
type coreEnvironment struct {
|
||||
cache Cache
|
||||
commands []string
|
||||
components ComponentFinder
|
||||
ui Ui
|
||||
}
|
||||
|
@ -64,22 +54,14 @@ type coreEnvironment struct {
|
|||
// This struct configures new environments.
|
||||
type EnvironmentConfig struct {
|
||||
Cache Cache
|
||||
Commands []string
|
||||
Components ComponentFinder
|
||||
Ui Ui
|
||||
}
|
||||
|
||||
type helpCommandEntry struct {
|
||||
i int
|
||||
key string
|
||||
synopsis string
|
||||
}
|
||||
|
||||
// DefaultEnvironmentConfig returns a default EnvironmentConfig that can
|
||||
// be used to create a new enviroment with NewEnvironment with sane defaults.
|
||||
func DefaultEnvironmentConfig() *EnvironmentConfig {
|
||||
config := &EnvironmentConfig{}
|
||||
config.Commands = make([]string, 0)
|
||||
config.Ui = &BasicUi{
|
||||
Reader: os.Stdin,
|
||||
Writer: os.Stdout,
|
||||
|
@ -98,7 +80,6 @@ func NewEnvironment(config *EnvironmentConfig) (resultEnv Environment, err error
|
|||
|
||||
env := &coreEnvironment{}
|
||||
env.cache = config.Cache
|
||||
env.commands = config.Commands
|
||||
env.components = config.Components
|
||||
env.ui = config.Ui
|
||||
|
||||
|
@ -109,10 +90,6 @@ func NewEnvironment(config *EnvironmentConfig) (resultEnv Environment, err error
|
|||
env.components.Builder = func(string) (Builder, error) { return nil, nil }
|
||||
}
|
||||
|
||||
if env.components.Command == nil {
|
||||
env.components.Command = func(string) (Command, error) { return nil, nil }
|
||||
}
|
||||
|
||||
if env.components.Hook == nil {
|
||||
env.components.Hook = func(string) (Hook, error) { return nil, nil }
|
||||
}
|
||||
|
@ -199,159 +176,6 @@ func (e *coreEnvironment) Provisioner(name string) (p Provisioner, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// Executes a command as if it was typed on the command-line interface.
|
||||
// The return value is the exit code of the command.
|
||||
func (e *coreEnvironment) Cli(args []string) (result int, err error) {
|
||||
log.Printf("Environment.Cli: %#v\n", args)
|
||||
|
||||
// If we have no arguments, just short-circuit here and print the help
|
||||
if len(args) == 0 {
|
||||
e.printHelp()
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
// This variable will track whether or not we're supposed to print
|
||||
// the help or not.
|
||||
isHelp := false
|
||||
for _, arg := range args {
|
||||
if arg == "-h" || arg == "--help" {
|
||||
isHelp = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Trim up to the command name
|
||||
for i, v := range args {
|
||||
if len(v) > 0 && v[0] != '-' {
|
||||
args = args[i:]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("command + args: %#v", args)
|
||||
|
||||
version := args[0] == "version"
|
||||
if !version {
|
||||
for _, arg := range args {
|
||||
if arg == "--version" || arg == "-v" {
|
||||
version = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var command Command
|
||||
if version {
|
||||
command = new(versionCommand)
|
||||
}
|
||||
|
||||
if command == nil {
|
||||
command, err = e.components.Command(args[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// If we still don't have a command, show the help.
|
||||
if command == nil {
|
||||
e.ui.Error(fmt.Sprintf("Unknown command: %s\n", args[0]))
|
||||
e.printHelp()
|
||||
return 1, nil
|
||||
}
|
||||
}
|
||||
|
||||
// If we're supposed to print help, then print the help of the
|
||||
// command rather than running it.
|
||||
if isHelp {
|
||||
e.ui.Say(command.Help())
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
log.Printf("Executing command: %s\n", args[0])
|
||||
return command.Run(e, args[1:]), nil
|
||||
}
|
||||
|
||||
// Prints the CLI help to the UI.
|
||||
func (e *coreEnvironment) printHelp() {
|
||||
// Created a sorted slice of the map keys and record the longest
|
||||
// command name so we can better format the output later.
|
||||
maxKeyLen := 0
|
||||
for _, command := range e.commands {
|
||||
if len(command) > maxKeyLen {
|
||||
maxKeyLen = len(command)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the keys
|
||||
sort.Strings(e.commands)
|
||||
|
||||
// Create the communication/sync mechanisms to get the synopsis' of
|
||||
// the various commands. We do this in parallel since the overhead
|
||||
// of the subprocess underneath is very expensive and this speeds things
|
||||
// up an incredible amount.
|
||||
var wg sync.WaitGroup
|
||||
ch := make(chan *helpCommandEntry)
|
||||
|
||||
for i, key := range e.commands {
|
||||
wg.Add(1)
|
||||
|
||||
// Get the synopsis in a goroutine since it may take awhile
|
||||
// to subprocess out.
|
||||
go func(i int, key string) {
|
||||
defer wg.Done()
|
||||
var synopsis string
|
||||
command, err := e.components.Command(key)
|
||||
if err != nil {
|
||||
synopsis = fmt.Sprintf("Error loading command: %s", err.Error())
|
||||
} else if command == nil {
|
||||
return
|
||||
} else {
|
||||
synopsis = command.Synopsis()
|
||||
}
|
||||
|
||||
// Pad the key with spaces so that they're all the same width
|
||||
key = fmt.Sprintf("%s%s", key, strings.Repeat(" ", maxKeyLen-len(key)))
|
||||
|
||||
// Output the command and the synopsis
|
||||
ch <- &helpCommandEntry{
|
||||
i: i,
|
||||
key: key,
|
||||
synopsis: synopsis,
|
||||
}
|
||||
}(i, key)
|
||||
}
|
||||
|
||||
e.ui.Say("usage: packer [--version] [--help] <command> [<args>]\n")
|
||||
e.ui.Say("Available commands are:")
|
||||
|
||||
// Make a goroutine that just waits for all the synopsis gathering
|
||||
// to complete, and then output it.
|
||||
synopsisDone := make(chan struct{})
|
||||
go func() {
|
||||
defer close(synopsisDone)
|
||||
entries := make([]string, len(e.commands))
|
||||
|
||||
for entry := range ch {
|
||||
e.ui.Machine("command", entry.key, entry.synopsis)
|
||||
message := fmt.Sprintf(" %s %s", entry.key, entry.synopsis)
|
||||
entries[entry.i] = message
|
||||
}
|
||||
|
||||
for _, message := range entries {
|
||||
if message != "" {
|
||||
e.ui.Say(message)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait to complete getting the synopsis' then close the channel
|
||||
wg.Wait()
|
||||
close(ch)
|
||||
<-synopsisDone
|
||||
|
||||
e.ui.Say("\nGlobally recognized options:")
|
||||
e.ui.Say(" -machine-readable Machine-readable output format.")
|
||||
}
|
||||
|
||||
// Returns the UI for the environment. The UI is the interface that should
|
||||
// be used for all communication with the outside world.
|
||||
func (e *coreEnvironment) Ui() Ui {
|
||||
|
|
|
@ -6,8 +6,6 @@ import (
|
|||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
@ -43,13 +41,6 @@ func testEnvironment() Environment {
|
|||
return env
|
||||
}
|
||||
|
||||
func TestEnvironment_DefaultConfig_Commands(t *testing.T) {
|
||||
config := DefaultEnvironmentConfig()
|
||||
if len(config.Commands) != 0 {
|
||||
t.Fatalf("bad: %#v", config.Commands)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvironment_DefaultConfig_Ui(t *testing.T) {
|
||||
config := DefaultEnvironmentConfig()
|
||||
if config.Ui == nil {
|
||||
|
@ -91,7 +82,6 @@ func TestEnvironment_NilComponents(t *testing.T) {
|
|||
// anything but if there is a panic in the test then yeah, something
|
||||
// went wrong.
|
||||
env.Builder("foo")
|
||||
env.Cli([]string{"foo"})
|
||||
env.Hook("foo")
|
||||
env.PostProcessor("foo")
|
||||
env.Provisioner("foo")
|
||||
|
@ -154,117 +144,6 @@ func TestEnvironment_Cache(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestEnvironment_Cli_Error(t *testing.T) {
|
||||
config := DefaultEnvironmentConfig()
|
||||
config.Components.Command = func(n string) (Command, error) { return nil, errors.New("foo") }
|
||||
|
||||
env, _ := NewEnvironment(config)
|
||||
_, err := env.Cli([]string{"foo"})
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
if err.Error() != "foo" {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvironment_Cli_CallsRun(t *testing.T) {
|
||||
command := &TestCommand{}
|
||||
commands := make(map[string]Command)
|
||||
commands["foo"] = command
|
||||
|
||||
config := &EnvironmentConfig{}
|
||||
config.Commands = []string{"foo"}
|
||||
config.Components.Command = func(n string) (Command, error) { return commands[n], nil }
|
||||
|
||||
env, _ := NewEnvironment(config)
|
||||
exitCode, err := env.Cli([]string{"foo", "bar", "baz"})
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("bad: %d", exitCode)
|
||||
}
|
||||
if !command.runCalled {
|
||||
t.Fatal("command should be run")
|
||||
}
|
||||
if command.runEnv != env {
|
||||
t.Fatalf("bad env: %#v", command.runEnv)
|
||||
}
|
||||
if !reflect.DeepEqual(command.runArgs, []string{"bar", "baz"}) {
|
||||
t.Fatalf("bad: %#v", command.runArgs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvironment_DefaultCli_Empty(t *testing.T) {
|
||||
defaultEnv := testEnvironment()
|
||||
|
||||
// Test with no args
|
||||
exitCode, _ := defaultEnv.Cli([]string{})
|
||||
if exitCode != 1 {
|
||||
t.Fatalf("bad: %d", exitCode)
|
||||
}
|
||||
|
||||
// Test with only blank args
|
||||
exitCode, _ = defaultEnv.Cli([]string{""})
|
||||
if exitCode != 1 {
|
||||
t.Fatalf("bad: %d", exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvironment_DefaultCli_Help(t *testing.T) {
|
||||
defaultEnv := testEnvironment()
|
||||
|
||||
// A little lambda to help us test the output actually contains help
|
||||
testOutput := func() {
|
||||
buffer := defaultEnv.Ui().(*BasicUi).Writer.(*bytes.Buffer)
|
||||
output := buffer.String()
|
||||
buffer.Reset()
|
||||
if !strings.Contains(output, "usage: packer") {
|
||||
t.Fatalf("should contain help: %#v", output)
|
||||
}
|
||||
}
|
||||
|
||||
// Test "--help"
|
||||
exitCode, _ := defaultEnv.Cli([]string{"--help"})
|
||||
if exitCode != 1 {
|
||||
t.Fatalf("bad: %d", exitCode)
|
||||
}
|
||||
testOutput()
|
||||
|
||||
// Test "-h"
|
||||
exitCode, _ = defaultEnv.Cli([]string{"--help"})
|
||||
if exitCode != 1 {
|
||||
t.Fatalf("bad: %d", exitCode)
|
||||
}
|
||||
testOutput()
|
||||
}
|
||||
|
||||
func TestEnvironment_DefaultCli_Version(t *testing.T) {
|
||||
defaultEnv := testEnvironment()
|
||||
|
||||
versionCommands := []string{"version", "--version", "-v"}
|
||||
for _, command := range versionCommands {
|
||||
exitCode, _ := defaultEnv.Cli([]string{command})
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("bad: %d", exitCode)
|
||||
}
|
||||
|
||||
// Test the --version and -v can appear anywhere
|
||||
exitCode, _ = defaultEnv.Cli([]string{"bad", command})
|
||||
|
||||
if command != "version" {
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("bad: %d", exitCode)
|
||||
}
|
||||
} else {
|
||||
if exitCode != 1 {
|
||||
t.Fatalf("bad: %d", exitCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvironment_Hook(t *testing.T) {
|
||||
hook := &MockHook{}
|
||||
hooks := make(map[string]Hook)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue