Compare commits
181 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 542cb1d872 | |||
| 58f0b8c22e | |||
| 09df6a733d | |||
| 64d5ef8709 | |||
| 49a6436c18 | |||
| 7dbd42c7f3 | |||
| 1e47421ba1 | |||
| 0e8afa37d5 | |||
| 01a0b0c766 | |||
| 9db844a807 | |||
| b8510f35fc | |||
| cc2e30826b | |||
| b32449cdcc | |||
| 674ff489a9 | |||
| 026e543b24 | |||
| 134cf3a141 | |||
| 279fb7a632 | |||
| 419a8c3324 | |||
| b0589c9643 | |||
| 04de86d211 | |||
| 1c3bc70b12 | |||
| 3f7c4f7406 | |||
| 05897c8653 | |||
| f7ba933809 | |||
| b9411712eb | |||
| 3cda840504 | |||
| 94a7a07c87 | |||
| 0673bb2c57 | |||
| ad9349a6c5 | |||
| ab939989ea | |||
| f751e20b7e | |||
| e4faa98b6f | |||
| e251adb37e | |||
| 4da79a8837 | |||
| c8a60c77d1 | |||
| 440f1f5f3c | |||
| 472b5ba710 | |||
| 1c030cb54b | |||
| 944c00900e | |||
| c12e9eea3a | |||
| b6b3cc901c | |||
| b37dee1a67 | |||
| 5eccbc702d | |||
| 3167d5d52c | |||
| 0f7065f6b1 | |||
| 7e2d86731e | |||
| 3f04b8193f | |||
| 4fae17767d | |||
| 5cc442872e | |||
| de336ef15e | |||
| b04b8b3857 | |||
| 560b15de00 | |||
| f6be550f1a | |||
| e93eda2c88 | |||
| 4f46bd74ab | |||
| 31c8acc5bc | |||
| 9b69790514 | |||
| 75f574bd4b | |||
| cb5c896c01 | |||
| 9f702af6d9 | |||
| bbc52f9a7d | |||
| be21850e32 | |||
| 7e7e486901 | |||
| 2a9f49a5c6 | |||
| 37b3305301 | |||
| bd3339f1f9 | |||
| 9fe1366eeb | |||
| b449af84ee | |||
| ee430d9a40 | |||
| ae59b81f44 | |||
| 9bada2e35d | |||
| bc69f3e1c3 | |||
| 36c1e8d838 | |||
| 8526244285 | |||
| c207451f7e | |||
| 956bc35769 | |||
| 62865cf37b | |||
| 203b6ca8b4 | |||
| c7278e871c | |||
| e0ed7b6742 | |||
| f2439625bf | |||
| 22e3f09db3 | |||
| 2a613dd6e9 | |||
| 3ae5a912d4 | |||
| 51b46b851a | |||
| 37f474ceca | |||
| d318f3942b | |||
| 591533cee0 | |||
| 49889802b9 | |||
| 7501b1434f | |||
| 61772d715d | |||
| 3fd589fa72 | |||
| e2d8c0bfd7 | |||
| 07c0c599e0 | |||
| c2b39b8b3b | |||
| 5057220ad2 | |||
| dc848ea5d7 | |||
| 228829139b | |||
| 34beaaabe4 | |||
| 1ee4517370 | |||
| fdf028c463 | |||
| b1fffee614 | |||
| b4d1c77cc0 | |||
| d7b93aa343 | |||
| e56d7f7234 | |||
| 608b7cb7a3 | |||
| 90baa006da | |||
| 3704a053d0 | |||
| 8d8b880ac4 | |||
| e8c655bf15 | |||
| 0d8e9f50bf | |||
| 9787e787d4 | |||
| 68516fc05c | |||
| a7d9d62996 | |||
| 8add176ab7 | |||
| 7a78b47e83 | |||
| 6b41a1663a | |||
| af7131b169 | |||
| 52892699ca | |||
| ad21367b21 | |||
| 9f93fb2748 | |||
| ee2ed063e9 | |||
| 2d3f84abf0 | |||
| 36546c5dfe | |||
| 6c2e60ffa9 | |||
| 26aa0144e6 | |||
| 52b55fb899 | |||
| 6d700d34df | |||
| 8621949e01 | |||
| 9f1ec211a0 | |||
| b404cfc365 | |||
| f7de056f23 | |||
| 5af1c89e6e | |||
| 5e8eade205 | |||
| 6857e937e2 | |||
| 482ba644a8 | |||
| b0abd1fbf3 | |||
| 6cac03044f | |||
| 47e03a815f | |||
| f0b1d3cbf2 | |||
| 6921cc93a9 | |||
| 4704565869 | |||
| 25f51b9f99 | |||
| ca180d27fb | |||
| e7e84f04e7 | |||
| dd3e2c255c | |||
| 074a74ec38 | |||
| 55bfee509a | |||
| 2bc4e6aec6 | |||
| 4d10d668ee | |||
| 60c7d22ffb | |||
| eef4fc7a01 | |||
| 6ea64bc378 | |||
| 888f4d440c | |||
| 63a3bb0105 | |||
| 864ea7084d | |||
| ce760eec09 | |||
| 0a08920816 | |||
| 20c2524881 | |||
| 3626fbd0f9 | |||
| 3d3e933f48 | |||
| 4a863379fa | |||
| bd4ce90728 | |||
| aaee600be0 | |||
| 8975f67901 | |||
| 91d2cb8c83 | |||
| c1892b60af | |||
| 3f559f8782 | |||
| 3ce05c7c26 | |||
| be30415395 | |||
| 9cbeae2c5a | |||
| 7947e52089 | |||
| f5b13e3cb5 | |||
| 9d2f98394e | |||
| 0c37a3c68f | |||
| 4d3f9cc1af | |||
| 4894873971 | |||
| 062c62eed8 | |||
| c0c5c6afac | |||
| 76d1bf567e | |||
| 444fcd12c9 |
+59
-1
@@ -1,4 +1,62 @@
|
||||
## 1.3.4 (upcoming)
|
||||
## 1.3.5 (February 28, 2019)
|
||||
|
||||
### IMPROVEMENTS:
|
||||
* builder/alicloud: Update aliyun sdk to support eu-west-1 region [GH-7338]
|
||||
* builder/amazon: AWS users can now use the Vault AWS engine to generate
|
||||
temporary credentials. [GH-7282]
|
||||
* builder/azure: IMDS to get subscription for Azure MSI [GH-7332]
|
||||
* builder/openstack: Replaced deprecated compute/ api with imageservice/
|
||||
[GH-7038]
|
||||
* builder/virtualbox: New "guest_additions_interface" option to enable
|
||||
attaching via a SATA interface. [GH-7298]
|
||||
* builder/vmware: Add `cores` option for specifying the number of cores per
|
||||
socket. [GH-7191]
|
||||
* bulder/openstac: Deprecated compute/v2/images API [GH-7268]
|
||||
* core: Add validation check to help folks who swap their iso_path and
|
||||
checksum_path [GH-7311]
|
||||
* fixer/amazon: Make the amazon-private-ip fixer errors more visible [GH-7336]
|
||||
* post-processor/googlecompute-export: Extend auth for the GCE-post-processors
|
||||
to act like the GCE builder. [GH-7222]
|
||||
* post-processor/googlecompute-import: Extend auth for the GCE-post-processors
|
||||
to act like the GCE builder. [GH-7222]
|
||||
* post-processor/manifest: Add "custom_data" key to packer manifest post-
|
||||
processor [GH-7248]
|
||||
|
||||
### BUG FIXES:
|
||||
* builder/amazon: Fix support for aws-us-gov [GH-7347]
|
||||
* builder/amazon: Move snapshot deletion to cleanup phase. [GH-7343]
|
||||
* builder/azure: Fixed Azure interactive authentication [GH-7276]
|
||||
* builder/cloudstack: Updated sdk version; can now use ostype name in
|
||||
template_os option. [GH-7264]
|
||||
* builder/google: Change metadata url to use a FQDN fixing bug stemming from
|
||||
differing DNS/search domains. [GH-7260]
|
||||
* builder/hyper-v: Fix integer overflows in 32-bit builds [GH-7251]
|
||||
* builder/hyper-v: Fix regression where we improperly handled spaces in switch
|
||||
names [GH-7266]
|
||||
* builder/openstack: Pass context So we know to cancel during WaitForImage
|
||||
[GH-7341]
|
||||
* builder/vmware-esxi: Strip \r\n whitespace from end of names of
|
||||
files stored on esxi. [GH-7310]
|
||||
* builder/vmware: Add "--noSSLVerify" to args in ovftool Validation [GH-7314]
|
||||
* core: clean up Makefile [GH-7254][GH-7265]
|
||||
* core: Fixes mismatches in checksums for dependencies for Go 1.11.4+ [GH-7261]
|
||||
* core: make sure 'only' option is completely ignored by post-processors
|
||||
[GH-7262]
|
||||
* core: name a post-processor to its type when it is not named [GH-7330]
|
||||
* provisioner/salt: Force powershell to overwrite duplicate files [GH-7281]
|
||||
|
||||
### Features:
|
||||
* **new builder** `vagrant` allows users to call vagrant to provision starting
|
||||
from vagrant boxes and save them as new vagrant boxes. [GH-7221]
|
||||
* **new builder:** `hyperone` for building new images on HyperOne Platform on
|
||||
top of existing image or from the scratch with the use of chroot. [GH-7294]
|
||||
* **new post-processor** `digitalocean-import`Add digitalocean-import post-
|
||||
processor. [GH-7060]
|
||||
* **new provisioner**`inspec` Added inspec.io provisioner [GH-7180]
|
||||
* communicator: Add configurable pause after communicator can connect but
|
||||
before it performs provisioning tasks [GH-7317] [GH-7351]
|
||||
|
||||
## 1.3.4 (January 30, 2019)
|
||||
### IMPROVEMENTS:
|
||||
* builder/alicloud: delete copied image and snapshots if corresponding options
|
||||
are specified [GH-7050]
|
||||
|
||||
+2
-1
@@ -5,7 +5,7 @@
|
||||
/builder/alicloud/ @chhaj5236
|
||||
/builder/amazon/ebssurrogate/ @jen20
|
||||
/builder/amazon/ebsvolume/ @jen20
|
||||
/builder/azure/ @boumenot
|
||||
/builder/azure/ @paulmey
|
||||
/builder/hyperv/ @taliesins
|
||||
/builder/lxc/ @ChrisLundquist
|
||||
/builder/lxd/ @ChrisLundquist
|
||||
@@ -16,6 +16,7 @@
|
||||
/builder/ncloud/ @YuSungDuk
|
||||
/builder/scaleway/ @sieben @mvaude @jqueuniet @fflorens @brmzkw
|
||||
/builder/hcloud/ @LKaemmerling
|
||||
/builder/hyperone @m110 @gregorybrzeski @ad-m
|
||||
|
||||
# provisioners
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ GOPATH=$(shell go env GOPATH)
|
||||
# gofmt
|
||||
UNFORMATTED_FILES=$(shell find . -not -path "./vendor/*" -name "*.go" | xargs gofmt -s -l)
|
||||
|
||||
EXECUTABLE_FILES=$(shell find . -type f -executable | egrep -v '^\./(website/[vendor|tmp]|vendor/|\.git|bin/|scripts/|pkg/)' | egrep -v '.*(\.sh|\.bats|\.git)' | egrep -v './provisioner/ansible/test-fixtures/exit1')
|
||||
EXECUTABLE_FILES=$(shell find . -type f -executable | egrep -v '^\./(website/[vendor|tmp]|vendor/|\.git|bin/|scripts/|pkg/)' | egrep -v '.*(\.sh|\.bats|\.git)' | egrep -v './provisioner/(ansible|inspec)/test-fixtures/exit1')
|
||||
|
||||
# Get the git commit
|
||||
GIT_DIRTY=$(shell test -n "`git status --porcelain`" && echo "+CHANGES" || true)
|
||||
@@ -101,7 +101,7 @@ generate: deps ## Generate dynamically generated code
|
||||
goimports -w common/bootcommand/boot_command.go
|
||||
gofmt -w command/plugin.go
|
||||
|
||||
test: fmt-check mode-check vet ## Run unit tests
|
||||
test: fmt-check mode-check ## Run unit tests
|
||||
@go test $(TEST) $(TESTARGS) -timeout=2m
|
||||
|
||||
# testacc runs acceptance tests
|
||||
|
||||
@@ -205,6 +205,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
// Setup the state bag and initial state for the steps
|
||||
state := new(multistep.BasicStateBag)
|
||||
state.Put("config", &b.config)
|
||||
state.Put("access_config", &b.config.AccessConfig)
|
||||
state.Put("ami_config", &b.config.AMIConfig)
|
||||
state.Put("ec2", ec2conn)
|
||||
state.Put("awsSession", session)
|
||||
state.Put("hook", hook)
|
||||
|
||||
@@ -16,8 +16,21 @@ import (
|
||||
cleanhttp "github.com/hashicorp/go-cleanhttp"
|
||||
commonhelper "github.com/hashicorp/packer/helper/common"
|
||||
"github.com/hashicorp/packer/template/interpolate"
|
||||
vaultapi "github.com/hashicorp/vault/api"
|
||||
)
|
||||
|
||||
type VaultAWSEngineOptions struct {
|
||||
Name string `mapstructure:"name"`
|
||||
RoleARN string `mapstructure:"role_arn"`
|
||||
TTL string `mapstructure:"ttl"`
|
||||
EngineName string `mapstructure:"engine_name"`
|
||||
}
|
||||
|
||||
func (v *VaultAWSEngineOptions) Empty() bool {
|
||||
return len(v.Name) == 0 && len(v.RoleARN) == 0 &&
|
||||
len(v.EngineName) == 0 && len(v.TTL) == 0
|
||||
}
|
||||
|
||||
// AccessConfig is for common configuration related to AWS access
|
||||
type AccessConfig struct {
|
||||
AccessKey string `mapstructure:"access_key"`
|
||||
@@ -32,6 +45,7 @@ type AccessConfig struct {
|
||||
SkipMetadataApiCheck bool `mapstructure:"skip_metadata_api_check"`
|
||||
Token string `mapstructure:"token"`
|
||||
session *session.Session
|
||||
VaultAWSEngine VaultAWSEngineOptions `mapstructure:"vault_aws_engine"`
|
||||
|
||||
getEC2Connection func() ec2iface.EC2API
|
||||
}
|
||||
@@ -44,6 +58,7 @@ func (c *AccessConfig) Session() (*session.Session, error) {
|
||||
}
|
||||
|
||||
config := aws.NewConfig().WithCredentialsChainVerboseErrors(true)
|
||||
|
||||
staticCreds := credentials.NewStaticCredentials(c.AccessKey, c.SecretKey, c.Token)
|
||||
if _, err := staticCreds.Get(); err != credentials.ErrStaticCredentialsEmpty {
|
||||
config.WithCredentials(staticCreds)
|
||||
@@ -122,14 +137,62 @@ func (c *AccessConfig) IsChinaCloud() bool {
|
||||
return strings.HasPrefix(c.SessionRegion(), "cn-")
|
||||
}
|
||||
|
||||
func (c *AccessConfig) GetCredsFromVault() error {
|
||||
// const EnvVaultAddress = "VAULT_ADDR"
|
||||
// const EnvVaultToken = "VAULT_TOKEN"
|
||||
vaultConfig := vaultapi.DefaultConfig()
|
||||
cli, err := vaultapi.NewClient(vaultConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error getting Vault client: %s", err)
|
||||
}
|
||||
if c.VaultAWSEngine.EngineName == "" {
|
||||
c.VaultAWSEngine.EngineName = "aws"
|
||||
}
|
||||
path := fmt.Sprintf("/%s/creds/%s", c.VaultAWSEngine.EngineName,
|
||||
c.VaultAWSEngine.Name)
|
||||
secret, err := cli.Logical().Read(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error reading vault secret: %s", err)
|
||||
}
|
||||
if secret == nil {
|
||||
return fmt.Errorf("Vault Secret does not exist at the given path.")
|
||||
}
|
||||
|
||||
c.AccessKey = secret.Data["access_key"].(string)
|
||||
c.SecretKey = secret.Data["secret_key"].(string)
|
||||
token := secret.Data["security_token"]
|
||||
if token != nil {
|
||||
c.Token = token.(string)
|
||||
} else {
|
||||
c.Token = ""
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error {
|
||||
var errs []error
|
||||
|
||||
if c.SkipMetadataApiCheck {
|
||||
log.Println("(WARN) skip_metadata_api_check ignored.")
|
||||
}
|
||||
// Either both access and secret key must be set or neither of them should
|
||||
// be.
|
||||
|
||||
// Make sure it's obvious from the config how we're getting credentials:
|
||||
// Vault, Packer config, or environemnt.
|
||||
if !c.VaultAWSEngine.Empty() {
|
||||
if len(c.AccessKey) > 0 {
|
||||
errs = append(errs,
|
||||
fmt.Errorf("If you have set vault_aws_engine, you must not set"+
|
||||
" the access_key or secret_key."))
|
||||
}
|
||||
// Go ahead and grab those credentials from Vault now, so we can set
|
||||
// the keys and token now.
|
||||
err := c.GetCredsFromVault()
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
if (len(c.AccessKey) > 0) != (len(c.SecretKey) > 0) {
|
||||
errs = append(errs,
|
||||
fmt.Errorf("`access_key` and `secret_key` must both be either set or not set."))
|
||||
|
||||
@@ -151,7 +151,7 @@ func (c *AMIConfig) prepareRegions(accessConfig *AccessConfig) (errs []error) {
|
||||
func validateKmsKey(kmsKey string) (valid bool) {
|
||||
kmsKeyIdPattern := `[a-f0-9-]+$`
|
||||
aliasPattern := `alias/[a-zA-Z0-9:/_-]+$`
|
||||
kmsArnStartPattern := `^arn:aws:kms:([a-z]{2}-(gov-)?[a-z]+-\d{1})?:(\d{12}):`
|
||||
kmsArnStartPattern := `^arn:aws(-us-gov)?:kms:([a-z]{2}-(gov-)?[a-z]+-\d{1})?:(\d{12}):`
|
||||
if regexp.MustCompile(fmt.Sprintf("^%s", kmsKeyIdPattern)).MatchString(kmsKey) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -187,6 +187,7 @@ func TestAMIConfigPrepare_ValidateKmsKey(t *testing.T) {
|
||||
"alias/foo/bar",
|
||||
"arn:aws:kms:us-east-1:012345678910:key/abcd1234-a123-456a-a12b-a123b4cd56ef",
|
||||
"arn:aws:kms:us-east-1:012345678910:alias/foo/bar",
|
||||
"arn:aws-us-gov:kms:us-gov-east-1:123456789012:key/12345678-1234-abcd-0000-123456789012",
|
||||
}
|
||||
for _, validCase := range validCases {
|
||||
c.AMIKmsKeyId = validCase
|
||||
@@ -201,6 +202,7 @@ func TestAMIConfigPrepare_ValidateKmsKey(t *testing.T) {
|
||||
"ghij1234+e567_890f-a12b-a123b4cd56ef",
|
||||
"foo/bar",
|
||||
"arn:aws:kms:us-east-1:012345678910:foo/bar",
|
||||
"arn:foo:kms:us-east-1:012345678910:key/abcd1234-a123-456a-a12b-a123b4cd56ef",
|
||||
}
|
||||
for _, invalidCase := range invalidCases {
|
||||
c.AMIKmsKeyId = invalidCase
|
||||
|
||||
@@ -17,6 +17,7 @@ type StepCreateEncryptedAMICopy struct {
|
||||
EncryptBootVolume bool
|
||||
Name string
|
||||
AMIMappings []BlockDevice
|
||||
ToDelete []*string
|
||||
}
|
||||
|
||||
func (s *StepCreateEncryptedAMICopy) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
@@ -107,8 +108,7 @@ func (s *StepCreateEncryptedAMICopy) Run(ctx context.Context, state multistep.St
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Remove associated unencrypted snapshot(s)
|
||||
ui.Say("Deleting unencrypted snapshots")
|
||||
// Figure out which unencrypted snapshot(s) to delete
|
||||
snapshots := state.Get("snapshots").(map[string][]string)
|
||||
|
||||
OuterLoop:
|
||||
@@ -121,15 +121,7 @@ OuterLoop:
|
||||
continue OuterLoop
|
||||
}
|
||||
}
|
||||
|
||||
ui.Message(fmt.Sprintf("Deleting Snapshot ID: %s", *blockDevice.Ebs.SnapshotId))
|
||||
deleteSnapOpts := &ec2.DeleteSnapshotInput{
|
||||
SnapshotId: aws.String(*blockDevice.Ebs.SnapshotId),
|
||||
}
|
||||
if _, err := ec2conn.DeleteSnapshot(deleteSnapOpts); err != nil {
|
||||
ui.Error(fmt.Sprintf("Error deleting snapshot, may still be around: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
s.ToDelete = append(s.ToDelete, aws.String(*blockDevice.Ebs.SnapshotId))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,4 +163,16 @@ func (s *StepCreateEncryptedAMICopy) Cleanup(state multistep.StateBag) {
|
||||
ui.Error(fmt.Sprintf("Error deregistering AMI, may still be around: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
ui.Say("Deleting unencrypted snapshots")
|
||||
for _, snap := range s.ToDelete {
|
||||
ui.Message(fmt.Sprintf("Deleting Snapshot ID: %s", *snap))
|
||||
deleteSnapOpts := &ec2.DeleteSnapshotInput{
|
||||
SnapshotId: snap,
|
||||
}
|
||||
if _, err := ec2conn.DeleteSnapshot(deleteSnapOpts); err != nil {
|
||||
ui.Error(fmt.Sprintf("Error deleting snapshot, may still be around: %s", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,12 @@ package common
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
retry "github.com/hashicorp/packer/common"
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
@@ -20,6 +23,51 @@ type StepPreValidate struct {
|
||||
|
||||
func (s *StepPreValidate) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
if accessConfig, ok := state.GetOk("access_config"); ok {
|
||||
accessconf := accessConfig.(*AccessConfig)
|
||||
if !accessconf.VaultAWSEngine.Empty() {
|
||||
// loop over the authentication a few times to give vault-created creds
|
||||
// time to become eventually-consistent
|
||||
ui.Say("You're using Vault-generated AWS credentials. It may take a " +
|
||||
"few moments for them to become available on AWS. Waiting...")
|
||||
err := retry.Retry(0.2, 30, 11, func(_ uint) (bool, error) {
|
||||
ec2conn, err := accessconf.NewEC2Connection()
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
_, err = listEC2Regions(ec2conn)
|
||||
if err != nil {
|
||||
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "AuthFailure" {
|
||||
log.Printf("Waiting for Vault-generated AWS credentials" +
|
||||
" to pass authentication... trying again.")
|
||||
return false, nil
|
||||
}
|
||||
return true, err
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
state.Put("error", fmt.Errorf("Was unable to Authenticate to AWS using Vault-"+
|
||||
"Generated Credentials within the retry timeout."))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
if amiConfig, ok := state.GetOk("ami_config"); ok {
|
||||
amiconf := amiConfig.(*AMIConfig)
|
||||
if !amiconf.AMISkipRegionValidation {
|
||||
regionsToValidate := append(amiconf.AMIRegions, accessconf.RawRegion)
|
||||
err := accessconf.ValidateRegion(regionsToValidate...)
|
||||
if err != nil {
|
||||
state.Put("error", fmt.Errorf("error validating regions: %v", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if s.ForceDeregister {
|
||||
ui.Say("Force Deregister flag found, skipping prevalidating AMI Name")
|
||||
return multistep.ActionContinue
|
||||
|
||||
@@ -92,13 +92,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !b.config.AMISkipRegionValidation {
|
||||
regionsToValidate := append(b.config.AMIRegions, b.config.RawRegion)
|
||||
err := b.config.AccessConfig.ValidateRegion(regionsToValidate...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error validating regions: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
ec2conn := ec2.New(session, &aws.Config{
|
||||
HTTPClient: commonhelper.HttpClientWithEnvironmentProxy(),
|
||||
})
|
||||
@@ -106,6 +100,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
// Setup the state bag and initial state for the steps
|
||||
state := new(multistep.BasicStateBag)
|
||||
state.Put("config", &b.config)
|
||||
state.Put("access_config", &b.config.AccessConfig)
|
||||
state.Put("ami_config", &b.config.AMIConfig)
|
||||
state.Put("ec2", ec2conn)
|
||||
state.Put("awsSession", session)
|
||||
state.Put("hook", hook)
|
||||
|
||||
@@ -114,6 +114,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
// Setup the state bag and initial state for the steps
|
||||
state := new(multistep.BasicStateBag)
|
||||
state.Put("config", &b.config)
|
||||
state.Put("access_config", &b.config.AccessConfig)
|
||||
state.Put("ami_config", &b.config.AMIConfig)
|
||||
state.Put("ec2", ec2conn)
|
||||
state.Put("awsSession", session)
|
||||
state.Put("hook", hook)
|
||||
|
||||
@@ -103,6 +103,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
// Setup the state bag and initial state for the steps
|
||||
state := new(multistep.BasicStateBag)
|
||||
state.Put("config", &b.config)
|
||||
state.Put("access_config", &b.config.AccessConfig)
|
||||
state.Put("ec2", ec2conn)
|
||||
state.Put("hook", hook)
|
||||
state.Put("ui", ui)
|
||||
|
||||
@@ -184,6 +184,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
// Setup the state bag and initial state for the steps
|
||||
state := new(multistep.BasicStateBag)
|
||||
state.Put("config", &b.config)
|
||||
state.Put("access_config", &b.config.AccessConfig)
|
||||
state.Put("ami_config", &b.config.AMIConfig)
|
||||
state.Put("ec2", ec2conn)
|
||||
state.Put("awsSession", session)
|
||||
state.Put("hook", hook)
|
||||
|
||||
@@ -10,7 +10,11 @@ import (
|
||||
)
|
||||
|
||||
func NewDeviceFlowOAuthTokenProvider(env azure.Environment, say func(string), tenantID string) oAuthTokenProvider {
|
||||
return &deviceflowOauthTokenProvider{}
|
||||
return &deviceflowOauthTokenProvider{
|
||||
env: env,
|
||||
say: say,
|
||||
tenantID: tenantID,
|
||||
}
|
||||
}
|
||||
|
||||
type deviceflowOauthTokenProvider struct {
|
||||
|
||||
@@ -59,8 +59,13 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
b.ctxCancel = cancel
|
||||
defer cancel()
|
||||
|
||||
if err := newConfigRetriever().FillParameters(b.config); err != nil {
|
||||
return nil, err
|
||||
// User's intent to use MSI is indicated with empty subscription id, tenant, client id, client cert, client secret and jwt.
|
||||
// FillParameters function will set subscription and tenant id here. Therefore getServicePrincipalTokens won't select right auth type.
|
||||
// If we run this after getServicePrincipalTokens call then getServicePrincipalTokens won't have tenant id.
|
||||
if !b.config.useMSI() {
|
||||
if err := newConfigRetriever().FillParameters(b.config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
log.Print(":: Configuration")
|
||||
@@ -74,6 +79,13 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We need subscription id and tenant id for arm operations. Users hasn't specified one so we try to detect them here.
|
||||
if b.config.useMSI() {
|
||||
if err := newConfigRetriever().FillParameters(b.config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
ui.Message("Creating Azure Resource Manager (ARM) client ...")
|
||||
azureClient, err := NewAzureClient(
|
||||
b.config.SubscriptionID,
|
||||
|
||||
@@ -155,8 +155,7 @@ func (c ClientConfig) useDeviceLogin() bool {
|
||||
c.ClientID == "" &&
|
||||
c.ClientSecret == "" &&
|
||||
c.ClientJWT == "" &&
|
||||
c.ClientCertPath == "" &&
|
||||
c.TenantID == ""
|
||||
c.ClientCertPath == ""
|
||||
}
|
||||
|
||||
func (c ClientConfig) useMSI() bool {
|
||||
|
||||
@@ -268,11 +268,18 @@ func getCloud() *azure.Environment {
|
||||
// tests for assertRequiredParametersSet
|
||||
|
||||
func Test_ClientConfig_CanUseDeviceCode(t *testing.T) {
|
||||
cfg := emptyClientConfig()
|
||||
cfg.SubscriptionID = "12345"
|
||||
// TenantID is optional
|
||||
|
||||
assertValid(t, cfg)
|
||||
// TenantID is optional, but Builder will look up tenant ID before requesting
|
||||
t.Run("without TenantID", func(t *testing.T) {
|
||||
cfg := emptyClientConfig()
|
||||
cfg.SubscriptionID = "12345"
|
||||
assertValid(t, cfg)
|
||||
})
|
||||
t.Run("with TenantID", func(t *testing.T) {
|
||||
cfg := emptyClientConfig()
|
||||
cfg.SubscriptionID = "12345"
|
||||
cfg.TenantID = "12345"
|
||||
assertValid(t, cfg)
|
||||
})
|
||||
}
|
||||
|
||||
func assertValid(t *testing.T, cfg ClientConfig) {
|
||||
|
||||
@@ -8,6 +8,10 @@ package arm
|
||||
// 1. TenantID
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest/azure"
|
||||
"github.com/hashicorp/packer/builder/azure/common"
|
||||
)
|
||||
@@ -24,6 +28,14 @@ func newConfigRetriever() configRetriever {
|
||||
}
|
||||
|
||||
func (cr configRetriever) FillParameters(c *Config) error {
|
||||
if c.SubscriptionID == "" {
|
||||
subscriptionID, err := cr.getSubscriptionFromIMDS()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.SubscriptionID = subscriptionID
|
||||
}
|
||||
|
||||
if c.TenantID == "" {
|
||||
tenantID, err := cr.findTenantID(*c.cloudEnvironment, c.SubscriptionID)
|
||||
if err != nil {
|
||||
@@ -34,3 +46,30 @@ func (cr configRetriever) FillParameters(c *Config) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRetriever) getSubscriptionFromIMDS() (string, error) {
|
||||
client := &http.Client{}
|
||||
|
||||
req, _ := http.NewRequest("GET", "http://169.254.169.254/metadata/instance/compute", nil)
|
||||
req.Header.Add("Metadata", "True")
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Add("format", "json")
|
||||
q.Add("api-version", "2017-08-01")
|
||||
|
||||
req.URL.RawQuery = q.Encode()
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
resp_body, _ := ioutil.ReadAll(resp.Body)
|
||||
result := map[string]string{}
|
||||
err = json.Unmarshal(resp_body, &result)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return result["subscriptionId"], nil
|
||||
}
|
||||
|
||||
@@ -12,16 +12,16 @@ import (
|
||||
|
||||
type Artifact struct {
|
||||
// The name of the snapshot
|
||||
snapshotName string
|
||||
SnapshotName string
|
||||
|
||||
// The ID of the image
|
||||
snapshotId int
|
||||
SnapshotId int
|
||||
|
||||
// The name of the region
|
||||
regionNames []string
|
||||
RegionNames []string
|
||||
|
||||
// The client for making API calls
|
||||
client *godo.Client
|
||||
Client *godo.Client
|
||||
}
|
||||
|
||||
func (*Artifact) BuilderId() string {
|
||||
@@ -34,11 +34,11 @@ func (*Artifact) Files() []string {
|
||||
}
|
||||
|
||||
func (a *Artifact) Id() string {
|
||||
return fmt.Sprintf("%s:%s", strings.Join(a.regionNames[:], ","), strconv.FormatUint(uint64(a.snapshotId), 10))
|
||||
return fmt.Sprintf("%s:%s", strings.Join(a.RegionNames[:], ","), strconv.FormatUint(uint64(a.SnapshotId), 10))
|
||||
}
|
||||
|
||||
func (a *Artifact) String() string {
|
||||
return fmt.Sprintf("A snapshot was created: '%v' (ID: %v) in regions '%v'", a.snapshotName, a.snapshotId, strings.Join(a.regionNames[:], ","))
|
||||
return fmt.Sprintf("A snapshot was created: '%v' (ID: %v) in regions '%v'", a.SnapshotName, a.SnapshotId, strings.Join(a.RegionNames[:], ","))
|
||||
}
|
||||
|
||||
func (a *Artifact) State(name string) interface{} {
|
||||
@@ -46,7 +46,7 @@ func (a *Artifact) State(name string) interface{} {
|
||||
}
|
||||
|
||||
func (a *Artifact) Destroy() error {
|
||||
log.Printf("Destroying image: %d (%s)", a.snapshotId, a.snapshotName)
|
||||
_, err := a.client.Images.Delete(context.TODO(), a.snapshotId)
|
||||
log.Printf("Destroying image: %d (%s)", a.SnapshotId, a.SnapshotName)
|
||||
_, err := a.Client.Images.Delete(context.TODO(), a.SnapshotId)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -113,10 +113,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
}
|
||||
|
||||
artifact := &Artifact{
|
||||
snapshotName: state.Get("snapshot_name").(string),
|
||||
snapshotId: state.Get("snapshot_image_id").(int),
|
||||
regionNames: state.Get("regions").([]string),
|
||||
client: client,
|
||||
SnapshotName: state.Get("snapshot_name").(string),
|
||||
SnapshotId: state.Get("snapshot_image_id").(int),
|
||||
RegionNames: state.Get("regions").([]string),
|
||||
Client: client,
|
||||
}
|
||||
|
||||
return artifact, nil
|
||||
|
||||
@@ -91,7 +91,7 @@ func (s *stepSnapshot) Run(_ context.Context, state multistep.StateBag) multiste
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
ui.Say(fmt.Sprintf("transferring Snapshot ID: %d", imageTransfer.ID))
|
||||
if err := waitForImageState(godo.ActionCompleted, imageTransfer.ID, action.ID,
|
||||
if err := WaitForImageState(godo.ActionCompleted, imageTransfer.ID, action.ID,
|
||||
client, 20*time.Minute); err != nil {
|
||||
// If we get an error the first time, actually report it
|
||||
err := fmt.Errorf("Error waiting for snapshot transfer: %s", err)
|
||||
|
||||
@@ -158,9 +158,9 @@ func waitForActionState(
|
||||
}
|
||||
}
|
||||
|
||||
// waitForImageState simply blocks until the image action is in
|
||||
// WaitForImageState simply blocks until the image action is in
|
||||
// a state we expect, while eventually timing out.
|
||||
func waitForImageState(
|
||||
func WaitForImageState(
|
||||
desiredState string, imageId, actionId int,
|
||||
client *godo.Client, timeout time.Duration) error {
|
||||
done := make(chan struct{})
|
||||
|
||||
@@ -34,7 +34,7 @@ type driverGCE struct {
|
||||
|
||||
var DriverScopes = []string{"https://www.googleapis.com/auth/compute", "https://www.googleapis.com/auth/devstorage.full_control"}
|
||||
|
||||
func NewDriverGCE(ui packer.Ui, p string, a *AccountFile) (Driver, error) {
|
||||
func NewClientGCE(a *AccountFile) (*http.Client, error) {
|
||||
var err error
|
||||
|
||||
var client *http.Client
|
||||
@@ -78,6 +78,15 @@ func NewDriverGCE(ui packer.Ui, p string, a *AccountFile) (Driver, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func NewDriverGCE(ui packer.Ui, p string, a *AccountFile) (Driver, error) {
|
||||
client, err := NewClientGCE(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Printf("[INFO] Instantiating GCE client...")
|
||||
service, err := compute.New(client)
|
||||
if err != nil {
|
||||
|
||||
@@ -15,7 +15,7 @@ const StartupScriptStatusNotDone string = "notdone"
|
||||
var StartupScriptLinux string = fmt.Sprintf(`#!/usr/bin/env bash
|
||||
echo "Packer startup script starting."
|
||||
RETVAL=0
|
||||
BASEMETADATAURL=http://metadata/computeMetadata/v1/instance/
|
||||
BASEMETADATAURL=http://metadata.google.internal/computeMetadata/v1/instance/
|
||||
|
||||
GetMetadata () {
|
||||
echo "$(curl -f -H "Metadata-Flavor: Google" ${BASEMETADATAURL}/${1} 2> /dev/null)"
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package hyperone
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hyperonecom/h1-client-go"
|
||||
)
|
||||
|
||||
type Artifact struct {
|
||||
imageName string
|
||||
imageID string
|
||||
client *openapi.APIClient
|
||||
}
|
||||
|
||||
func (a *Artifact) BuilderId() string {
|
||||
return BuilderID
|
||||
}
|
||||
|
||||
func (a *Artifact) Files() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Artifact) Id() string {
|
||||
return a.imageID
|
||||
}
|
||||
|
||||
func (a *Artifact) String() string {
|
||||
return fmt.Sprintf("Image '%s' created, ID: %s", a.imageName, a.imageID)
|
||||
}
|
||||
|
||||
func (a *Artifact) State(name string) interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Artifact) Destroy() error {
|
||||
if a.imageID == "" {
|
||||
// No image to destroy
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := a.client.ImageApi.ImageDelete(context.TODO(), a.imageID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package hyperone
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/hashicorp/packer/common"
|
||||
"github.com/hashicorp/packer/helper/communicator"
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/hashicorp/packer/template/interpolate"
|
||||
"github.com/hyperonecom/h1-client-go"
|
||||
)
|
||||
|
||||
const BuilderID = "hyperone.builder"
|
||||
|
||||
type Builder struct {
|
||||
config Config
|
||||
runner multistep.Runner
|
||||
client *openapi.APIClient
|
||||
}
|
||||
|
||||
func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
||||
config, warnings, errs := NewConfig(raws...)
|
||||
if errs != nil {
|
||||
return warnings, errs
|
||||
}
|
||||
|
||||
b.config = *config
|
||||
|
||||
cfg := openapi.NewConfiguration()
|
||||
cfg.AddDefaultHeader("x-auth-token", b.config.Token)
|
||||
if b.config.Project != "" {
|
||||
cfg.AddDefaultHeader("x-project", b.config.Project)
|
||||
}
|
||||
|
||||
if b.config.APIURL != "" {
|
||||
cfg.BasePath = b.config.APIURL
|
||||
}
|
||||
|
||||
prefer := fmt.Sprintf("respond-async,wait=%d", int(b.config.StateTimeout.Seconds()))
|
||||
cfg.AddDefaultHeader("Prefer", prefer)
|
||||
|
||||
b.client = openapi.NewAPIClient(cfg)
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type wrappedCommandTemplate struct {
|
||||
Command string
|
||||
}
|
||||
|
||||
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
|
||||
wrappedCommand := func(command string) (string, error) {
|
||||
ctx := b.config.ctx
|
||||
ctx.Data = &wrappedCommandTemplate{Command: command}
|
||||
return interpolate.Render(b.config.ChrootCommandWrapper, &ctx)
|
||||
}
|
||||
|
||||
state := &multistep.BasicStateBag{}
|
||||
state.Put("config", &b.config)
|
||||
state.Put("client", b.client)
|
||||
state.Put("hook", hook)
|
||||
state.Put("ui", ui)
|
||||
state.Put("wrappedCommand", CommandWrapper(wrappedCommand))
|
||||
|
||||
steps := []multistep.Step{
|
||||
&stepCreateSSHKey{},
|
||||
&stepCreateVM{},
|
||||
&communicator.StepConnect{
|
||||
Config: &b.config.Comm,
|
||||
Host: getPublicIP,
|
||||
SSHConfig: b.config.Comm.SSHConfigFunc(),
|
||||
},
|
||||
}
|
||||
|
||||
if b.config.ChrootDisk {
|
||||
steps = append(steps,
|
||||
&stepPrepareDevice{},
|
||||
&stepPreMountCommands{},
|
||||
&stepMountChroot{},
|
||||
&stepPostMountCommands{},
|
||||
&stepMountExtra{},
|
||||
&stepCopyFiles{},
|
||||
&stepChrootProvision{},
|
||||
&stepStopVM{},
|
||||
&stepDetachDisk{},
|
||||
&stepCreateVMFromDisk{},
|
||||
&stepCreateImage{},
|
||||
)
|
||||
} else {
|
||||
steps = append(steps,
|
||||
&common.StepProvision{},
|
||||
&stepStopVM{},
|
||||
&stepCreateImage{},
|
||||
)
|
||||
}
|
||||
|
||||
b.runner = common.NewRunner(steps, b.config.PackerConfig, ui)
|
||||
b.runner.Run(state)
|
||||
|
||||
if rawErr, ok := state.GetOk("error"); ok {
|
||||
return nil, rawErr.(error)
|
||||
}
|
||||
|
||||
artifact := &Artifact{
|
||||
imageID: state.Get("image_id").(string),
|
||||
imageName: state.Get("image_name").(string),
|
||||
client: b.client,
|
||||
}
|
||||
|
||||
return artifact, nil
|
||||
}
|
||||
|
||||
func (b *Builder) Cancel() {
|
||||
if b.runner != nil {
|
||||
log.Println("Cancelling the runner...")
|
||||
b.runner.Cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package hyperone
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
builderT "github.com/hashicorp/packer/helper/builder/testing"
|
||||
)
|
||||
|
||||
func TestBuilderAcc_basic(t *testing.T) {
|
||||
builderT.Test(t, builderT.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Builder: &Builder{},
|
||||
Template: testBuilderAccBasic,
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuilderAcc_chroot(t *testing.T) {
|
||||
builderT.Test(t, builderT.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Builder: &Builder{},
|
||||
Template: testBuilderAccChroot,
|
||||
})
|
||||
}
|
||||
|
||||
func testAccPreCheck(t *testing.T) {
|
||||
if v := os.Getenv("HYPERONE_TOKEN"); v == "" {
|
||||
t.Fatal("HYPERONE_TOKEN must be set for acceptance tests")
|
||||
}
|
||||
}
|
||||
|
||||
const testBuilderAccBasic = `
|
||||
{
|
||||
"builders": [{
|
||||
"type": "test",
|
||||
"vm_type": "a1.nano",
|
||||
"source_image": "ubuntu",
|
||||
"disk_size": 10
|
||||
}]
|
||||
}
|
||||
`
|
||||
|
||||
const testBuilderAccChroot = `
|
||||
{
|
||||
"builders": [{
|
||||
"type": "test",
|
||||
"source_image": "ubuntu",
|
||||
"disk_size": 10,
|
||||
"vm_type": "a1.nano",
|
||||
"chroot_disk": true,
|
||||
"chroot_command_wrapper": "sudo {{.Command}}",
|
||||
"pre_mount_commands": [
|
||||
"parted {{.Device}} mklabel msdos mkpart primary 1M 100% set 1 boot on print",
|
||||
"mkfs.ext4 {{.Device}}1"
|
||||
],
|
||||
"post_mount_commands": [
|
||||
"apt-get update",
|
||||
"apt-get install debootstrap",
|
||||
"debootstrap --arch amd64 bionic {{.MountPath}}"
|
||||
]
|
||||
}]
|
||||
}
|
||||
`
|
||||
@@ -0,0 +1,54 @@
|
||||
package hyperone
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
||||
type CommandWrapper func(string) (string, error)
|
||||
|
||||
// ChrootCommunicator works as a wrapper on SSHCommunicator, modyfing paths in
|
||||
// flight to be run in a chroot.
|
||||
type ChrootCommunicator struct {
|
||||
Chroot string
|
||||
CmdWrapper CommandWrapper
|
||||
Wrapped packer.Communicator
|
||||
}
|
||||
|
||||
func (c *ChrootCommunicator) Start(cmd *packer.RemoteCmd) error {
|
||||
command := strconv.Quote(cmd.Command)
|
||||
chrootCommand, err := c.CmdWrapper(
|
||||
fmt.Sprintf("sudo chroot %s /bin/sh -c %s", c.Chroot, command))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.Command = chrootCommand
|
||||
|
||||
return c.Wrapped.Start(cmd)
|
||||
}
|
||||
|
||||
func (c *ChrootCommunicator) Upload(dst string, r io.Reader, fi *os.FileInfo) error {
|
||||
dst = filepath.Join(c.Chroot, dst)
|
||||
return c.Wrapped.Upload(dst, r, fi)
|
||||
}
|
||||
|
||||
func (c *ChrootCommunicator) UploadDir(dst string, src string, exclude []string) error {
|
||||
dst = filepath.Join(c.Chroot, dst)
|
||||
return c.Wrapped.UploadDir(dst, src, exclude)
|
||||
}
|
||||
|
||||
func (c *ChrootCommunicator) Download(src string, w io.Writer) error {
|
||||
src = filepath.Join(c.Chroot, src)
|
||||
return c.Wrapped.Download(src, w)
|
||||
}
|
||||
|
||||
func (c *ChrootCommunicator) DownloadDir(src string, dst string, exclude []string) error {
|
||||
src = filepath.Join(c.Chroot, src)
|
||||
return c.Wrapped.DownloadDir(src, dst, exclude)
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
package hyperone
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/packer/common"
|
||||
"github.com/hashicorp/packer/common/json"
|
||||
"github.com/hashicorp/packer/common/uuid"
|
||||
"github.com/hashicorp/packer/helper/communicator"
|
||||
"github.com/hashicorp/packer/helper/config"
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/hashicorp/packer/template/interpolate"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
const (
|
||||
configPath = "~/.h1-cli/conf.json"
|
||||
tokenEnv = "HYPERONE_TOKEN"
|
||||
|
||||
defaultDiskType = "ssd"
|
||||
defaultImageService = "564639bc052c084e2f2e3266"
|
||||
defaultStateTimeout = 5 * time.Minute
|
||||
defaultUserName = "guru"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
Comm communicator.Config `mapstructure:",squash"`
|
||||
|
||||
APIURL string `mapstructure:"api_url"`
|
||||
Token string `mapstructure:"token"`
|
||||
Project string `mapstructure:"project"`
|
||||
TokenLogin string `mapstructure:"token_login"`
|
||||
|
||||
StateTimeout time.Duration `mapstructure:"state_timeout"`
|
||||
|
||||
SourceImage string `mapstructure:"source_image"`
|
||||
ImageName string `mapstructure:"image_name"`
|
||||
ImageDescription string `mapstructure:"image_description"`
|
||||
ImageTags map[string]interface{} `mapstructure:"image_tags"`
|
||||
ImageService string `mapstructure:"image_service"`
|
||||
|
||||
VmType string `mapstructure:"vm_type"`
|
||||
VmName string `mapstructure:"vm_name"`
|
||||
VmTags map[string]interface{} `mapstructure:"vm_tags"`
|
||||
|
||||
DiskName string `mapstructure:"disk_name"`
|
||||
DiskType string `mapstructure:"disk_type"`
|
||||
DiskSize float32 `mapstructure:"disk_size"`
|
||||
|
||||
Network string `mapstructure:"network"`
|
||||
PrivateIP string `mapstructure:"private_ip"`
|
||||
PublicIP string `mapstructure:"public_ip"`
|
||||
PublicNetAdpService string `mapstructure:"public_netadp_service"`
|
||||
|
||||
ChrootDisk bool `mapstructure:"chroot_disk"`
|
||||
ChrootDiskSize float32 `mapstructure:"chroot_disk_size"`
|
||||
ChrootDiskType string `mapstructure:"chroot_disk_type"`
|
||||
ChrootMountPath string `mapstructure:"chroot_mount_path"`
|
||||
ChrootMounts [][]string `mapstructure:"chroot_mounts"`
|
||||
ChrootCopyFiles []string `mapstructure:"chroot_copy_files"`
|
||||
ChrootCommandWrapper string `mapstructure:"chroot_command_wrapper"`
|
||||
|
||||
MountOptions []string `mapstructure:"mount_options"`
|
||||
MountPartition string `mapstructure:"mount_partition"`
|
||||
PreMountCommands []string `mapstructure:"pre_mount_commands"`
|
||||
PostMountCommands []string `mapstructure:"post_mount_commands"`
|
||||
|
||||
SSHKeys []string `mapstructure:"ssh_keys"`
|
||||
UserData string `mapstructure:"user_data"`
|
||||
|
||||
ctx interpolate.Context
|
||||
}
|
||||
|
||||
func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
||||
c := &Config{}
|
||||
|
||||
var md mapstructure.Metadata
|
||||
err := config.Decode(c, &config.DecodeOpts{
|
||||
Metadata: &md,
|
||||
Interpolate: true,
|
||||
InterpolateContext: &c.ctx,
|
||||
InterpolateFilter: &interpolate.RenderFilter{
|
||||
Exclude: []string{
|
||||
"run_command",
|
||||
"chroot_command_wrapper",
|
||||
"post_mount_commands",
|
||||
"pre_mount_commands",
|
||||
"mount_path",
|
||||
},
|
||||
},
|
||||
}, raws...)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
cliConfig, err := loadCLIConfig()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Defaults
|
||||
if c.Comm.SSHUsername == "" {
|
||||
c.Comm.SSHUsername = defaultUserName
|
||||
}
|
||||
|
||||
if c.Comm.SSHTimeout == 0 {
|
||||
c.Comm.SSHTimeout = 10 * time.Minute
|
||||
}
|
||||
|
||||
if c.APIURL == "" {
|
||||
c.APIURL = os.Getenv("HYPERONE_API_URL")
|
||||
}
|
||||
|
||||
if c.Token == "" {
|
||||
c.Token = os.Getenv(tokenEnv)
|
||||
|
||||
if c.Token == "" {
|
||||
c.Token = cliConfig.Profile.APIKey
|
||||
}
|
||||
|
||||
// Fetching token by SSH is available only for the default API endpoint
|
||||
if c.TokenLogin != "" && c.APIURL == "" {
|
||||
c.Token, err = fetchTokenBySSH(c.TokenLogin)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.Project == "" {
|
||||
c.Project = cliConfig.Profile.Project.ID
|
||||
}
|
||||
|
||||
if c.StateTimeout == 0 {
|
||||
c.StateTimeout = defaultStateTimeout
|
||||
}
|
||||
|
||||
if c.ImageName == "" {
|
||||
name, err := interpolate.Render("packer-{{timestamp}}", nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
c.ImageName = name
|
||||
}
|
||||
|
||||
if c.ImageService == "" {
|
||||
c.ImageService = defaultImageService
|
||||
}
|
||||
|
||||
if c.VmName == "" {
|
||||
c.VmName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
|
||||
}
|
||||
|
||||
if c.DiskType == "" {
|
||||
c.DiskType = defaultDiskType
|
||||
}
|
||||
|
||||
if c.PublicNetAdpService == "" {
|
||||
c.PublicNetAdpService = "public"
|
||||
}
|
||||
|
||||
if c.ChrootCommandWrapper == "" {
|
||||
c.ChrootCommandWrapper = "{{.Command}}"
|
||||
}
|
||||
|
||||
if c.ChrootDiskSize == 0 {
|
||||
c.ChrootDiskSize = c.DiskSize
|
||||
}
|
||||
|
||||
if c.ChrootDiskType == "" {
|
||||
c.ChrootDiskType = c.DiskType
|
||||
}
|
||||
|
||||
if c.ChrootMountPath == "" {
|
||||
path, err := interpolate.Render("/mnt/packer-hyperone-volumes/{{timestamp}}", nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
c.ChrootMountPath = path
|
||||
}
|
||||
|
||||
if c.ChrootMounts == nil {
|
||||
c.ChrootMounts = make([][]string, 0)
|
||||
}
|
||||
|
||||
if len(c.ChrootMounts) == 0 {
|
||||
c.ChrootMounts = [][]string{
|
||||
{"proc", "proc", "/proc"},
|
||||
{"sysfs", "sysfs", "/sys"},
|
||||
{"bind", "/dev", "/dev"},
|
||||
{"devpts", "devpts", "/dev/pts"},
|
||||
{"binfmt_misc", "binfmt_misc", "/proc/sys/fs/binfmt_misc"},
|
||||
}
|
||||
}
|
||||
|
||||
if c.ChrootCopyFiles == nil {
|
||||
c.ChrootCopyFiles = []string{"/etc/resolv.conf"}
|
||||
}
|
||||
|
||||
if c.MountPartition == "" {
|
||||
c.MountPartition = "1"
|
||||
}
|
||||
|
||||
// Validation
|
||||
var errs *packer.MultiError
|
||||
if es := c.Comm.Prepare(&c.ctx); len(es) > 0 {
|
||||
errs = packer.MultiErrorAppend(errs, es...)
|
||||
}
|
||||
|
||||
if c.Token == "" {
|
||||
errs = packer.MultiErrorAppend(errs, errors.New("token is required"))
|
||||
}
|
||||
|
||||
if c.VmType == "" {
|
||||
errs = packer.MultiErrorAppend(errs, errors.New("vm type is required"))
|
||||
}
|
||||
|
||||
if c.DiskSize == 0 {
|
||||
errs = packer.MultiErrorAppend(errs, errors.New("disk size is required"))
|
||||
}
|
||||
|
||||
if c.SourceImage == "" {
|
||||
errs = packer.MultiErrorAppend(errs, errors.New("source image is required"))
|
||||
}
|
||||
|
||||
if c.ChrootDisk {
|
||||
if len(c.PreMountCommands) == 0 {
|
||||
errs = packer.MultiErrorAppend(errs, errors.New("pre-mount commands are required for chroot disk"))
|
||||
}
|
||||
}
|
||||
|
||||
for _, mounts := range c.ChrootMounts {
|
||||
if len(mounts) != 3 {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("each chroot_mounts entry should have three elements"))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if errs != nil && len(errs.Errors) > 0 {
|
||||
return nil, nil, errs
|
||||
}
|
||||
|
||||
packer.LogSecretFilter.Set(c.Token)
|
||||
|
||||
return c, nil, nil
|
||||
}
|
||||
|
||||
type cliConfig struct {
|
||||
Profile struct {
|
||||
APIKey string `json:"apiKey"`
|
||||
Project struct {
|
||||
ID string `json:"_id"`
|
||||
} `json:"project"`
|
||||
} `json:"profile"`
|
||||
}
|
||||
|
||||
func loadCLIConfig() (cliConfig, error) {
|
||||
path, err := homedir.Expand(configPath)
|
||||
if err != nil {
|
||||
return cliConfig{}, err
|
||||
}
|
||||
|
||||
_, err = os.Stat(path)
|
||||
if err != nil {
|
||||
// Config not found
|
||||
return cliConfig{}, nil
|
||||
}
|
||||
|
||||
content, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return cliConfig{}, err
|
||||
}
|
||||
|
||||
var c cliConfig
|
||||
err = json.Unmarshal(content, &c)
|
||||
if err != nil {
|
||||
return cliConfig{}, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func getPublicIP(state multistep.StateBag) (string, error) {
|
||||
return state.Get("public_ip").(string), nil
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package hyperone
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/packer/common"
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
||||
type stepChrootProvision struct{}
|
||||
|
||||
func (s *stepChrootProvision) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
|
||||
sshCommunicator := state.Get("communicator").(packer.Communicator)
|
||||
|
||||
comm := &ChrootCommunicator{
|
||||
Chroot: config.ChrootMountPath,
|
||||
CmdWrapper: wrappedCommand,
|
||||
Wrapped: sshCommunicator,
|
||||
}
|
||||
|
||||
stepProvision := common.StepProvision{Comm: comm}
|
||||
return stepProvision.Run(ctx, state)
|
||||
}
|
||||
|
||||
func (s *stepChrootProvision) Cleanup(multistep.StateBag) {}
|
||||
@@ -0,0 +1,40 @@
|
||||
package hyperone
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
||||
type stepCopyFiles struct{}
|
||||
|
||||
func (s *stepCopyFiles) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
if len(config.ChrootCopyFiles) == 0 {
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
ui.Say("Copying files from host to chroot...")
|
||||
for _, path := range config.ChrootCopyFiles {
|
||||
chrootPath := filepath.Join(config.ChrootMountPath, path)
|
||||
log.Printf("Copying '%s' to '%s'", path, chrootPath)
|
||||
|
||||
command := fmt.Sprintf("cp --remove-destination %s %s", path, chrootPath)
|
||||
err := runCommands([]string{command}, config.ctx, state)
|
||||
if err != nil {
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepCopyFiles) Cleanup(state multistep.StateBag) {}
|
||||
@@ -0,0 +1,65 @@
|
||||
package hyperone
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/hyperonecom/h1-client-go"
|
||||
)
|
||||
|
||||
type stepCreateImage struct {
|
||||
imageID string
|
||||
}
|
||||
|
||||
func (s *stepCreateImage) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
client := state.Get("client").(*openapi.APIClient)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
config := state.Get("config").(*Config)
|
||||
vmID := state.Get("vm_id").(string)
|
||||
|
||||
ui.Say("Creating image...")
|
||||
|
||||
image, _, err := client.ImageApi.ImageCreate(ctx, openapi.ImageCreate{
|
||||
Name: config.ImageName,
|
||||
Vm: vmID,
|
||||
Service: config.ImageService,
|
||||
Description: config.ImageDescription,
|
||||
Tag: config.ImageTags,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("error creating image: %s", formatOpenAPIError(err))
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
s.imageID = image.Id
|
||||
|
||||
state.Put("image_id", image.Id)
|
||||
state.Put("image_name", image.Name)
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepCreateImage) Cleanup(state multistep.StateBag) {
|
||||
if s.imageID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
_, cancelled := state.GetOk(multistep.StateCancelled)
|
||||
_, halted := state.GetOk(multistep.StateHalted)
|
||||
if !cancelled && !halted {
|
||||
return
|
||||
}
|
||||
|
||||
client := state.Get("client").(*openapi.APIClient)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
_, err := client.ImageApi.ImageDelete(context.TODO(), s.imageID)
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf("error deleting image '%s' - consider deleting it manually: %s",
|
||||
s.imageID, formatOpenAPIError(err)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package hyperone
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type stepCreateSSHKey struct {
|
||||
Debug bool
|
||||
DebugKeyPath string
|
||||
}
|
||||
|
||||
func (s *stepCreateSSHKey) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
c := state.Get("config").(*Config)
|
||||
ui.Say("Creating a temporary ssh key for the VM...")
|
||||
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
state.Put("error", fmt.Errorf("error generating ssh key: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
privDER := x509.MarshalPKCS1PrivateKey(priv)
|
||||
privBLK := pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Headers: nil,
|
||||
Bytes: privDER,
|
||||
}
|
||||
|
||||
c.Comm.SSHPrivateKey = pem.EncodeToMemory(&privBLK)
|
||||
|
||||
pub, err := ssh.NewPublicKey(&priv.PublicKey)
|
||||
if err != nil {
|
||||
state.Put("error", fmt.Errorf("error getting public key: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
pubSSHFormat := string(ssh.MarshalAuthorizedKey(pub))
|
||||
|
||||
// Remember public SSH key for future connections
|
||||
state.Put("ssh_public_key", pubSSHFormat)
|
||||
|
||||
// If we're in debug mode, output the private key to the working directory.
|
||||
if s.Debug {
|
||||
ui.Message(fmt.Sprintf("Saving key for debug purposes: %s", s.DebugKeyPath))
|
||||
f, err := os.Create(s.DebugKeyPath)
|
||||
if err != nil {
|
||||
state.Put("error", fmt.Errorf("error saving debug key: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Write the key out
|
||||
if _, err := f.Write(pem.EncodeToMemory(&privBLK)); err != nil {
|
||||
state.Put("error", fmt.Errorf("error saving debug key: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Chmod it so that it is SSH ready
|
||||
if runtime.GOOS != "windows" {
|
||||
if err := f.Chmod(0600); err != nil {
|
||||
state.Put("error", fmt.Errorf("error setting permissions of debug key: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepCreateSSHKey) Cleanup(state multistep.StateBag) {}
|
||||
@@ -0,0 +1,199 @@
|
||||
package hyperone
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/hyperonecom/h1-client-go"
|
||||
)
|
||||
|
||||
type stepCreateVM struct {
|
||||
vmID string
|
||||
}
|
||||
|
||||
const (
|
||||
chrootDiskName = "packer-chroot-disk"
|
||||
)
|
||||
|
||||
func (s *stepCreateVM) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
client := state.Get("client").(*openapi.APIClient)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
config := state.Get("config").(*Config)
|
||||
sshKey := state.Get("ssh_public_key").(string)
|
||||
|
||||
ui.Say("Creating VM...")
|
||||
|
||||
netAdapter := pickNetAdapter(config)
|
||||
|
||||
var sshKeys = []string{sshKey}
|
||||
sshKeys = append(sshKeys, config.SSHKeys...)
|
||||
|
||||
disks := []openapi.VmCreateDisk{
|
||||
{
|
||||
Service: config.DiskType,
|
||||
Size: config.DiskSize,
|
||||
},
|
||||
}
|
||||
|
||||
if config.ChrootDisk {
|
||||
disks = append(disks, openapi.VmCreateDisk{
|
||||
Service: config.ChrootDiskType,
|
||||
Size: config.ChrootDiskSize,
|
||||
Name: chrootDiskName,
|
||||
})
|
||||
}
|
||||
|
||||
options := openapi.VmCreate{
|
||||
Name: config.VmName,
|
||||
Image: config.SourceImage,
|
||||
Service: config.VmType,
|
||||
SshKeys: sshKeys,
|
||||
Disk: disks,
|
||||
Netadp: []openapi.VmCreateNetadp{netAdapter},
|
||||
UserMetadata: config.UserData,
|
||||
Tag: config.VmTags,
|
||||
}
|
||||
|
||||
vm, _, err := client.VmApi.VmCreate(ctx, options)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("error creating VM: %s", formatOpenAPIError(err))
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
s.vmID = vm.Id
|
||||
state.Put("vm_id", vm.Id)
|
||||
|
||||
hdds, _, err := client.VmApi.VmListHdd(ctx, vm.Id)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("error listing hdd: %s", formatOpenAPIError(err))
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
for _, hdd := range hdds {
|
||||
if hdd.Disk.Name == chrootDiskName {
|
||||
state.Put("chroot_disk_id", hdd.Disk.Id)
|
||||
controllerNumber := strings.ToLower(strings.Trim(hdd.ControllerNumber, "{}"))
|
||||
state.Put("chroot_controller_number", controllerNumber)
|
||||
state.Put("chroot_controller_location", int(hdd.ControllerLocation))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
netadp, _, err := client.VmApi.VmListNetadp(ctx, vm.Id)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("error listing netadp: %s", formatOpenAPIError(err))
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
if len(netadp) < 1 {
|
||||
err := fmt.Errorf("no network adapters found")
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
publicIP, err := associatePublicIP(ctx, config, client, netadp[0])
|
||||
if err != nil {
|
||||
err := fmt.Errorf("error associating IP: %s", formatOpenAPIError(err))
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
state.Put("public_ip", publicIP)
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func pickNetAdapter(config *Config) openapi.VmCreateNetadp {
|
||||
if config.Network == "" {
|
||||
if config.PublicIP != "" {
|
||||
return openapi.VmCreateNetadp{
|
||||
Service: config.PublicNetAdpService,
|
||||
Ip: []string{config.PublicIP},
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var privateIPs []string
|
||||
|
||||
if config.PrivateIP == "" {
|
||||
privateIPs = nil
|
||||
} else {
|
||||
privateIPs = []string{config.PrivateIP}
|
||||
}
|
||||
|
||||
return openapi.VmCreateNetadp{
|
||||
Service: "private",
|
||||
Network: config.Network,
|
||||
Ip: privateIPs,
|
||||
}
|
||||
}
|
||||
|
||||
return openapi.VmCreateNetadp{
|
||||
Service: config.PublicNetAdpService,
|
||||
}
|
||||
}
|
||||
|
||||
func associatePublicIP(ctx context.Context, config *Config, client *openapi.APIClient, netadp openapi.Netadp) (string, error) {
|
||||
if config.Network == "" || config.PublicIP == "" {
|
||||
// Public IP belongs to attached net adapter
|
||||
return netadp.Ip[0].Address, nil
|
||||
}
|
||||
|
||||
var privateIP string
|
||||
if config.PrivateIP == "" {
|
||||
privateIP = netadp.Ip[0].Id
|
||||
} else {
|
||||
privateIP = config.PrivateIP
|
||||
}
|
||||
|
||||
ip, _, err := client.IpApi.IpActionAssociate(ctx, config.PublicIP, openapi.IpActionAssociate{Ip: privateIP})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return ip.Address, nil
|
||||
}
|
||||
|
||||
func (s *stepCreateVM) Cleanup(state multistep.StateBag) {
|
||||
if s.vmID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
ui.Say(fmt.Sprintf("Deleting VM %s...", s.vmID))
|
||||
err := deleteVMWithDisks(s.vmID, state)
|
||||
if err != nil {
|
||||
ui.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func deleteVMWithDisks(vmID string, state multistep.StateBag) error {
|
||||
client := state.Get("client").(*openapi.APIClient)
|
||||
hdds, _, err := client.VmApi.VmListHdd(context.TODO(), vmID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error listing hdd: %s", formatOpenAPIError(err))
|
||||
}
|
||||
|
||||
deleteOptions := openapi.VmDelete{}
|
||||
for _, hdd := range hdds {
|
||||
deleteOptions.RemoveDisks = append(deleteOptions.RemoveDisks, hdd.Disk.Id)
|
||||
}
|
||||
|
||||
_, err = client.VmApi.VmDelete(context.TODO(), vmID, deleteOptions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error deleting server '%s' - please delete it manually: %s", vmID, formatOpenAPIError(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package hyperone
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/hyperonecom/h1-client-go"
|
||||
)
|
||||
|
||||
type stepCreateVMFromDisk struct {
|
||||
vmID string
|
||||
}
|
||||
|
||||
func (s *stepCreateVMFromDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
client := state.Get("client").(*openapi.APIClient)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
config := state.Get("config").(*Config)
|
||||
sshKey := state.Get("ssh_public_key").(string)
|
||||
chrootDiskID := state.Get("chroot_disk_id").(string)
|
||||
|
||||
ui.Say("Creating VM from disk...")
|
||||
|
||||
options := openapi.VmCreate{
|
||||
Name: config.VmName,
|
||||
Service: config.VmType,
|
||||
Disk: []openapi.VmCreateDisk{
|
||||
{
|
||||
Id: chrootDiskID,
|
||||
},
|
||||
},
|
||||
SshKeys: []string{sshKey},
|
||||
Boot: false,
|
||||
}
|
||||
|
||||
vm, _, err := client.VmApi.VmCreate(ctx, options)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("error creating VM from disk: %s", formatOpenAPIError(err))
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
s.vmID = vm.Id
|
||||
state.Put("vm_id", vm.Id)
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepCreateVMFromDisk) Cleanup(state multistep.StateBag) {
|
||||
if s.vmID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
ui.Say(fmt.Sprintf("Deleting VM %s (from chroot disk)...", s.vmID))
|
||||
err := deleteVMWithDisks(s.vmID, state)
|
||||
if err != nil {
|
||||
ui.Error(err.Error())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package hyperone
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hyperonecom/h1-client-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPickNetAdapter(t *testing.T) {
|
||||
cases := []struct {
|
||||
Name string
|
||||
Config Config
|
||||
Expected openapi.VmCreateNetadp
|
||||
}{
|
||||
{
|
||||
Name: "no_network",
|
||||
Config: Config{
|
||||
PublicNetAdpService: "public",
|
||||
},
|
||||
Expected: openapi.VmCreateNetadp{
|
||||
Service: "public",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "no_network_public_ip",
|
||||
Config: Config{
|
||||
PublicIP: "some-public-ip",
|
||||
PublicNetAdpService: "public",
|
||||
},
|
||||
Expected: openapi.VmCreateNetadp{
|
||||
Service: "public",
|
||||
Ip: []string{"some-public-ip"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "no_network_private_ip",
|
||||
Config: Config{
|
||||
PrivateIP: "some-private-ip",
|
||||
PublicNetAdpService: "public",
|
||||
},
|
||||
Expected: openapi.VmCreateNetadp{
|
||||
Service: "public",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "no_network_both_ip",
|
||||
Config: Config{
|
||||
PublicIP: "some-public-ip",
|
||||
PrivateIP: "some-private-ip",
|
||||
PublicNetAdpService: "public",
|
||||
},
|
||||
Expected: openapi.VmCreateNetadp{
|
||||
Service: "public",
|
||||
Ip: []string{"some-public-ip"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "network_no_ip",
|
||||
Config: Config{
|
||||
Network: "some-network",
|
||||
PublicNetAdpService: "public",
|
||||
},
|
||||
Expected: openapi.VmCreateNetadp{
|
||||
Service: "private",
|
||||
Network: "some-network",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "network_public_ip",
|
||||
Config: Config{
|
||||
Network: "some-network",
|
||||
PublicIP: "some-public-ip",
|
||||
PublicNetAdpService: "public",
|
||||
},
|
||||
Expected: openapi.VmCreateNetadp{
|
||||
Service: "private",
|
||||
Network: "some-network",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "network_private_ip",
|
||||
Config: Config{
|
||||
Network: "some-network",
|
||||
PrivateIP: "some-private-ip",
|
||||
PublicNetAdpService: "public",
|
||||
},
|
||||
Expected: openapi.VmCreateNetadp{
|
||||
Service: "private",
|
||||
Network: "some-network",
|
||||
Ip: []string{"some-private-ip"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "network_both_ip",
|
||||
Config: Config{
|
||||
Network: "some-network",
|
||||
PublicIP: "some-public-ip",
|
||||
PrivateIP: "some-private-ip",
|
||||
PublicNetAdpService: "public",
|
||||
},
|
||||
Expected: openapi.VmCreateNetadp{
|
||||
Service: "private",
|
||||
Network: "some-network",
|
||||
Ip: []string{"some-private-ip"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.Name, func(t *testing.T) {
|
||||
result := pickNetAdapter(&c.Config)
|
||||
assert.Equal(t, c.Expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package hyperone
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/hyperonecom/h1-client-go"
|
||||
)
|
||||
|
||||
type stepDetachDisk struct {
|
||||
vmID string
|
||||
}
|
||||
|
||||
func (s *stepDetachDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
client := state.Get("client").(*openapi.APIClient)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
vmID := state.Get("vm_id").(string)
|
||||
chrootDiskID := state.Get("chroot_disk_id").(string)
|
||||
|
||||
ui.Say("Detaching chroot disk...")
|
||||
_, _, err := client.VmApi.VmDeleteHddDiskId(ctx, vmID, chrootDiskID)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("error detaching disk: %s", formatOpenAPIError(err))
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepDetachDisk) Cleanup(state multistep.StateBag) {}
|
||||
@@ -0,0 +1,51 @@
|
||||
package hyperone
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
||||
type stepMountChroot struct{}
|
||||
|
||||
func (s *stepMountChroot) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
device := state.Get("device").(string)
|
||||
|
||||
log.Printf("Mount path: %s", config.ChrootMountPath)
|
||||
|
||||
ui.Say(fmt.Sprintf("Creating mount directory: %s", config.ChrootMountPath))
|
||||
|
||||
opts := ""
|
||||
if len(config.MountOptions) > 0 {
|
||||
opts = "-o " + strings.Join(config.MountOptions, " -o ")
|
||||
}
|
||||
|
||||
deviceMount := device
|
||||
if config.MountPartition != "" {
|
||||
deviceMount = fmt.Sprintf("%s%s", device, config.MountPartition)
|
||||
}
|
||||
|
||||
commands := []string{
|
||||
fmt.Sprintf("mkdir -m 755 -p %s", config.ChrootMountPath),
|
||||
fmt.Sprintf("mount %s %s %s", opts, deviceMount, config.ChrootMountPath),
|
||||
}
|
||||
|
||||
err := runCommands(commands, config.ctx, state)
|
||||
if err != nil {
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
state.Put("mount_path", config.ChrootMountPath)
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepMountChroot) Cleanup(state multistep.StateBag) {}
|
||||
@@ -0,0 +1,45 @@
|
||||
package hyperone
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
||||
type stepMountExtra struct{}
|
||||
|
||||
func (s *stepMountExtra) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
mountPath := state.Get("mount_path").(string)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
ui.Say("Mounting additional paths within the chroot...")
|
||||
for _, mountInfo := range config.ChrootMounts {
|
||||
innerPath := mountPath + mountInfo[2]
|
||||
|
||||
flags := "-t " + mountInfo[0]
|
||||
if mountInfo[0] == "bind" {
|
||||
flags = "--bind"
|
||||
}
|
||||
|
||||
ui.Message(fmt.Sprintf("Mounting: %s", mountInfo[2]))
|
||||
|
||||
commands := []string{
|
||||
fmt.Sprintf("mkdir -m 755 -p %s", innerPath),
|
||||
fmt.Sprintf("mount %s %s %s", flags, mountInfo[1], innerPath),
|
||||
}
|
||||
|
||||
err := runCommands(commands, config.ctx, state)
|
||||
if err != nil {
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepMountExtra) Cleanup(state multistep.StateBag) {}
|
||||
@@ -0,0 +1,37 @@
|
||||
package hyperone
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
||||
type postMountCommandsData struct {
|
||||
Device string
|
||||
MountPath string
|
||||
}
|
||||
|
||||
type stepPostMountCommands struct{}
|
||||
|
||||
func (s *stepPostMountCommands) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
device := state.Get("device").(string)
|
||||
|
||||
ctx := config.ctx
|
||||
ctx.Data = &postMountCommandsData{
|
||||
Device: device,
|
||||
MountPath: config.ChrootMountPath,
|
||||
}
|
||||
|
||||
ui.Say("Running post-mount commands...")
|
||||
if err := runCommands(config.PostMountCommands, ctx, state); err != nil {
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepPostMountCommands) Cleanup(state multistep.StateBag) {}
|
||||
@@ -0,0 +1,37 @@
|
||||
package hyperone
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
||||
type preMountCommandsData struct {
|
||||
Device string
|
||||
MountPath string
|
||||
}
|
||||
|
||||
type stepPreMountCommands struct{}
|
||||
|
||||
func (s *stepPreMountCommands) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
device := state.Get("device").(string)
|
||||
|
||||
ctx := config.ctx
|
||||
ctx.Data = &preMountCommandsData{
|
||||
Device: device,
|
||||
MountPath: config.ChrootMountPath,
|
||||
}
|
||||
|
||||
ui.Say("Running pre-mount commands...")
|
||||
if err := runCommands(config.PreMountCommands, ctx, state); err != nil {
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepPreMountCommands) Cleanup(state multistep.StateBag) {}
|
||||
@@ -0,0 +1,50 @@
|
||||
package hyperone
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
||||
const (
|
||||
vmBusPath = "/sys/bus/vmbus/devices"
|
||||
)
|
||||
|
||||
type stepPrepareDevice struct{}
|
||||
|
||||
func (s *stepPrepareDevice) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
controllerNumber := state.Get("chroot_controller_number").(string)
|
||||
controllerLocation := state.Get("chroot_controller_location").(int)
|
||||
|
||||
log.Println("Searching for available device...")
|
||||
|
||||
cmd := fmt.Sprintf("find %s/%s/ -path *:%d/block -exec ls {} \\;",
|
||||
vmBusPath, controllerNumber, controllerLocation)
|
||||
|
||||
block, err := captureOutput(cmd, state)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("error finding available device: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
if block == "" {
|
||||
err := fmt.Errorf("device not found")
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
device := fmt.Sprintf("/dev/%s", block)
|
||||
|
||||
ui.Say(fmt.Sprintf("Found device: %s", device))
|
||||
state.Put("device", device)
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepPrepareDevice) Cleanup(state multistep.StateBag) {}
|
||||
@@ -0,0 +1,32 @@
|
||||
package hyperone
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/hyperonecom/h1-client-go"
|
||||
)
|
||||
|
||||
type stepStopVM struct{}
|
||||
|
||||
func (s *stepStopVM) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
client := state.Get("client").(*openapi.APIClient)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
vmID := state.Get("vm_id").(string)
|
||||
|
||||
ui.Say("Stopping VM...")
|
||||
|
||||
_, _, err := client.VmApi.VmActionStop(ctx, vmID)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("error stopping VM: %s", formatOpenAPIError(err))
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepStopVM) Cleanup(multistep.StateBag) {}
|
||||
@@ -0,0 +1,89 @@
|
||||
package hyperone
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/crypto/ssh/agent"
|
||||
|
||||
"github.com/hashicorp/packer/common/json"
|
||||
)
|
||||
|
||||
const (
|
||||
sshAddress = "api.hyperone.com:22"
|
||||
sshSubsystem = "rbx-auth"
|
||||
hostKeyHash = "3e2aa423d42d7e8b14d50625512c8ac19db767ed"
|
||||
)
|
||||
|
||||
type sshData struct {
|
||||
ID string `json:"_id"`
|
||||
}
|
||||
|
||||
func sshAgent() ssh.AuthMethod {
|
||||
if sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")); err == nil {
|
||||
return ssh.PublicKeysCallback(agent.NewClient(sshAgent).Signers)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fetchTokenBySSH(user string) (string, error) {
|
||||
sshConfig := &ssh.ClientConfig{
|
||||
User: user,
|
||||
Auth: []ssh.AuthMethod{
|
||||
sshAgent(),
|
||||
},
|
||||
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
||||
hash := sha1Sum(key)
|
||||
if hash != hostKeyHash {
|
||||
return fmt.Errorf("invalid host key hash: %s", hash)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
client, err := ssh.Dial("tcp", sshAddress, sshConfig)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
stdout, err := session.StdoutPipe()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = session.RequestSubsystem(sshSubsystem)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
out, err := ioutil.ReadAll(stdout)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var data sshData
|
||||
err = json.Unmarshal(out, &data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return data.ID, nil
|
||||
}
|
||||
|
||||
func sha1Sum(pubKey ssh.PublicKey) string {
|
||||
sum := sha1.Sum(pubKey.Marshal())
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package hyperone
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/hashicorp/packer/template/interpolate"
|
||||
"github.com/hyperonecom/h1-client-go"
|
||||
)
|
||||
|
||||
func formatOpenAPIError(err error) string {
|
||||
openAPIError, ok := err.(openapi.GenericOpenAPIError)
|
||||
if !ok {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s (body: %s)", openAPIError.Error(), openAPIError.Body())
|
||||
}
|
||||
|
||||
func runCommands(commands []string, ctx interpolate.Context, state multistep.StateBag) error {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
|
||||
comm := state.Get("communicator").(packer.Communicator)
|
||||
|
||||
for _, rawCmd := range commands {
|
||||
intCmd, err := interpolate.Render(rawCmd, &ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error interpolating: %s", err)
|
||||
}
|
||||
|
||||
command, err := wrappedCommand(intCmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error wrapping command: %s", err)
|
||||
}
|
||||
|
||||
remoteCmd := &packer.RemoteCmd{
|
||||
Command: command,
|
||||
}
|
||||
|
||||
ui.Say(fmt.Sprintf("Executing command: %s", command))
|
||||
|
||||
err = remoteCmd.StartWithUi(comm, ui)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error running remote cmd: %s", err)
|
||||
}
|
||||
|
||||
if remoteCmd.ExitStatus != 0 {
|
||||
return fmt.Errorf(
|
||||
"received non-zero exit code %d from command: %s",
|
||||
remoteCmd.ExitStatus,
|
||||
command)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func captureOutput(command string, state multistep.StateBag) (string, error) {
|
||||
comm := state.Get("communicator").(packer.Communicator)
|
||||
|
||||
var stdout bytes.Buffer
|
||||
remoteCmd := &packer.RemoteCmd{
|
||||
Command: command,
|
||||
Stdout: &stdout,
|
||||
}
|
||||
|
||||
log.Println(fmt.Sprintf("Executing command: %s", command))
|
||||
|
||||
err := comm.Start(remoteCmd)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error running remote cmd: %s", err)
|
||||
}
|
||||
|
||||
remoteCmd.Wait()
|
||||
if remoteCmd.ExitStatus != 0 {
|
||||
return "", fmt.Errorf(
|
||||
"received non-zero exit code %d from command: %s",
|
||||
remoteCmd.ExitStatus,
|
||||
command)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(stdout.String()), nil
|
||||
}
|
||||
@@ -61,9 +61,9 @@ func (s *StepCreateVM) Run(_ context.Context, state multistep.StateBag) multiste
|
||||
}
|
||||
|
||||
// convert the MB to bytes
|
||||
ramSize := int64(s.RamSize * 1024 * 1024)
|
||||
diskSize := int64(s.DiskSize * 1024 * 1024)
|
||||
diskBlockSize := int64(s.DiskBlockSize * 1024 * 1024)
|
||||
ramSize := int64(s.RamSize) * 1024 * 1024
|
||||
diskSize := int64(s.DiskSize) * 1024 * 1024
|
||||
diskBlockSize := int64(s.DiskBlockSize) * 1024 * 1024
|
||||
|
||||
err := driver.CreateVirtualMachine(s.VMName, path, harddrivePath, ramSize, diskSize, diskBlockSize,
|
||||
s.SwitchName, s.Generation, s.DifferencingDisk, s.FixedVHD, s.Version)
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"log"
|
||||
|
||||
"github.com/gophercloud/gophercloud"
|
||||
"github.com/gophercloud/gophercloud/openstack/compute/v2/images"
|
||||
"github.com/gophercloud/gophercloud/openstack/imageservice/v2/images"
|
||||
)
|
||||
|
||||
// Artifact is an artifact implementation that contains built images.
|
||||
|
||||
@@ -71,6 +71,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
return nil, fmt.Errorf("Error initializing compute client: %s", err)
|
||||
}
|
||||
|
||||
imageClient, err := b.config.imageV2Client()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error initializing image client: %s", err)
|
||||
}
|
||||
|
||||
// Setup the state bag and initial state for the steps
|
||||
state := new(multistep.BasicStateBag)
|
||||
state.Put("config", &b.config)
|
||||
@@ -165,7 +170,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
artifact := &Artifact{
|
||||
ImageId: state.Get("image").(string),
|
||||
BuilderIdValue: BuilderId,
|
||||
Client: computeClient,
|
||||
Client: imageClient,
|
||||
}
|
||||
|
||||
return artifact, nil
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions"
|
||||
|
||||
"github.com/gophercloud/gophercloud"
|
||||
"github.com/gophercloud/gophercloud/openstack/compute/v2/images"
|
||||
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
|
||||
"github.com/gophercloud/gophercloud/openstack/imageservice/v2/images"
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
@@ -19,19 +19,27 @@ type stepCreateImage struct {
|
||||
UseBlockStorageVolume bool
|
||||
}
|
||||
|
||||
func (s *stepCreateImage) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
func (s *stepCreateImage) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
server := state.Get("server").(*servers.Server)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
// We need the v2 compute client
|
||||
client, err := config.computeV2Client()
|
||||
computeClient, err := config.computeV2Client()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error initializing compute client: %s", err)
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// We need the v2 image client
|
||||
imageClient, err := config.imageV2Client()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error initializing image service client: %s", err)
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Create the image.
|
||||
// Image source depends on the type of the Compute instance. It can be
|
||||
// Block Storage service volume or regular Compute service local volume.
|
||||
@@ -58,7 +66,7 @@ func (s *stepCreateImage) Run(_ context.Context, state multistep.StateBag) multi
|
||||
}
|
||||
imageId = image.ImageID
|
||||
} else {
|
||||
imageId, err = servers.CreateImage(client, server.ID, servers.CreateImageOpts{
|
||||
imageId, err = servers.CreateImage(computeClient, server.ID, servers.CreateImageOpts{
|
||||
Name: config.ImageName,
|
||||
Metadata: config.ImageMetadata,
|
||||
}).ExtractImageID()
|
||||
@@ -76,7 +84,7 @@ func (s *stepCreateImage) Run(_ context.Context, state multistep.StateBag) multi
|
||||
|
||||
// Wait for the image to become ready
|
||||
ui.Say(fmt.Sprintf("Waiting for image %s (image id: %s) to become ready...", config.ImageName, imageId))
|
||||
if err := WaitForImage(client, imageId); err != nil {
|
||||
if err := WaitForImage(ctx, imageClient, imageId); err != nil {
|
||||
err := fmt.Errorf("Error waiting for image: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
@@ -91,11 +99,14 @@ func (s *stepCreateImage) Cleanup(multistep.StateBag) {
|
||||
}
|
||||
|
||||
// WaitForImage waits for the given Image ID to become ready.
|
||||
func WaitForImage(client *gophercloud.ServiceClient, imageId string) error {
|
||||
func WaitForImage(ctx context.Context, client *gophercloud.ServiceClient, imageId string) error {
|
||||
maxNumErrors := 10
|
||||
numErrors := 0
|
||||
|
||||
for {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
image, err := images.Get(client, imageId).Extract()
|
||||
if err != nil {
|
||||
errCode, ok := err.(*gophercloud.ErrUnexpectedResponseCode)
|
||||
@@ -113,11 +124,11 @@ func WaitForImage(client *gophercloud.ServiceClient, imageId string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if image.Status == "ACTIVE" {
|
||||
if image.Status == "active" {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("Waiting for image creation status: %s (%d%%)", image.Status, image.Progress)
|
||||
log.Printf("Waiting for image creation status: %s", image.Status)
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package vagrant
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
||||
// This is the common builder ID to all of these artifacts.
|
||||
const BuilderId = "vagrant"
|
||||
|
||||
// Artifact is the result of running the vagrant builder, namely a set
|
||||
// of files associated with the resulting machine.
|
||||
type artifact struct {
|
||||
OutputDir string
|
||||
BoxName string
|
||||
}
|
||||
|
||||
// NewArtifact returns a vagrant artifact containing the .box file
|
||||
func NewArtifact(dir string) (packer.Artifact, error) {
|
||||
return &artifact{
|
||||
OutputDir: dir,
|
||||
BoxName: "package.box",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (*artifact) BuilderId() string {
|
||||
return BuilderId
|
||||
}
|
||||
|
||||
func (a *artifact) Files() []string {
|
||||
return []string{a.BoxName}
|
||||
}
|
||||
|
||||
func (a *artifact) Id() string {
|
||||
return filepath.Join(a.OutputDir, a.BoxName)
|
||||
}
|
||||
|
||||
func (a *artifact) String() string {
|
||||
return fmt.Sprintf("Vagrant box is %s", a.Id())
|
||||
}
|
||||
|
||||
func (a *artifact) State(name string) interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *artifact) Destroy() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package vagrant
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
||||
func TestArtifact_Impl(t *testing.T) {
|
||||
var raw interface{} = &artifact{}
|
||||
|
||||
if _, ok := raw.(packer.Artifact); !ok {
|
||||
t.Fatalf("Artifact does not implement packer.Artifact")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArtifactId(t *testing.T) {
|
||||
a := &artifact{
|
||||
OutputDir: "/my/dir",
|
||||
BoxName: "package.box",
|
||||
}
|
||||
|
||||
expected := "/my/dir/package.box"
|
||||
if runtime.GOOS == "windows" {
|
||||
expected = strings.Replace(expected, "/", "\\", -1)
|
||||
}
|
||||
if strings.Compare(a.Id(), expected) != 0 {
|
||||
t.Fatalf("artifact ID should match: expected: %s received: %s", expected, a.Id())
|
||||
}
|
||||
}
|
||||
|
||||
func TestArtifactString(t *testing.T) {
|
||||
a := &artifact{
|
||||
OutputDir: "/my/dir",
|
||||
BoxName: "package.box",
|
||||
}
|
||||
expected := "Vagrant box is /my/dir/package.box"
|
||||
if runtime.GOOS == "windows" {
|
||||
expected = strings.Replace(expected, "/", "\\", -1)
|
||||
}
|
||||
|
||||
if strings.Compare(a.String(), expected) != 0 {
|
||||
t.Fatalf("artifact string should match: expected: %s received: %s", expected, a.String())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
package vagrant
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/packer/common"
|
||||
"github.com/hashicorp/packer/common/bootcommand"
|
||||
"github.com/hashicorp/packer/helper/communicator"
|
||||
"github.com/hashicorp/packer/helper/config"
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/hashicorp/packer/template/interpolate"
|
||||
)
|
||||
|
||||
// Builder implements packer.Builder and builds the actual VirtualBox
|
||||
// images.
|
||||
type Builder struct {
|
||||
config *Config
|
||||
runner multistep.Runner
|
||||
}
|
||||
|
||||
type SSHConfig struct {
|
||||
Comm communicator.Config `mapstructure:",squash"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
common.HTTPConfig `mapstructure:",squash"`
|
||||
common.ISOConfig `mapstructure:",squash"`
|
||||
common.FloppyConfig `mapstructure:",squash"`
|
||||
bootcommand.BootConfig `mapstructure:",squash"`
|
||||
SSHConfig `mapstructure:",squash"`
|
||||
|
||||
// This is the name of the new virtual machine.
|
||||
// By default this is "packer-BUILDNAME", where "BUILDNAME" is the name of the build.
|
||||
OutputDir string `mapstructure:"output_dir"`
|
||||
SourceBox string `mapstructure:"source_path"`
|
||||
GlobalID string `mapstructure:"global_id"`
|
||||
Checksum string `mapstructure:"checksum"`
|
||||
ChecksumType string `mapstructure:"checksum_type"`
|
||||
BoxName string `mapstructure:"box_name"`
|
||||
|
||||
Provider string `mapstructure:"provider"`
|
||||
|
||||
Communicator string `mapstructure:"communicator"`
|
||||
|
||||
// What vagrantfile to use
|
||||
VagrantfileTpl string `mapstructure:"vagrantfile_template"`
|
||||
|
||||
// Whether to Halt, Suspend, or Destroy the box
|
||||
TeardownMethod string `mapstructure:"teardown_method"`
|
||||
|
||||
// Options for the "vagrant init" command
|
||||
BoxVersion string `mapstructure:"box_version"`
|
||||
Template string `mapstructure:"template"`
|
||||
SyncedFolder string `mapstructure:"synced_folder"`
|
||||
|
||||
// Options for the "vagrant box add" command
|
||||
SkipAdd bool `mapstructure:"skip_add"`
|
||||
AddCACert string `mapstructure:"add_cacert"`
|
||||
AddCAPath string `mapstructure:"add_capath"`
|
||||
AddCert string `mapstructure:"add_cert"`
|
||||
AddClean bool `mapstructure:"add_clean"`
|
||||
AddForce bool `mapstructure:"add_force"`
|
||||
AddInsecure bool `mapstructure:"add_insecure"`
|
||||
|
||||
// Don't package the Vagrant box after build.
|
||||
SkipPackage bool `mapstructure:"skip_package"`
|
||||
OutputVagrantfile string `mapstructure:"output_vagrantfile"`
|
||||
PackageInclude []string `mapstructure:"package_include"`
|
||||
|
||||
ctx interpolate.Context
|
||||
}
|
||||
|
||||
// Prepare processes the build configuration parameters.
|
||||
func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
||||
b.config = new(Config)
|
||||
err := config.Decode(&b.config, &config.DecodeOpts{
|
||||
Interpolate: true,
|
||||
InterpolateContext: &b.config.ctx,
|
||||
InterpolateFilter: &interpolate.RenderFilter{
|
||||
Exclude: []string{
|
||||
"boot_command",
|
||||
},
|
||||
},
|
||||
}, raws...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Accumulate any errors and warnings
|
||||
var errs *packer.MultiError
|
||||
warnings := make([]string, 0)
|
||||
|
||||
if b.config.OutputDir == "" {
|
||||
b.config.OutputDir = fmt.Sprintf("output-%s", b.config.PackerBuildName)
|
||||
}
|
||||
|
||||
if b.config.Comm.SSHTimeout == 0 {
|
||||
b.config.Comm.SSHTimeout = 10 * time.Minute
|
||||
}
|
||||
|
||||
if b.config.Comm.Type != "ssh" {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf(`The Vagrant builder currently only supports the ssh communicator"`))
|
||||
}
|
||||
// The box isn't a namespace like you'd pull from vagrant cloud
|
||||
if b.config.BoxName == "" {
|
||||
b.config.BoxName = fmt.Sprintf("packer_%s", b.config.PackerBuildName)
|
||||
}
|
||||
|
||||
if b.config.SourceBox == "" {
|
||||
if b.config.GlobalID == "" {
|
||||
errs = packer.MultiErrorAppend(errs, fmt.Errorf("source_path is required unless you have set global_id"))
|
||||
}
|
||||
} else {
|
||||
if b.config.GlobalID != "" {
|
||||
errs = packer.MultiErrorAppend(errs, fmt.Errorf("You may either set global_id or source_path but not both"))
|
||||
}
|
||||
if strings.HasSuffix(b.config.SourceBox, ".box") {
|
||||
b.config.SourceBox, err = common.ValidatedURL(b.config.SourceBox)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(errs, fmt.Errorf("source_path is invalid: %s", err))
|
||||
}
|
||||
fileOK := common.FileExistsLocally(b.config.SourceBox)
|
||||
if !fileOK {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("Source file '%s' needs to exist at time of config validation!", b.config.SourceBox))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if b.config.TeardownMethod == "" {
|
||||
// If we're using a box that's already opened on the system, don't
|
||||
// automatically destroy it. If we open the box ourselves, then go ahead
|
||||
// and kill it by default.
|
||||
if b.config.GlobalID != "" {
|
||||
b.config.TeardownMethod = "halt"
|
||||
} else {
|
||||
b.config.TeardownMethod = "destroy"
|
||||
}
|
||||
} else {
|
||||
matches := false
|
||||
for _, name := range []string{"halt", "suspend", "destroy"} {
|
||||
if strings.ToLower(b.config.TeardownMethod) == name {
|
||||
matches = true
|
||||
}
|
||||
}
|
||||
if !matches {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf(`TeardownMethod must be "halt", "suspend", or "destroy"`))
|
||||
}
|
||||
}
|
||||
|
||||
if errs != nil && len(errs.Errors) > 0 {
|
||||
return warnings, errs
|
||||
}
|
||||
|
||||
return warnings, nil
|
||||
}
|
||||
|
||||
// Run executes a Packer build and returns a packer.Artifact representing
|
||||
// a VirtualBox appliance.
|
||||
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
|
||||
// Create the driver that we'll use to communicate with VirtualBox
|
||||
VagrantCWD, err := filepath.Abs(b.config.OutputDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
driver, err := NewDriver(VagrantCWD)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed creating VirtualBox driver: %s", err)
|
||||
}
|
||||
|
||||
// Set up the state.
|
||||
state := new(multistep.BasicStateBag)
|
||||
state.Put("config", b.config)
|
||||
state.Put("debug", b.config.PackerDebug)
|
||||
state.Put("driver", driver)
|
||||
state.Put("cache", cache)
|
||||
state.Put("hook", hook)
|
||||
state.Put("ui", ui)
|
||||
|
||||
// Build the steps.
|
||||
steps := []multistep.Step{}
|
||||
// Download if source box isn't from vagrant cloud.
|
||||
if strings.HasSuffix(b.config.SourceBox, ".box") {
|
||||
steps = append(steps, &common.StepDownload{
|
||||
Checksum: b.config.Checksum,
|
||||
ChecksumType: b.config.ChecksumType,
|
||||
Description: "Box",
|
||||
Extension: "box",
|
||||
ResultKey: "box_path",
|
||||
Url: []string{b.config.SourceBox},
|
||||
})
|
||||
}
|
||||
steps = append(steps,
|
||||
&common.StepOutputDir{
|
||||
Force: b.config.PackerForce,
|
||||
Path: b.config.OutputDir,
|
||||
},
|
||||
&StepCreateVagrantfile{
|
||||
Template: b.config.Template,
|
||||
SyncedFolder: b.config.SyncedFolder,
|
||||
SourceBox: b.config.SourceBox,
|
||||
OutputDir: b.config.OutputDir,
|
||||
GlobalID: b.config.GlobalID,
|
||||
},
|
||||
&StepAddBox{
|
||||
BoxVersion: b.config.BoxVersion,
|
||||
CACert: b.config.AddCACert,
|
||||
CAPath: b.config.AddCAPath,
|
||||
DownloadCert: b.config.AddCert,
|
||||
Clean: b.config.AddClean,
|
||||
Force: b.config.AddForce,
|
||||
Insecure: b.config.AddInsecure,
|
||||
Provider: b.config.Provider,
|
||||
SourceBox: b.config.SourceBox,
|
||||
BoxName: b.config.BoxName,
|
||||
GlobalID: b.config.GlobalID,
|
||||
SkipAdd: b.config.SkipAdd,
|
||||
},
|
||||
&StepUp{
|
||||
TeardownMethod: b.config.TeardownMethod,
|
||||
Provider: b.config.Provider,
|
||||
GlobalID: b.config.GlobalID,
|
||||
},
|
||||
&StepSSHConfig{
|
||||
b.config.GlobalID,
|
||||
},
|
||||
&communicator.StepConnect{
|
||||
Config: &b.config.SSHConfig.Comm,
|
||||
Host: CommHost(),
|
||||
SSHConfig: b.config.SSHConfig.Comm.SSHConfigFunc(),
|
||||
},
|
||||
new(common.StepProvision),
|
||||
&StepPackage{
|
||||
SkipPackage: b.config.SkipPackage,
|
||||
Include: b.config.PackageInclude,
|
||||
Vagrantfile: b.config.OutputVagrantfile,
|
||||
GlobalID: b.config.GlobalID,
|
||||
})
|
||||
|
||||
// Run the steps.
|
||||
b.runner = common.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state)
|
||||
b.runner.Run(state)
|
||||
|
||||
// Report any errors.
|
||||
if rawErr, ok := state.GetOk("error"); ok {
|
||||
return nil, rawErr.(error)
|
||||
}
|
||||
|
||||
// If we were interrupted or cancelled, then just exit.
|
||||
if _, ok := state.GetOk(multistep.StateCancelled); ok {
|
||||
return nil, errors.New("Build was cancelled.")
|
||||
}
|
||||
|
||||
if _, ok := state.GetOk(multistep.StateHalted); ok {
|
||||
return nil, errors.New("Build was halted.")
|
||||
}
|
||||
|
||||
return NewArtifact(b.config.OutputDir)
|
||||
}
|
||||
|
||||
// Cancel.
|
||||
func (b *Builder) Cancel() {
|
||||
if b.runner != nil {
|
||||
log.Println("Cancelling the step runner...")
|
||||
b.runner.Cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package vagrant
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
||||
func TestBuilder_ImplementsBuilder(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = &Builder{}
|
||||
if _, ok := raw.(packer.Builder); !ok {
|
||||
t.Fatalf("Builder should be a builder")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilder_Prepare_ValidateSource(t *testing.T) {
|
||||
b := &Builder{}
|
||||
type testCase struct {
|
||||
config map[string]interface{}
|
||||
errExpected bool
|
||||
reason string
|
||||
}
|
||||
|
||||
cases := []testCase{
|
||||
{
|
||||
config: map[string]interface{}{
|
||||
"global_id": "a3559ec",
|
||||
},
|
||||
errExpected: true,
|
||||
reason: "Need to set SSH communicator.",
|
||||
},
|
||||
{
|
||||
config: map[string]interface{}{
|
||||
"global_id": "a3559ec",
|
||||
"communicator": "ssh",
|
||||
},
|
||||
errExpected: false,
|
||||
reason: "Shouldn't fail because we've set global_id",
|
||||
},
|
||||
{
|
||||
config: map[string]interface{}{
|
||||
"communicator": "ssh",
|
||||
},
|
||||
errExpected: true,
|
||||
reason: "Should fail because we must set source_path or global_id",
|
||||
},
|
||||
{
|
||||
config: map[string]interface{}{
|
||||
"source_path": "./mybox",
|
||||
"communicator": "ssh",
|
||||
},
|
||||
errExpected: false,
|
||||
reason: "Source path is set; we should be fine",
|
||||
},
|
||||
{
|
||||
config: map[string]interface{}{
|
||||
"source_path": "./mybox",
|
||||
"communicator": "ssh",
|
||||
"global_id": "a3559ec",
|
||||
},
|
||||
errExpected: true,
|
||||
reason: "Both source path and global are set: we should error.",
|
||||
},
|
||||
{
|
||||
config: map[string]interface{}{
|
||||
"communicator": "ssh",
|
||||
"global_id": "a3559ec",
|
||||
"teardown_method": "suspend",
|
||||
},
|
||||
errExpected: false,
|
||||
reason: "Valid argument for teardown method",
|
||||
},
|
||||
{
|
||||
config: map[string]interface{}{
|
||||
"communicator": "ssh",
|
||||
"global_id": "a3559ec",
|
||||
"teardown_method": "surspernd",
|
||||
},
|
||||
errExpected: true,
|
||||
reason: "Inalid argument for teardown method",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := b.Prepare(tc.config)
|
||||
if (err != nil) != tc.errExpected {
|
||||
t.Fatalf("Unexpected behavior from test case %#v; %s.", tc.config, tc.reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package vagrant
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// A driver is able to talk to Vagrant and perform certain
|
||||
// operations with it.
|
||||
|
||||
type VagrantDriver interface {
|
||||
// Calls "vagrant init"
|
||||
Init([]string) error
|
||||
|
||||
// Calls "vagrant add"
|
||||
Add([]string) error
|
||||
|
||||
// Calls "vagrant up"
|
||||
Up([]string) (string, string, error)
|
||||
|
||||
// Calls "vagrant halt"
|
||||
Halt(string) error
|
||||
|
||||
// Calls "vagrant suspend"
|
||||
Suspend(string) error
|
||||
|
||||
SSHConfig(string) (*VagrantSSHConfig, error)
|
||||
|
||||
// Calls "vagrant destroy"
|
||||
Destroy(string) error
|
||||
|
||||
// Calls "vagrant package"[
|
||||
Package([]string) error
|
||||
|
||||
// Verify checks to make sure that this driver should function
|
||||
// properly. If there is any indication the driver can't function,
|
||||
// this will return an error.
|
||||
Verify() error
|
||||
|
||||
// Version reads the version of VirtualBox that is installed.
|
||||
Version() (string, error)
|
||||
}
|
||||
|
||||
func NewDriver(outputDir string) (VagrantDriver, error) {
|
||||
// Hardcode path for now while I'm developing. Obviously this path needs
|
||||
// to be discovered based on OS.
|
||||
vagrantBinary := "vagrant"
|
||||
if runtime.GOOS == "windows" {
|
||||
vagrantBinary = "vagrant.exe"
|
||||
}
|
||||
|
||||
if _, err := exec.LookPath(vagrantBinary); err != nil {
|
||||
return nil, fmt.Errorf("Error: Packer cannot find Vagrant in the path: %s", err.Error())
|
||||
}
|
||||
|
||||
driver := &Vagrant_2_2_Driver{
|
||||
vagrantBinary: vagrantBinary,
|
||||
VagrantCWD: outputDir,
|
||||
}
|
||||
|
||||
if err := driver.Verify(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return driver, nil
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package vagrant
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
)
|
||||
|
||||
const VAGRANT_MIN_VERSION = ">= 2.0.2"
|
||||
|
||||
type Vagrant_2_2_Driver struct {
|
||||
vagrantBinary string
|
||||
VagrantCWD string
|
||||
}
|
||||
|
||||
// Calls "vagrant init"
|
||||
func (d *Vagrant_2_2_Driver) Init(args []string) error {
|
||||
_, _, err := d.vagrantCmd(append([]string{"init"}, args...)...)
|
||||
return err
|
||||
}
|
||||
|
||||
// Calls "vagrant add"
|
||||
func (d *Vagrant_2_2_Driver) Add(args []string) error {
|
||||
// vagrant box add partyvm ubuntu-14.04.vmware.box
|
||||
_, _, err := d.vagrantCmd(append([]string{"box", "add"}, args...)...)
|
||||
return err
|
||||
}
|
||||
|
||||
// Calls "vagrant up"
|
||||
func (d *Vagrant_2_2_Driver) Up(args []string) (string, string, error) {
|
||||
stdout, stderr, err := d.vagrantCmd(append([]string{"up"}, args...)...)
|
||||
return stdout, stderr, err
|
||||
}
|
||||
|
||||
// Calls "vagrant halt"
|
||||
func (d *Vagrant_2_2_Driver) Halt(id string) error {
|
||||
args := []string{"halt"}
|
||||
if id != "" {
|
||||
args = append(args, id)
|
||||
}
|
||||
_, _, err := d.vagrantCmd(args...)
|
||||
return err
|
||||
}
|
||||
|
||||
// Calls "vagrant suspend"
|
||||
func (d *Vagrant_2_2_Driver) Suspend(id string) error {
|
||||
args := []string{"suspend"}
|
||||
if id != "" {
|
||||
args = append(args, id)
|
||||
}
|
||||
_, _, err := d.vagrantCmd(args...)
|
||||
return err
|
||||
}
|
||||
|
||||
// Calls "vagrant destroy"
|
||||
func (d *Vagrant_2_2_Driver) Destroy(id string) error {
|
||||
args := []string{"destroy", "-f"}
|
||||
if id != "" {
|
||||
args = append(args, id)
|
||||
}
|
||||
_, _, err := d.vagrantCmd(args...)
|
||||
return err
|
||||
}
|
||||
|
||||
// Calls "vagrant package"
|
||||
func (d *Vagrant_2_2_Driver) Package(args []string) error {
|
||||
args = append(args, "--output", filepath.Join(d.VagrantCWD, "package.box"))
|
||||
_, _, err := d.vagrantCmd(append([]string{"package"}, args...)...)
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify makes sure that Vagrant exists at the given path
|
||||
func (d *Vagrant_2_2_Driver) Verify() error {
|
||||
vagrantPath, err := exec.LookPath(d.vagrantBinary)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Can't find Vagrant binary!")
|
||||
}
|
||||
_, err = os.Stat(vagrantPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Can't find Vagrant binary.")
|
||||
}
|
||||
|
||||
constraints, err := version.NewConstraint(VAGRANT_MIN_VERSION)
|
||||
vers, err := d.Version()
|
||||
v, err := version.NewVersion(vers)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error figuring out Vagrant version.")
|
||||
}
|
||||
|
||||
if !constraints.Check(v) {
|
||||
return fmt.Errorf("installed Vagrant version must be >=2.0.2")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type VagrantSSHConfig struct {
|
||||
Hostname string
|
||||
User string
|
||||
Port string
|
||||
UserKnownHostsFile string
|
||||
StrictHostKeyChecking bool
|
||||
PasswordAuthentication bool
|
||||
IdentityFile string
|
||||
IdentitiesOnly bool
|
||||
LogLevel string
|
||||
}
|
||||
|
||||
func parseSSHConfig(lines []string, value string) string {
|
||||
out := ""
|
||||
for _, line := range lines {
|
||||
if index := strings.Index(line, value); index != -1 {
|
||||
out = line[index+len(value):]
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func yesno(yn string) bool {
|
||||
if yn == "no" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (d *Vagrant_2_2_Driver) SSHConfig(id string) (*VagrantSSHConfig, error) {
|
||||
// vagrant ssh-config --host 8df7860
|
||||
args := []string{"ssh-config"}
|
||||
if id != "" {
|
||||
args = append(args, id)
|
||||
}
|
||||
stdout, _, err := d.vagrantCmd(args...)
|
||||
sshConf := &VagrantSSHConfig{}
|
||||
|
||||
lines := strings.Split(stdout, "\n")
|
||||
sshConf.Hostname = parseSSHConfig(lines, "HostName ")
|
||||
sshConf.User = parseSSHConfig(lines, "User ")
|
||||
sshConf.Port = parseSSHConfig(lines, "Port ")
|
||||
sshConf.UserKnownHostsFile = parseSSHConfig(lines, "UserKnownHostsFile ")
|
||||
sshConf.IdentityFile = parseSSHConfig(lines, "IdentityFile ")
|
||||
sshConf.LogLevel = parseSSHConfig(lines, "LogLevel ")
|
||||
|
||||
// handle the booleans
|
||||
sshConf.StrictHostKeyChecking = yesno(parseSSHConfig(lines, "StrictHostKeyChecking "))
|
||||
sshConf.PasswordAuthentication = yesno(parseSSHConfig(lines, "PasswordAuthentication "))
|
||||
sshConf.IdentitiesOnly = yesno((parseSSHConfig(lines, "IdentitiesOnly ")))
|
||||
|
||||
return sshConf, err
|
||||
}
|
||||
|
||||
// Version reads the version of VirtualBox that is installed.
|
||||
func (d *Vagrant_2_2_Driver) Version() (string, error) {
|
||||
stdoutString, _, err := d.vagrantCmd([]string{"--version"}...)
|
||||
// Example stdout:
|
||||
|
||||
// Installed Version: 2.2.3
|
||||
//
|
||||
// Vagrant was unable to check for the latest version of Vagrant.
|
||||
// Please check manually at https://www.vagrantup.com
|
||||
|
||||
// Use regex to find version
|
||||
reg := regexp.MustCompile(`(\d+\.)?(\d+\.)?(\*|\d+)`)
|
||||
version := reg.FindString(stdoutString)
|
||||
if version == "" {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return version, nil
|
||||
}
|
||||
|
||||
func (d *Vagrant_2_2_Driver) vagrantCmd(args ...string) (string, string, error) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
|
||||
log.Printf("Calling Vagrant CLI: %#v", args)
|
||||
cmd := exec.Command(d.vagrantBinary, args...)
|
||||
cmd.Env = append(os.Environ(), fmt.Sprintf("VAGRANT_CWD=%s", d.VagrantCWD))
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Run()
|
||||
|
||||
stdoutString := strings.TrimSpace(stdout.String())
|
||||
stderrString := strings.TrimSpace(stderr.String())
|
||||
|
||||
if _, ok := err.(*exec.ExitError); ok {
|
||||
err = fmt.Errorf("Vagrant error: %s", stderrString)
|
||||
}
|
||||
|
||||
log.Printf("[vagrant driver] stdout: %s", stdoutString)
|
||||
log.Printf("[vagrant driver] stderr: %s", stderrString)
|
||||
|
||||
return stdoutString, stderrString, err
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package vagrant
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
)
|
||||
|
||||
func CommHost() func(multistep.StateBag) (string, error) {
|
||||
return func(state multistep.StateBag) (string, error) {
|
||||
config := state.Get("config").(*Config)
|
||||
return config.Comm.SSHHost, nil
|
||||
}
|
||||
}
|
||||
|
||||
func SSHPort() func(multistep.StateBag) (int, error) {
|
||||
return func(state multistep.StateBag) (int, error) {
|
||||
config := state.Get("config").(*Config)
|
||||
return config.Comm.SSHPort, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package vagrant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
||||
type StepAddBox struct {
|
||||
BoxVersion string
|
||||
CACert string
|
||||
CAPath string
|
||||
DownloadCert string
|
||||
Clean bool
|
||||
Force bool
|
||||
Insecure bool
|
||||
Provider string
|
||||
SourceBox string
|
||||
BoxName string
|
||||
GlobalID string
|
||||
SkipAdd bool
|
||||
}
|
||||
|
||||
func (s *StepAddBox) generateAddArgs() []string {
|
||||
addArgs := []string{}
|
||||
|
||||
if strings.HasSuffix(s.SourceBox, ".box") {
|
||||
addArgs = append(addArgs, s.BoxName)
|
||||
}
|
||||
|
||||
addArgs = append(addArgs, s.SourceBox)
|
||||
|
||||
if s.BoxVersion != "" {
|
||||
addArgs = append(addArgs, "--box-version", s.BoxVersion)
|
||||
}
|
||||
|
||||
if s.CACert != "" {
|
||||
addArgs = append(addArgs, "--cacert", s.CACert)
|
||||
}
|
||||
|
||||
if s.CAPath != "" {
|
||||
addArgs = append(addArgs, "--capath", s.CAPath)
|
||||
}
|
||||
|
||||
if s.DownloadCert != "" {
|
||||
addArgs = append(addArgs, "--cert", s.DownloadCert)
|
||||
}
|
||||
|
||||
if s.Clean {
|
||||
addArgs = append(addArgs, "--clean")
|
||||
}
|
||||
|
||||
if s.Force {
|
||||
addArgs = append(addArgs, "--force")
|
||||
}
|
||||
|
||||
if s.Insecure {
|
||||
addArgs = append(addArgs, "--insecure")
|
||||
}
|
||||
|
||||
if s.Provider != "" {
|
||||
addArgs = append(addArgs, "--provider", s.Provider)
|
||||
}
|
||||
|
||||
return addArgs
|
||||
}
|
||||
|
||||
func (s *StepAddBox) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
driver := state.Get("driver").(VagrantDriver)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
if s.SkipAdd {
|
||||
ui.Say("skip_add was set so we assume the box is already in Vagrant...")
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
if s.GlobalID != "" {
|
||||
ui.Say("Using a global-id; skipping Vagrant add command...")
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
ui.Say("Adding box using vagrant box add..")
|
||||
addArgs := s.generateAddArgs()
|
||||
|
||||
log.Printf("[vagrant] Calling box add with following args %s", strings.Join(addArgs, " "))
|
||||
// Call vagrant using prepared arguments
|
||||
err := driver.Add(addArgs)
|
||||
if err != nil {
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepAddBox) Cleanup(state multistep.StateBag) {
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package vagrant
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
)
|
||||
|
||||
func TestStepAdd_Impl(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = new(StepAddBox)
|
||||
if _, ok := raw.(multistep.Step); !ok {
|
||||
t.Fatalf("initialize should be a step")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepAddArgs(t *testing.T) {
|
||||
type testArgs struct {
|
||||
Step StepAddBox
|
||||
Expected []string
|
||||
}
|
||||
addTests := []testArgs{
|
||||
{
|
||||
Step: StepAddBox{
|
||||
SourceBox: "my_source_box.box",
|
||||
BoxName: "AWESOME BOX",
|
||||
},
|
||||
Expected: []string{"AWESOME BOX", "my_source_box.box"},
|
||||
},
|
||||
{
|
||||
Step: StepAddBox{
|
||||
SourceBox: "my_source_box",
|
||||
BoxName: "AWESOME BOX",
|
||||
},
|
||||
Expected: []string{"my_source_box"},
|
||||
},
|
||||
{
|
||||
Step: StepAddBox{
|
||||
BoxVersion: "eleventyone",
|
||||
CACert: "adfasdf",
|
||||
CAPath: "adfasdf",
|
||||
DownloadCert: "adfasdf",
|
||||
Clean: true,
|
||||
Force: true,
|
||||
Insecure: true,
|
||||
Provider: "virtualbox",
|
||||
SourceBox: "bananabox.box",
|
||||
BoxName: "bananas",
|
||||
},
|
||||
Expected: []string{"bananas", "bananabox.box", "--box-version", "eleventyone", "--cacert", "adfasdf", "--capath", "adfasdf", "--cert", "adfasdf", "--clean", "--force", "--insecure", "--provider", "virtualbox"},
|
||||
},
|
||||
}
|
||||
for _, addTest := range addTests {
|
||||
addArgs := addTest.Step.generateAddArgs()
|
||||
for i, val := range addTest.Expected {
|
||||
if strings.Compare(addArgs[i], val) != 0 {
|
||||
t.Fatalf("expected %#v but received %#v", addTest.Expected, addArgs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package vagrant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
||||
type StepCreateVagrantfile struct {
|
||||
Template string
|
||||
SourceBox string
|
||||
OutputDir string
|
||||
SyncedFolder string
|
||||
GlobalID string
|
||||
}
|
||||
|
||||
var DEFAULT_TEMPLATE = `Vagrant.configure("2") do |config|
|
||||
config.vm.box = "{{.BoxName}}"
|
||||
{{ if ne .SyncedFolder "" -}}
|
||||
config.vm.synced_folder "{{.SyncedFolder}}", "/vagrant"
|
||||
{{- else -}}
|
||||
config.vm.synced_folder ".", "/vagrant", disabled: true
|
||||
{{- end}}
|
||||
end`
|
||||
|
||||
type VagrantfileOptions struct {
|
||||
SyncedFolder string
|
||||
BoxName string
|
||||
}
|
||||
|
||||
func (s *StepCreateVagrantfile) createVagrantfile() (string, error) {
|
||||
tplPath := filepath.Join(s.OutputDir, "Vagrantfile")
|
||||
templateFile, err := os.Create(tplPath)
|
||||
if err != nil {
|
||||
retErr := fmt.Errorf("Error creating vagrantfile %s", err.Error())
|
||||
return "", retErr
|
||||
}
|
||||
|
||||
var tpl *template.Template
|
||||
if s.Template == "" {
|
||||
// Generate vagrantfile template based on our default
|
||||
tpl = template.Must(template.New("VagrantTpl").Parse(DEFAULT_TEMPLATE))
|
||||
} else {
|
||||
// Read in the template from provided file.
|
||||
tpl, err = template.ParseFiles(s.Template)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
opts := &VagrantfileOptions{
|
||||
SyncedFolder: s.SyncedFolder,
|
||||
BoxName: s.SourceBox,
|
||||
}
|
||||
|
||||
err = tpl.Execute(templateFile, opts)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
abspath, err := filepath.Abs(tplPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return abspath, nil
|
||||
}
|
||||
|
||||
func (s *StepCreateVagrantfile) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
// Skip the initialize step if we're trying to launch from a global ID.
|
||||
if s.GlobalID != "" {
|
||||
ui.Say("Using a global-id; skipping Vagrant init in this directory...")
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
ui.Say("Creating a Vagrantfile in the build directory...")
|
||||
vagrantfilePath, err := s.createVagrantfile()
|
||||
if err != nil {
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
log.Printf("Created vagrantfile at %s", vagrantfilePath)
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepCreateVagrantfile) Cleanup(state multistep.StateBag) {
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package vagrant
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
)
|
||||
|
||||
func TestStepCreateVagrantfile_Impl(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = new(StepCreateVagrantfile)
|
||||
if _, ok := raw.(multistep.Step); !ok {
|
||||
t.Fatalf("initialize should be a step")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateFile(t *testing.T) {
|
||||
testy := StepCreateVagrantfile{
|
||||
OutputDir: "./",
|
||||
SourceBox: "bananas",
|
||||
}
|
||||
templatePath, err := testy.createVagrantfile()
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
defer os.Remove(templatePath)
|
||||
contents, err := ioutil.ReadFile(templatePath)
|
||||
actual := string(contents)
|
||||
expected := `Vagrant.configure("2") do |config|
|
||||
config.vm.box = "bananas"
|
||||
config.vm.synced_folder ".", "/vagrant", disabled: true
|
||||
end`
|
||||
if ok := strings.Compare(actual, expected); ok != 0 {
|
||||
t.Fatalf("EXPECTED: \n%s\n\n RECEIVED: \n%s\n\n", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateFile_customSync(t *testing.T) {
|
||||
testy := StepCreateVagrantfile{
|
||||
OutputDir: "./",
|
||||
SyncedFolder: "myfolder/foldertimes",
|
||||
}
|
||||
templatePath, err := testy.createVagrantfile()
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
defer os.Remove(templatePath)
|
||||
contents, err := ioutil.ReadFile(templatePath)
|
||||
actual := string(contents)
|
||||
expected := `Vagrant.configure("2") do |config|
|
||||
config.vm.box = ""
|
||||
config.vm.synced_folder "myfolder/foldertimes", "/vagrant"
|
||||
end`
|
||||
if ok := strings.Compare(actual, expected); ok != 0 {
|
||||
t.Fatalf("EXPECTED: \n%s\n\n RECEIVED: \n%s\n\n", expected, actual)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package vagrant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
||||
type StepPackage struct {
|
||||
SkipPackage bool
|
||||
Include []string
|
||||
Vagrantfile string
|
||||
GlobalID string
|
||||
}
|
||||
|
||||
func (s *StepPackage) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
driver := state.Get("driver").(VagrantDriver)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
if s.SkipPackage {
|
||||
ui.Say("skip_package flag set; not going to call Vagrant package on this box.")
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
ui.Say("Packaging box...")
|
||||
packageArgs := []string{}
|
||||
if s.GlobalID != "" {
|
||||
packageArgs = append(packageArgs, s.GlobalID)
|
||||
}
|
||||
|
||||
if len(s.Include) > 0 {
|
||||
packageArgs = append(packageArgs, "--include", strings.Join(s.Include, ","))
|
||||
}
|
||||
if s.Vagrantfile != "" {
|
||||
packageArgs = append(packageArgs, "--vagrantfile", s.Vagrantfile)
|
||||
}
|
||||
|
||||
err := driver.Package(packageArgs)
|
||||
if err != nil {
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepPackage) Cleanup(state multistep.StateBag) {
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package vagrant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
)
|
||||
|
||||
// Vagrant already sets up ssh on the guests; our job is to find out what
|
||||
// it did. We can do that with the ssh-config command. Example output:
|
||||
|
||||
// $ vagrant ssh-config
|
||||
// Host default
|
||||
// HostName 172.16.41.194
|
||||
// User vagrant
|
||||
// Port 22
|
||||
// UserKnownHostsFile /dev/null
|
||||
// StrictHostKeyChecking no
|
||||
// PasswordAuthentication no
|
||||
// IdentityFile /Users/mmarsh/Projects/vagrant-boxes/ubuntu/.vagrant/machines/default/vmware_fusion/private_key
|
||||
// IdentitiesOnly yes
|
||||
// LogLevel FATAL
|
||||
|
||||
type StepSSHConfig struct {
|
||||
GlobalID string
|
||||
}
|
||||
|
||||
func (s *StepSSHConfig) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
driver := state.Get("driver").(VagrantDriver)
|
||||
config := state.Get("config").(*Config)
|
||||
|
||||
sshConfig, err := driver.SSHConfig(s.GlobalID)
|
||||
if err != nil {
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
config.Comm.SSHPrivateKeyFile = sshConfig.IdentityFile
|
||||
config.Comm.SSHUsername = sshConfig.User
|
||||
config.Comm.SSHHost = sshConfig.Hostname
|
||||
port, err := strconv.Atoi(sshConfig.Port)
|
||||
if err != nil {
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
config.Comm.SSHPort = port
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepSSHConfig) Cleanup(state multistep.StateBag) {
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package vagrant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
||||
type StepUp struct {
|
||||
TeardownMethod string
|
||||
Provider string
|
||||
GlobalID string
|
||||
}
|
||||
|
||||
func (s *StepUp) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
driver := state.Get("driver").(VagrantDriver)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
ui.Say("Calling Vagrant Up...")
|
||||
|
||||
var args []string
|
||||
if s.GlobalID != "" {
|
||||
args = append(args, s.GlobalID)
|
||||
}
|
||||
|
||||
if s.Provider != "" {
|
||||
args = append(args, fmt.Sprintf("--provider=%s", s.Provider))
|
||||
}
|
||||
|
||||
_, _, err := driver.Up(args)
|
||||
|
||||
if err != nil {
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepUp) Cleanup(state multistep.StateBag) {
|
||||
driver := state.Get("driver").(VagrantDriver)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
ui.Say(fmt.Sprintf("%sing Vagrant box...", s.TeardownMethod))
|
||||
|
||||
var err error
|
||||
if s.TeardownMethod == "halt" {
|
||||
err = driver.Halt(s.GlobalID)
|
||||
} else if s.TeardownMethod == "suspend" {
|
||||
err = driver.Suspend(s.GlobalID)
|
||||
} else if s.TeardownMethod == "destroy" {
|
||||
err = driver.Destroy(s.GlobalID)
|
||||
} else {
|
||||
// Should never get here because of template validation
|
||||
state.Put("error", fmt.Errorf("Invalid teardown method selected; must be either halt, suspend, or destory."))
|
||||
}
|
||||
if err != nil {
|
||||
state.Put("error", fmt.Errorf("Error halting Vagrant machine; please try to do this manually"))
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,9 @@ import (
|
||||
//
|
||||
// Produces:
|
||||
type StepAttachGuestAdditions struct {
|
||||
attachedPath string
|
||||
GuestAdditionsMode string
|
||||
attachedPath string
|
||||
GuestAdditionsMode string
|
||||
GuestAdditionsInterface string
|
||||
}
|
||||
|
||||
func (s *StepAttachGuestAdditions) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
@@ -40,12 +41,22 @@ func (s *StepAttachGuestAdditions) Run(_ context.Context, state multistep.StateB
|
||||
guestAdditionsPath := state.Get("guest_additions_path").(string)
|
||||
|
||||
// Attach the guest additions to the computer
|
||||
|
||||
controllerName := "IDE Controller"
|
||||
port := "1"
|
||||
device := "0"
|
||||
if s.GuestAdditionsInterface == "sata" {
|
||||
controllerName = "SATA Controller"
|
||||
port = "2"
|
||||
device = "0"
|
||||
}
|
||||
|
||||
log.Println("Attaching guest additions ISO onto IDE controller...")
|
||||
command := []string{
|
||||
"storageattach", vmName,
|
||||
"--storagectl", "IDE Controller",
|
||||
"--port", "1",
|
||||
"--device", "0",
|
||||
"--storagectl", controllerName,
|
||||
"--port", port,
|
||||
"--device", device,
|
||||
"--type", "dvddrive",
|
||||
"--medium", guestAdditionsPath,
|
||||
}
|
||||
@@ -71,11 +82,20 @@ func (s *StepAttachGuestAdditions) Cleanup(state multistep.StateBag) {
|
||||
driver := state.Get("driver").(Driver)
|
||||
vmName := state.Get("vmName").(string)
|
||||
|
||||
controllerName := "IDE Controller"
|
||||
port := "1"
|
||||
device := "0"
|
||||
if s.GuestAdditionsInterface == "sata" {
|
||||
controllerName = "SATA Controller"
|
||||
port = "2"
|
||||
device = "0"
|
||||
}
|
||||
|
||||
command := []string{
|
||||
"storageattach", vmName,
|
||||
"--storagectl", "IDE Controller",
|
||||
"--port", "1",
|
||||
"--device", "0",
|
||||
"--storagectl", controllerName,
|
||||
"--port", port,
|
||||
"--device", device,
|
||||
"--medium", "none",
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ import (
|
||||
//
|
||||
// Produces:
|
||||
type StepRemoveDevices struct {
|
||||
Bundling VBoxBundleConfig
|
||||
Bundling VBoxBundleConfig
|
||||
GuestAdditionsInterface string
|
||||
}
|
||||
|
||||
func (s *StepRemoveDevices) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
@@ -100,11 +101,19 @@ func (s *StepRemoveDevices) Run(_ context.Context, state multistep.StateBag) mul
|
||||
|
||||
if _, ok := state.GetOk("guest_additions_attached"); ok {
|
||||
ui.Message("Removing guest additions drive...")
|
||||
controllerName := "IDE Controller"
|
||||
port := "1"
|
||||
device := "0"
|
||||
if s.GuestAdditionsInterface == "sata" {
|
||||
controllerName = "SATA Controller"
|
||||
port = "2"
|
||||
device = "0"
|
||||
}
|
||||
command := []string{
|
||||
"storageattach", vmName,
|
||||
"--storagectl", "IDE Controller",
|
||||
"--port", "1",
|
||||
"--device", "0",
|
||||
"--storagectl", controllerName,
|
||||
"--port", port,
|
||||
"--device", device,
|
||||
"--medium", "none",
|
||||
}
|
||||
if err := driver.VBoxManage(command...); err != nil {
|
||||
|
||||
@@ -41,20 +41,21 @@ type Config struct {
|
||||
vboxcommon.VBoxVersionConfig `mapstructure:",squash"`
|
||||
vboxcommon.VBoxBundleConfig `mapstructure:",squash"`
|
||||
|
||||
DiskSize uint `mapstructure:"disk_size"`
|
||||
GuestAdditionsMode string `mapstructure:"guest_additions_mode"`
|
||||
GuestAdditionsPath string `mapstructure:"guest_additions_path"`
|
||||
GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"`
|
||||
GuestAdditionsURL string `mapstructure:"guest_additions_url"`
|
||||
GuestOSType string `mapstructure:"guest_os_type"`
|
||||
HardDriveDiscard bool `mapstructure:"hard_drive_discard"`
|
||||
HardDriveInterface string `mapstructure:"hard_drive_interface"`
|
||||
SATAPortCount int `mapstructure:"sata_port_count"`
|
||||
HardDriveNonrotational bool `mapstructure:"hard_drive_nonrotational"`
|
||||
ISOInterface string `mapstructure:"iso_interface"`
|
||||
KeepRegistered bool `mapstructure:"keep_registered"`
|
||||
SkipExport bool `mapstructure:"skip_export"`
|
||||
VMName string `mapstructure:"vm_name"`
|
||||
DiskSize uint `mapstructure:"disk_size"`
|
||||
GuestAdditionsMode string `mapstructure:"guest_additions_mode"`
|
||||
GuestAdditionsPath string `mapstructure:"guest_additions_path"`
|
||||
GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"`
|
||||
GuestAdditionsURL string `mapstructure:"guest_additions_url"`
|
||||
GuestAdditionsInterface string `mapstructure:"guest_additions_interface"`
|
||||
GuestOSType string `mapstructure:"guest_os_type"`
|
||||
HardDriveDiscard bool `mapstructure:"hard_drive_discard"`
|
||||
HardDriveInterface string `mapstructure:"hard_drive_interface"`
|
||||
SATAPortCount int `mapstructure:"sata_port_count"`
|
||||
HardDriveNonrotational bool `mapstructure:"hard_drive_nonrotational"`
|
||||
ISOInterface string `mapstructure:"iso_interface"`
|
||||
KeepRegistered bool `mapstructure:"keep_registered"`
|
||||
SkipExport bool `mapstructure:"skip_export"`
|
||||
VMName string `mapstructure:"vm_name"`
|
||||
|
||||
ctx interpolate.Context
|
||||
}
|
||||
@@ -125,6 +126,10 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
||||
b.config.ISOInterface = "ide"
|
||||
}
|
||||
|
||||
if b.config.GuestAdditionsInterface == "" {
|
||||
b.config.GuestAdditionsInterface = b.config.ISOInterface
|
||||
}
|
||||
|
||||
if b.config.VMName == "" {
|
||||
b.config.VMName = fmt.Sprintf(
|
||||
"packer-%s-%d", b.config.PackerBuildName, interpolate.InitTime.Unix())
|
||||
@@ -227,7 +232,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
new(stepCreateDisk),
|
||||
new(stepAttachISO),
|
||||
&vboxcommon.StepAttachGuestAdditions{
|
||||
GuestAdditionsMode: b.config.GuestAdditionsMode,
|
||||
GuestAdditionsMode: b.config.GuestAdditionsMode,
|
||||
GuestAdditionsInterface: b.config.GuestAdditionsInterface,
|
||||
},
|
||||
&vboxcommon.StepConfigureVRDP{
|
||||
VRDPBindAddress: b.config.VRDPBindAddress,
|
||||
@@ -280,7 +286,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
Delay: b.config.PostShutdownDelay,
|
||||
},
|
||||
&vboxcommon.StepRemoveDevices{
|
||||
Bundling: b.config.VBoxBundleConfig,
|
||||
Bundling: b.config.VBoxBundleConfig,
|
||||
GuestAdditionsInterface: b.config.GuestAdditionsInterface,
|
||||
},
|
||||
&vboxcommon.StepVBoxManage{
|
||||
Commands: b.config.VBoxManagePost,
|
||||
|
||||
@@ -84,7 +84,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
ImportFlags: b.config.ImportFlags,
|
||||
},
|
||||
&vboxcommon.StepAttachGuestAdditions{
|
||||
GuestAdditionsMode: b.config.GuestAdditionsMode,
|
||||
GuestAdditionsMode: b.config.GuestAdditionsMode,
|
||||
GuestAdditionsInterface: b.config.GuestAdditionsInterface,
|
||||
},
|
||||
&vboxcommon.StepConfigureVRDP{
|
||||
VRDPBindAddress: b.config.VRDPBindAddress,
|
||||
@@ -136,7 +137,9 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
Timeout: b.config.ShutdownTimeout,
|
||||
Delay: b.config.PostShutdownDelay,
|
||||
},
|
||||
new(vboxcommon.StepRemoveDevices),
|
||||
&vboxcommon.StepRemoveDevices{
|
||||
GuestAdditionsInterface: b.config.GuestAdditionsInterface,
|
||||
},
|
||||
&vboxcommon.StepVBoxManage{
|
||||
Commands: b.config.VBoxManagePost,
|
||||
Ctx: b.config.ctx,
|
||||
|
||||
@@ -28,19 +28,20 @@ type Config struct {
|
||||
vboxcommon.VBoxManagePostConfig `mapstructure:",squash"`
|
||||
vboxcommon.VBoxVersionConfig `mapstructure:",squash"`
|
||||
|
||||
Checksum string `mapstructure:"checksum"`
|
||||
ChecksumType string `mapstructure:"checksum_type"`
|
||||
GuestAdditionsMode string `mapstructure:"guest_additions_mode"`
|
||||
GuestAdditionsPath string `mapstructure:"guest_additions_path"`
|
||||
GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"`
|
||||
GuestAdditionsURL string `mapstructure:"guest_additions_url"`
|
||||
ImportFlags []string `mapstructure:"import_flags"`
|
||||
ImportOpts string `mapstructure:"import_opts"`
|
||||
SourcePath string `mapstructure:"source_path"`
|
||||
TargetPath string `mapstructure:"target_path"`
|
||||
VMName string `mapstructure:"vm_name"`
|
||||
KeepRegistered bool `mapstructure:"keep_registered"`
|
||||
SkipExport bool `mapstructure:"skip_export"`
|
||||
Checksum string `mapstructure:"checksum"`
|
||||
ChecksumType string `mapstructure:"checksum_type"`
|
||||
GuestAdditionsMode string `mapstructure:"guest_additions_mode"`
|
||||
GuestAdditionsPath string `mapstructure:"guest_additions_path"`
|
||||
GuestAdditionsInterface string `mapstructure:"guest_additions_interface"`
|
||||
GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"`
|
||||
GuestAdditionsURL string `mapstructure:"guest_additions_url"`
|
||||
ImportFlags []string `mapstructure:"import_flags"`
|
||||
ImportOpts string `mapstructure:"import_opts"`
|
||||
SourcePath string `mapstructure:"source_path"`
|
||||
TargetPath string `mapstructure:"target_path"`
|
||||
VMName string `mapstructure:"vm_name"`
|
||||
KeepRegistered bool `mapstructure:"keep_registered"`
|
||||
SkipExport bool `mapstructure:"skip_export"`
|
||||
|
||||
ctx interpolate.Context
|
||||
}
|
||||
@@ -72,6 +73,9 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
||||
if c.GuestAdditionsPath == "" {
|
||||
c.GuestAdditionsPath = "VBoxGuestAdditions.iso"
|
||||
}
|
||||
if c.GuestAdditionsInterface == "" {
|
||||
c.GuestAdditionsInterface = "ide"
|
||||
}
|
||||
|
||||
if c.VMName == "" {
|
||||
c.VMName = fmt.Sprintf(
|
||||
|
||||
@@ -69,7 +69,7 @@ func (c *DriverConfig) Validate(SkipExport bool) error {
|
||||
// now, so that we don't fail for a simple mistake after a long
|
||||
// build
|
||||
ovftool := GetOVFTool()
|
||||
ovfToolArgs := []string{"--verifyOnly", fmt.Sprintf("vi://%s:%s@%s",
|
||||
ovfToolArgs := []string{"--noSSLVerify", "--verifyOnly", fmt.Sprintf("vi://%s:%s@%s",
|
||||
url.QueryEscape(c.RemoteUser),
|
||||
url.QueryEscape(c.RemotePassword),
|
||||
c.RemoteHost)}
|
||||
|
||||
@@ -47,7 +47,7 @@ type ESX5Driver struct {
|
||||
|
||||
func (d *ESX5Driver) Clone(dst, src string, linked bool) error {
|
||||
|
||||
linesToArray := func(lines string) []string { return strings.Split(strings.Trim(lines, "\n"), "\n") }
|
||||
linesToArray := func(lines string) []string { return strings.Split(strings.Trim(lines, "\r\n"), "\n") }
|
||||
|
||||
d.SetOutputDir(path.Dir(filepath.ToSlash(dst)))
|
||||
srcVmx := d.datastorePath(src)
|
||||
|
||||
@@ -14,6 +14,7 @@ type HWConfig struct {
|
||||
// cpu information
|
||||
CpuCount int `mapstructure:"cpus"`
|
||||
MemorySize int `mapstructure:"memory"`
|
||||
CoreCount int `mapstructure:"cores"`
|
||||
|
||||
// network type and adapter
|
||||
Network string `mapstructure:"network"`
|
||||
@@ -40,6 +41,11 @@ func (c *HWConfig) Prepare(ctx *interpolate.Context) []error {
|
||||
errs = append(errs, fmt.Errorf("An invalid amount of memory was specified (memory < 0): %d", c.MemorySize))
|
||||
}
|
||||
|
||||
// Hardware and cpu options
|
||||
if c.CoreCount < 0 {
|
||||
errs = append(errs, fmt.Errorf("An invalid number of cores was specified (cores < 0): %d", c.CoreCount))
|
||||
}
|
||||
|
||||
// Peripherals
|
||||
if !c.Sound {
|
||||
c.Sound = false
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
func testHWConfig() *HWConfig {
|
||||
return &HWConfig{
|
||||
CpuCount: 1,
|
||||
CoreCount: 1,
|
||||
MemorySize: 512,
|
||||
|
||||
Sound: true,
|
||||
@@ -26,6 +27,10 @@ func TestHWConfigPrepare(t *testing.T) {
|
||||
t.Errorf("bad cpu count: %d", c.CpuCount)
|
||||
}
|
||||
|
||||
if c.CoreCount < 0 {
|
||||
t.Errorf("bad core count: %d", c.CoreCount)
|
||||
}
|
||||
|
||||
if c.MemorySize < 0 {
|
||||
t.Errorf("bad memory size: %d", c.MemorySize)
|
||||
}
|
||||
|
||||
@@ -423,12 +423,19 @@ func (s *stepCreateVMX) Run(_ context.Context, state multistep.StateBag) multist
|
||||
s.tempDir = vmxDir
|
||||
}
|
||||
|
||||
/// Now to handle options that will modify the template
|
||||
/// Now to handle options that will modify the template without using "vmxTemplateData"
|
||||
vmxData := vmwcommon.ParseVMX(vmxContents)
|
||||
|
||||
// If no cpus were specified, then remove the entry to use the default
|
||||
if vmxData["numvcpus"] == "" {
|
||||
delete(vmxData, "numvcpus")
|
||||
}
|
||||
|
||||
// If some number of cores were specified, then update "cpuid.coresPerSocket" with the requested value
|
||||
if config.HWConfig.CoreCount > 0 {
|
||||
vmxData["cpuid.corespersocket"] = strconv.Itoa(config.HWConfig.CoreCount)
|
||||
}
|
||||
|
||||
/// Write the vmxData to the vmxPath
|
||||
vmxPath := filepath.Join(vmxDir, config.VMName+".vmx")
|
||||
if err := vmwcommon.WriteVMX(vmxPath, vmxData); err != nil {
|
||||
|
||||
+39
-7
@@ -28,7 +28,7 @@ func TestBuildOnlyFileCommaFlags(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, f := range []string{"chocolate.txt", "vanilla.txt",
|
||||
"apple.txt", "peach.txt", "pear.txt"} {
|
||||
"apple.txt", "peach.txt", "pear.txt", "unnamed.txt"} {
|
||||
if !fileExists(f) {
|
||||
t.Errorf("Expected to find %s", f)
|
||||
}
|
||||
@@ -37,6 +37,10 @@ func TestBuildOnlyFileCommaFlags(t *testing.T) {
|
||||
if fileExists("cherry.txt") {
|
||||
t.Error("Expected NOT to find cherry.txt")
|
||||
}
|
||||
|
||||
if !fileExists("tomato.txt") {
|
||||
t.Error("Expected to find tomato.txt")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildStdin(t *testing.T) {
|
||||
@@ -58,7 +62,8 @@ func TestBuildStdin(t *testing.T) {
|
||||
fatalCommand(t, c.Meta)
|
||||
}
|
||||
|
||||
for _, f := range []string{"vanilla.txt", "cherry.txt", "chocolate.txt"} {
|
||||
for _, f := range []string{"vanilla.txt", "cherry.txt", "chocolate.txt",
|
||||
"unnamed.txt"} {
|
||||
if !fileExists(f) {
|
||||
t.Errorf("Expected to find %s", f)
|
||||
}
|
||||
@@ -85,13 +90,37 @@ func TestBuildOnlyFileMultipleFlags(t *testing.T) {
|
||||
fatalCommand(t, c.Meta)
|
||||
}
|
||||
|
||||
for _, f := range []string{"vanilla.txt"} {
|
||||
for _, f := range []string{"vanilla.txt", "tomato.txt"} {
|
||||
if fileExists(f) {
|
||||
t.Errorf("Expected NOT to find %s", f)
|
||||
}
|
||||
}
|
||||
for _, f := range []string{"chocolate.txt", "cherry.txt",
|
||||
"apple.txt", "peach.txt", "pear.txt"} {
|
||||
"apple.txt", "peach.txt", "pear.txt", "unnamed.txt"} {
|
||||
if !fileExists(f) {
|
||||
t.Errorf("Expected to find %s", f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildEverything(t *testing.T) {
|
||||
c := &BuildCommand{
|
||||
Meta: testMetaFile(t),
|
||||
}
|
||||
|
||||
args := []string{
|
||||
`-except=`,
|
||||
filepath.Join(testFixture("build-only"), "template.json"),
|
||||
}
|
||||
|
||||
defer cleanup()
|
||||
|
||||
if code := c.Run(args); code != 0 {
|
||||
fatalCommand(t, c.Meta)
|
||||
}
|
||||
|
||||
for _, f := range []string{"chocolate.txt", "vanilla.txt", "tomato.txt",
|
||||
"apple.txt", "cherry.txt", "pear.txt", "peach.txt", "unnamed.txt"} {
|
||||
if !fileExists(f) {
|
||||
t.Errorf("Expected to find %s", f)
|
||||
}
|
||||
@@ -104,7 +133,7 @@ func TestBuildExceptFileCommaFlags(t *testing.T) {
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"-except=chocolate,apple",
|
||||
"-except=chocolate,vanilla",
|
||||
filepath.Join(testFixture("build-only"), "template.json"),
|
||||
}
|
||||
|
||||
@@ -114,12 +143,13 @@ func TestBuildExceptFileCommaFlags(t *testing.T) {
|
||||
fatalCommand(t, c.Meta)
|
||||
}
|
||||
|
||||
for _, f := range []string{"chocolate.txt", "apple.txt", "peach.txt"} {
|
||||
for _, f := range []string{"chocolate.txt", "vanilla.txt", "tomato.txt",
|
||||
"unnamed.txt"} {
|
||||
if fileExists(f) {
|
||||
t.Errorf("Expected NOT to find %s", f)
|
||||
}
|
||||
}
|
||||
for _, f := range []string{"vanilla.txt", "cherry.txt", "pear.txt"} {
|
||||
for _, f := range []string{"apple.txt", "cherry.txt", "pear.txt", "peach.txt"} {
|
||||
if !fileExists(f) {
|
||||
t.Errorf("Expected to find %s", f)
|
||||
}
|
||||
@@ -169,4 +199,6 @@ func cleanup() {
|
||||
os.RemoveAll("apple.txt")
|
||||
os.RemoveAll("peach.txt")
|
||||
os.RemoveAll("pear.txt")
|
||||
os.RemoveAll("tomato.txt")
|
||||
os.RemoveAll("unnamed.txt")
|
||||
}
|
||||
|
||||
+10
-2
@@ -26,6 +26,7 @@ import (
|
||||
filebuilder "github.com/hashicorp/packer/builder/file"
|
||||
googlecomputebuilder "github.com/hashicorp/packer/builder/googlecompute"
|
||||
hcloudbuilder "github.com/hashicorp/packer/builder/hcloud"
|
||||
hyperonebuilder "github.com/hashicorp/packer/builder/hyperone"
|
||||
hypervisobuilder "github.com/hashicorp/packer/builder/hyperv/iso"
|
||||
hypervvmcxbuilder "github.com/hashicorp/packer/builder/hyperv/vmcx"
|
||||
lxcbuilder "github.com/hashicorp/packer/builder/lxc"
|
||||
@@ -41,8 +42,9 @@ import (
|
||||
profitbricksbuilder "github.com/hashicorp/packer/builder/profitbricks"
|
||||
qemubuilder "github.com/hashicorp/packer/builder/qemu"
|
||||
scalewaybuilder "github.com/hashicorp/packer/builder/scaleway"
|
||||
tencentcloudbuilder "github.com/hashicorp/packer/builder/tencentcloud/cvm"
|
||||
tencentcloudcvmbuilder "github.com/hashicorp/packer/builder/tencentcloud/cvm"
|
||||
tritonbuilder "github.com/hashicorp/packer/builder/triton"
|
||||
vagrantbuilder "github.com/hashicorp/packer/builder/vagrant"
|
||||
virtualboxisobuilder "github.com/hashicorp/packer/builder/virtualbox/iso"
|
||||
virtualboxovfbuilder "github.com/hashicorp/packer/builder/virtualbox/ovf"
|
||||
vmwareisobuilder "github.com/hashicorp/packer/builder/vmware/iso"
|
||||
@@ -52,6 +54,7 @@ import (
|
||||
artificepostprocessor "github.com/hashicorp/packer/post-processor/artifice"
|
||||
checksumpostprocessor "github.com/hashicorp/packer/post-processor/checksum"
|
||||
compresspostprocessor "github.com/hashicorp/packer/post-processor/compress"
|
||||
digitaloceanimportpostprocessor "github.com/hashicorp/packer/post-processor/digitalocean-import"
|
||||
dockerimportpostprocessor "github.com/hashicorp/packer/post-processor/docker-import"
|
||||
dockerpushpostprocessor "github.com/hashicorp/packer/post-processor/docker-push"
|
||||
dockersavepostprocessor "github.com/hashicorp/packer/post-processor/docker-save"
|
||||
@@ -71,6 +74,7 @@ import (
|
||||
chefsoloprovisioner "github.com/hashicorp/packer/provisioner/chef-solo"
|
||||
convergeprovisioner "github.com/hashicorp/packer/provisioner/converge"
|
||||
fileprovisioner "github.com/hashicorp/packer/provisioner/file"
|
||||
inspecprovisioner "github.com/hashicorp/packer/provisioner/inspec"
|
||||
powershellprovisioner "github.com/hashicorp/packer/provisioner/powershell"
|
||||
puppetmasterlessprovisioner "github.com/hashicorp/packer/provisioner/puppet-masterless"
|
||||
puppetserverprovisioner "github.com/hashicorp/packer/provisioner/puppet-server"
|
||||
@@ -99,6 +103,7 @@ var Builders = map[string]packer.Builder{
|
||||
"file": new(filebuilder.Builder),
|
||||
"googlecompute": new(googlecomputebuilder.Builder),
|
||||
"hcloud": new(hcloudbuilder.Builder),
|
||||
"hyperone": new(hyperonebuilder.Builder),
|
||||
"hyperv-iso": new(hypervisobuilder.Builder),
|
||||
"hyperv-vmcx": new(hypervvmcxbuilder.Builder),
|
||||
"lxc": new(lxcbuilder.Builder),
|
||||
@@ -114,8 +119,9 @@ var Builders = map[string]packer.Builder{
|
||||
"profitbricks": new(profitbricksbuilder.Builder),
|
||||
"qemu": new(qemubuilder.Builder),
|
||||
"scaleway": new(scalewaybuilder.Builder),
|
||||
"tencentcloud-cvm": new(tencentcloudbuilder.Builder),
|
||||
"tencentcloud-cvm": new(tencentcloudcvmbuilder.Builder),
|
||||
"triton": new(tritonbuilder.Builder),
|
||||
"vagrant": new(vagrantbuilder.Builder),
|
||||
"virtualbox-iso": new(virtualboxisobuilder.Builder),
|
||||
"virtualbox-ovf": new(virtualboxovfbuilder.Builder),
|
||||
"vmware-iso": new(vmwareisobuilder.Builder),
|
||||
@@ -130,6 +136,7 @@ var Provisioners = map[string]packer.Provisioner{
|
||||
"chef-solo": new(chefsoloprovisioner.Provisioner),
|
||||
"converge": new(convergeprovisioner.Provisioner),
|
||||
"file": new(fileprovisioner.Provisioner),
|
||||
"inspec": new(inspecprovisioner.Provisioner),
|
||||
"powershell": new(powershellprovisioner.Provisioner),
|
||||
"puppet-masterless": new(puppetmasterlessprovisioner.Provisioner),
|
||||
"puppet-server": new(puppetserverprovisioner.Provisioner),
|
||||
@@ -146,6 +153,7 @@ var PostProcessors = map[string]packer.PostProcessor{
|
||||
"artifice": new(artificepostprocessor.PostProcessor),
|
||||
"checksum": new(checksumpostprocessor.PostProcessor),
|
||||
"compress": new(compresspostprocessor.PostProcessor),
|
||||
"digitalocean-import": new(digitaloceanimportpostprocessor.PostProcessor),
|
||||
"docker-import": new(dockerimportpostprocessor.PostProcessor),
|
||||
"docker-push": new(dockerpushpostprocessor.PostProcessor),
|
||||
"docker-save": new(dockersavepostprocessor.PostProcessor),
|
||||
|
||||
@@ -38,6 +38,25 @@
|
||||
"type": "shell-local",
|
||||
"inline": [ "touch pear.txt" ]
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"only": [
|
||||
"vanilla"
|
||||
],
|
||||
"name": "tomato",
|
||||
"type": "shell-local",
|
||||
"inline": [ "touch tomato.txt" ]
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"only": [
|
||||
"chocolate"
|
||||
],
|
||||
"type": "shell-local",
|
||||
"inline": [ "touch unnamed.txt" ]
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package ansible
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
|
||||
// An adapter satisfies SSH requests (from an Ansible client) by delegating SSH
|
||||
// exec and subsystem commands to a packer.Communicator.
|
||||
type adapter struct {
|
||||
type Adapter struct {
|
||||
done <-chan struct{}
|
||||
l net.Listener
|
||||
config *ssh.ServerConfig
|
||||
@@ -26,8 +26,8 @@ type adapter struct {
|
||||
comm packer.Communicator
|
||||
}
|
||||
|
||||
func newAdapter(done <-chan struct{}, l net.Listener, config *ssh.ServerConfig, sftpCmd string, ui packer.Ui, comm packer.Communicator) *adapter {
|
||||
return &adapter{
|
||||
func NewAdapter(done <-chan struct{}, l net.Listener, config *ssh.ServerConfig, sftpCmd string, ui packer.Ui, comm packer.Communicator) *Adapter {
|
||||
return &Adapter{
|
||||
done: done,
|
||||
l: l,
|
||||
config: config,
|
||||
@@ -37,7 +37,7 @@ func newAdapter(done <-chan struct{}, l net.Listener, config *ssh.ServerConfig,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *adapter) Serve() {
|
||||
func (c *Adapter) Serve() {
|
||||
log.Printf("SSH proxy: serving on %s", c.l.Addr())
|
||||
|
||||
for {
|
||||
@@ -62,7 +62,7 @@ func (c *adapter) Serve() {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *adapter) Handle(conn net.Conn, ui packer.Ui) error {
|
||||
func (c *Adapter) Handle(conn net.Conn, ui packer.Ui) error {
|
||||
log.Print("SSH proxy: accepted connection")
|
||||
_, chans, reqs, err := ssh.NewServerConn(conn, c.config)
|
||||
if err != nil {
|
||||
@@ -89,7 +89,7 @@ func (c *adapter) Handle(conn net.Conn, ui packer.Ui) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *adapter) handleSession(newChannel ssh.NewChannel) error {
|
||||
func (c *Adapter) handleSession(newChannel ssh.NewChannel) error {
|
||||
channel, requests, err := newChannel.Accept()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -182,11 +182,11 @@ func (c *adapter) handleSession(newChannel ssh.NewChannel) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *adapter) Shutdown() {
|
||||
func (c *Adapter) Shutdown() {
|
||||
c.l.Close()
|
||||
}
|
||||
|
||||
func (c *adapter) exec(command string, in io.Reader, out io.Writer, err io.Writer) int {
|
||||
func (c *Adapter) exec(command string, in io.Reader, out io.Writer, err io.Writer) int {
|
||||
var exitStatus int
|
||||
switch {
|
||||
case strings.HasPrefix(command, "scp ") && serveSCP(command[4:]):
|
||||
@@ -206,7 +206,7 @@ func serveSCP(args string) bool {
|
||||
return bytes.IndexAny(opts, "tf") >= 0
|
||||
}
|
||||
|
||||
func (c *adapter) scpExec(args string, in io.Reader, out io.Writer) error {
|
||||
func (c *Adapter) scpExec(args string, in io.Reader, out io.Writer) error {
|
||||
opts, rest := scpOptions(args)
|
||||
|
||||
// remove the quoting that ansible added to rest for shell safety.
|
||||
@@ -226,7 +226,7 @@ func (c *adapter) scpExec(args string, in io.Reader, out io.Writer) error {
|
||||
return errors.New("no scp mode specified")
|
||||
}
|
||||
|
||||
func (c *adapter) remoteExec(command string, in io.Reader, out io.Writer, err io.Writer) int {
|
||||
func (c *Adapter) remoteExec(command string, in io.Reader, out io.Writer, err io.Writer) int {
|
||||
cmd := &packer.RemoteCmd{
|
||||
Stdin: in,
|
||||
Stdout: out,
|
||||
@@ -1,4 +1,4 @@
|
||||
package ansible
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -26,7 +26,7 @@ func TestAdapter_Serve(t *testing.T) {
|
||||
|
||||
ui := new(packer.NoopUi)
|
||||
|
||||
sut := newAdapter(done, &l, config, "", newUi(ui), communicator{})
|
||||
sut := NewAdapter(done, &l, config, "", ui, communicator{})
|
||||
go func() {
|
||||
i := 0
|
||||
for range acceptC {
|
||||
@@ -1,4 +1,4 @@
|
||||
package ansible
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -1190,7 +1190,6 @@ func newParser(filename string, b []byte, opts ...Option) *parser {
|
||||
Stats: &stats,
|
||||
// start rule is rule [0] unless an alternate entrypoint is specified
|
||||
entrypoint: g.rules[0].name,
|
||||
emptyState: make(storeDict),
|
||||
}
|
||||
p.setOptions(opts)
|
||||
|
||||
@@ -1279,9 +1278,6 @@ type parser struct {
|
||||
choiceNoMatch string
|
||||
// recovery expression stack, keeps track of the currently available recovery expression, these are traversed in reverse
|
||||
recoveryStack []map[string]interface{}
|
||||
|
||||
// emptyState contains an empty storeDict, which is used to optimize cloneState if global "state" store is not used.
|
||||
emptyState storeDict
|
||||
}
|
||||
|
||||
// push a variable set on the vstack.
|
||||
@@ -1455,13 +1451,6 @@ func (p *parser) cloneState() storeDict {
|
||||
defer p.out(p.in("cloneState"))
|
||||
}
|
||||
|
||||
if len(p.cur.state) == 0 {
|
||||
if len(p.emptyState) > 0 {
|
||||
p.emptyState = make(storeDict)
|
||||
}
|
||||
return p.emptyState
|
||||
}
|
||||
|
||||
state := make(storeDict, len(p.cur.state))
|
||||
for k, v := range p.cur.state {
|
||||
if c, ok := v.(Cloner); ok {
|
||||
|
||||
@@ -58,6 +58,11 @@ func (c *ISOConfig) Prepare(ctx *interpolate.Context) (warnings []string, errs [
|
||||
|
||||
// If iso_checksum has no value use iso_checksum_url instead.
|
||||
if c.ISOChecksum == "" {
|
||||
if strings.HasSuffix(strings.ToLower(c.ISOChecksumURL), ".iso") {
|
||||
errs = append(errs, fmt.Errorf("Error parsing checksum:"+
|
||||
" .iso is not a valid checksum extension"))
|
||||
return warnings, errs
|
||||
}
|
||||
u, err := url.Parse(c.ISOChecksumURL)
|
||||
if err != nil {
|
||||
errs = append(errs,
|
||||
|
||||
@@ -239,6 +239,16 @@ func TestISOConfigPrepare_ISOChecksumURL(t *testing.T) {
|
||||
if i.ISOChecksum != "bar0" {
|
||||
t.Fatalf("should've found \"bar0\" got: %s", i.ISOChecksum)
|
||||
}
|
||||
|
||||
// Test that we won't try to read an iso into memory because of a user
|
||||
// error
|
||||
i = testISOConfig()
|
||||
i.ISOChecksumURL = "file:///not_read.iso"
|
||||
i.ISOChecksum = ""
|
||||
warns, err = i.Prepare(nil)
|
||||
if err == nil {
|
||||
t.Fatalf("should have error because iso is bad filetype: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestISOConfigPrepare_ISOChecksumType(t *testing.T) {
|
||||
|
||||
@@ -242,13 +242,13 @@ func getCreateVMScript(opts *scriptOptions) (string, error) {
|
||||
}
|
||||
|
||||
var tpl = template.Must(template.New("createVM").Parse(`
|
||||
$vhdPath = Join-Path -Path {{ .Path }} -ChildPath {{ .VHDX }}
|
||||
$vhdPath = Join-Path -Path "{{ .Path }}" -ChildPath "{{ .VHDX }}"
|
||||
|
||||
{{ if ne .HardDrivePath "" -}}
|
||||
{{- if .DiffDisks -}}
|
||||
Hyper-V\New-VHD -Path $vhdPath -ParentPath {{ .HardDrivePath }} -Differencing -BlockSizeBytes {{ .VHDBlockSizeBytes }}
|
||||
Hyper-V\New-VHD -Path $vhdPath -ParentPath "{{ .HardDrivePath }}" -Differencing -BlockSizeBytes {{ .VHDBlockSizeBytes }}
|
||||
{{- else -}}
|
||||
Copy-Item -Path {{ .HardDrivePath }} -Destination $vhdPath
|
||||
Copy-Item -Path "{{ .HardDrivePath }}" -Destination $vhdPath
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
{{- if .FixedVHD -}}
|
||||
@@ -258,7 +258,7 @@ $vhdPath = Join-Path -Path {{ .Path }} -ChildPath {{ .VHDX }}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
Hyper-V\New-VM -Name {{ .VMName }} -Path {{ .Path }} -MemoryStartupBytes {{ .MemoryStartupBytes }} -VHDPath $vhdPath -SwitchName {{ .SwitchName }}
|
||||
Hyper-V\New-VM -Name "{{ .VMName }}" -Path "{{ .Path }}" -MemoryStartupBytes {{ .MemoryStartupBytes }} -VHDPath $vhdPath -SwitchName "{{ .SwitchName }}"
|
||||
{{- if eq .Generation 2}} -Generation {{ .Generation }} {{- end -}}
|
||||
{{- if ne .Version ""}} -Version {{ .Version }} {{- end -}}
|
||||
`))
|
||||
|
||||
@@ -26,9 +26,9 @@ func Test_getCreateVMScript(t *testing.T) {
|
||||
t.Fatalf("Error: %s", err.Error())
|
||||
}
|
||||
|
||||
expected := `$vhdPath = Join-Path -Path C://mypath -ChildPath myvm.vhd
|
||||
Hyper-V\New-VHD -Path $vhdPath -ParentPath C://harddrivepath -Differencing -BlockSizeBytes 10
|
||||
Hyper-V\New-VM -Name myvm -Path C://mypath -MemoryStartupBytes 1024 -VHDPath $vhdPath -SwitchName hyperv-vmx-switch -Version 5.0`
|
||||
expected := `$vhdPath = Join-Path -Path "C://mypath" -ChildPath "myvm.vhd"
|
||||
Hyper-V\New-VHD -Path $vhdPath -ParentPath "C://harddrivepath" -Differencing -BlockSizeBytes 10
|
||||
Hyper-V\New-VM -Name "myvm" -Path "C://mypath" -MemoryStartupBytes 1024 -VHDPath $vhdPath -SwitchName "hyperv-vmx-switch" -Version 5.0`
|
||||
if ok := strings.Compare(scriptString, expected); ok != 0 {
|
||||
t.Fatalf("EXPECTED: \n%s\n\n RECEIVED: \n%s\n\n", expected, scriptString)
|
||||
}
|
||||
@@ -48,9 +48,9 @@ Hyper-V\New-VM -Name myvm -Path C://mypath -MemoryStartupBytes 1024 -VHDPath $vh
|
||||
t.Fatalf("Error: %s", err.Error())
|
||||
}
|
||||
|
||||
expected = `$vhdPath = Join-Path -Path C://mypath -ChildPath myvm.vhdx
|
||||
Hyper-V\New-VHD -Path $vhdPath -ParentPath C://harddrivepath -Differencing -BlockSizeBytes 10
|
||||
Hyper-V\New-VM -Name myvm -Path C://mypath -MemoryStartupBytes 1024 -VHDPath $vhdPath -SwitchName hyperv-vmx-switch -Generation 2 -Version 5.0`
|
||||
expected = `$vhdPath = Join-Path -Path "C://mypath" -ChildPath "myvm.vhdx"
|
||||
Hyper-V\New-VHD -Path $vhdPath -ParentPath "C://harddrivepath" -Differencing -BlockSizeBytes 10
|
||||
Hyper-V\New-VM -Name "myvm" -Path "C://mypath" -MemoryStartupBytes 1024 -VHDPath $vhdPath -SwitchName "hyperv-vmx-switch" -Generation 2 -Version 5.0`
|
||||
if ok := strings.Compare(scriptString, expected); ok != 0 {
|
||||
t.Fatalf("EXPECTED: \n%s\n\n RECEIVED: \n%s\n\n", expected, scriptString)
|
||||
}
|
||||
@@ -63,9 +63,9 @@ Hyper-V\New-VM -Name myvm -Path C://mypath -MemoryStartupBytes 1024 -VHDPath $vh
|
||||
t.Fatalf("Error: %s", err.Error())
|
||||
}
|
||||
|
||||
expected = `$vhdPath = Join-Path -Path C://mypath -ChildPath myvm.vhdx
|
||||
Hyper-V\New-VHD -Path $vhdPath -ParentPath C://harddrivepath -Differencing -BlockSizeBytes 10
|
||||
Hyper-V\New-VM -Name myvm -Path C://mypath -MemoryStartupBytes 1024 -VHDPath $vhdPath -SwitchName hyperv-vmx-switch -Version 5.0`
|
||||
expected = `$vhdPath = Join-Path -Path "C://mypath" -ChildPath "myvm.vhdx"
|
||||
Hyper-V\New-VHD -Path $vhdPath -ParentPath "C://harddrivepath" -Differencing -BlockSizeBytes 10
|
||||
Hyper-V\New-VM -Name "myvm" -Path "C://mypath" -MemoryStartupBytes 1024 -VHDPath $vhdPath -SwitchName "hyperv-vmx-switch" -Version 5.0`
|
||||
if ok := strings.Compare(scriptString, expected); ok != 0 {
|
||||
t.Fatalf("EXPECTED: \n%s\n\n RECEIVED: \n%s\n\n", expected, scriptString)
|
||||
}
|
||||
@@ -86,9 +86,9 @@ Hyper-V\New-VM -Name myvm -Path C://mypath -MemoryStartupBytes 1024 -VHDPath $vh
|
||||
if err != nil {
|
||||
t.Fatalf("Error: %s", err.Error())
|
||||
}
|
||||
expected = `$vhdPath = Join-Path -Path C://mypath -ChildPath myvm.vhdx
|
||||
Hyper-V\New-VHD -Path $vhdPath -ParentPath C://harddrivepath -Differencing -BlockSizeBytes 10
|
||||
Hyper-V\New-VM -Name myvm -Path C://mypath -MemoryStartupBytes 1024 -VHDPath $vhdPath -SwitchName hyperv-vmx-switch`
|
||||
expected = `$vhdPath = Join-Path -Path "C://mypath" -ChildPath "myvm.vhdx"
|
||||
Hyper-V\New-VHD -Path $vhdPath -ParentPath "C://harddrivepath" -Differencing -BlockSizeBytes 10
|
||||
Hyper-V\New-VM -Name "myvm" -Path "C://mypath" -MemoryStartupBytes 1024 -VHDPath $vhdPath -SwitchName "hyperv-vmx-switch"`
|
||||
if ok := strings.Compare(scriptString, expected); ok != 0 {
|
||||
t.Fatalf("EXPECTED: \n%s\n\n RECEIVED: \n%s\n\n", expected, scriptString)
|
||||
}
|
||||
@@ -98,9 +98,9 @@ Hyper-V\New-VM -Name myvm -Path C://mypath -MemoryStartupBytes 1024 -VHDPath $vh
|
||||
if err != nil {
|
||||
t.Fatalf("Error: %s", err.Error())
|
||||
}
|
||||
expected = `$vhdPath = Join-Path -Path C://mypath -ChildPath myvm.vhdx
|
||||
Copy-Item -Path C://harddrivepath -Destination $vhdPath
|
||||
Hyper-V\New-VM -Name myvm -Path C://mypath -MemoryStartupBytes 1024 -VHDPath $vhdPath -SwitchName hyperv-vmx-switch`
|
||||
expected = `$vhdPath = Join-Path -Path "C://mypath" -ChildPath "myvm.vhdx"
|
||||
Copy-Item -Path "C://harddrivepath" -Destination $vhdPath
|
||||
Hyper-V\New-VM -Name "myvm" -Path "C://mypath" -MemoryStartupBytes 1024 -VHDPath $vhdPath -SwitchName "hyperv-vmx-switch"`
|
||||
if ok := strings.Compare(scriptString, expected); ok != 0 {
|
||||
t.Fatalf("EXPECTED: \n%s\n\n RECEIVED: \n%s\n\n", expected, scriptString)
|
||||
}
|
||||
@@ -110,9 +110,9 @@ Hyper-V\New-VM -Name myvm -Path C://mypath -MemoryStartupBytes 1024 -VHDPath $vh
|
||||
if err != nil {
|
||||
t.Fatalf("Error: %s", err.Error())
|
||||
}
|
||||
expected = `$vhdPath = Join-Path -Path C://mypath -ChildPath myvm.vhdx
|
||||
expected = `$vhdPath = Join-Path -Path "C://mypath" -ChildPath "myvm.vhdx"
|
||||
Hyper-V\New-VHD -Path $vhdPath -SizeBytes 8192 -BlockSizeBytes 10
|
||||
Hyper-V\New-VM -Name myvm -Path C://mypath -MemoryStartupBytes 1024 -VHDPath $vhdPath -SwitchName hyperv-vmx-switch`
|
||||
Hyper-V\New-VM -Name "myvm" -Path "C://mypath" -MemoryStartupBytes 1024 -VHDPath $vhdPath -SwitchName "hyperv-vmx-switch"`
|
||||
if ok := strings.Compare(scriptString, expected); ok != 0 {
|
||||
t.Fatalf("EXPECTED: \n%s\n\n RECEIVED: \n%s\n\n", expected, scriptString)
|
||||
}
|
||||
@@ -122,9 +122,9 @@ Hyper-V\New-VM -Name myvm -Path C://mypath -MemoryStartupBytes 1024 -VHDPath $vh
|
||||
if err != nil {
|
||||
t.Fatalf("Error: %s", err.Error())
|
||||
}
|
||||
expected = `$vhdPath = Join-Path -Path C://mypath -ChildPath myvm.vhd
|
||||
expected = `$vhdPath = Join-Path -Path "C://mypath" -ChildPath "myvm.vhd"
|
||||
Hyper-V\New-VHD -Path $vhdPath -Fixed -SizeBytes 8192
|
||||
Hyper-V\New-VM -Name myvm -Path C://mypath -MemoryStartupBytes 1024 -VHDPath $vhdPath -SwitchName hyperv-vmx-switch`
|
||||
Hyper-V\New-VM -Name "myvm" -Path "C://mypath" -MemoryStartupBytes 1024 -VHDPath $vhdPath -SwitchName "hyperv-vmx-switch"`
|
||||
if ok := strings.Compare(scriptString, expected); ok != 0 {
|
||||
t.Fatalf("EXPECTED: \n%s\n\n RECEIVED: \n%s\n\n", expected, scriptString)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"variables": {
|
||||
"token": "{{ env `HYPERONE_TOKEN` }}",
|
||||
"project": "{{ env `HYPERONE_PROJECT` }}"
|
||||
},
|
||||
"builders": [
|
||||
{
|
||||
"token": "{{ user `token` }}",
|
||||
"project": "{{ user `project` }}",
|
||||
"type": "hyperone",
|
||||
"source_image": "ubuntu",
|
||||
"disk_size": 10,
|
||||
"vm_type": "a1.nano"
|
||||
}
|
||||
],
|
||||
"provisioners": [
|
||||
{
|
||||
"type": "shell",
|
||||
"inline": [
|
||||
"apt-get update",
|
||||
"apt-get upgrade -y"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variables": {
|
||||
"token": "{{ env `HYPERONE_TOKEN` }}",
|
||||
"project": "{{ env `HYPERONE_PROJECT` }}"
|
||||
},
|
||||
"builders": [
|
||||
{
|
||||
"token": "{{ user `token` }}",
|
||||
"project": "{{ user `project` }}",
|
||||
"type": "hyperone",
|
||||
"source_image": "ubuntu",
|
||||
"disk_size": 10,
|
||||
"vm_type": "a1.nano",
|
||||
|
||||
"chroot_disk": true,
|
||||
"chroot_command_wrapper": "sudo {{.Command}}",
|
||||
"pre_mount_commands": [
|
||||
"parted {{.Device}} mklabel msdos mkpart primary 1M 100% set 1 boot on print",
|
||||
"mkfs.ext4 {{.Device}}1"
|
||||
],
|
||||
"post_mount_commands": [
|
||||
"apt-get update",
|
||||
"apt-get install debootstrap",
|
||||
"debootstrap --arch amd64 bionic {{.MountPath}}"
|
||||
]
|
||||
}
|
||||
],
|
||||
"provisioners": []
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package fix
|
||||
|
||||
import (
|
||||
"log"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -53,8 +53,7 @@ func (FixerAmazonPrivateIP) Fix(input map[string]interface{}) (map[string]interf
|
||||
var err error
|
||||
privateIP, err = strconv.ParseBool(privateIPi.(string))
|
||||
if err != nil {
|
||||
log.Fatalf("Wrong type for ssh_private_ip")
|
||||
continue
|
||||
return nil, fmt.Errorf("ssh_private_ip is not a boolean, %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,3 +75,19 @@ func TestFixerAmazonPrivateIP(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFixerAmazonPrivateIPNonBoolean(t *testing.T) {
|
||||
var f FixerAmazonPrivateIP
|
||||
|
||||
input := map[string]interface{}{
|
||||
"builders": []map[string]interface{}{{
|
||||
"type": "amazon-ebs",
|
||||
"ssh_private_ip": "not-a-boolean-value",
|
||||
}},
|
||||
}
|
||||
|
||||
_, err := f.Fix(input)
|
||||
if err == nil {
|
||||
t.Fatal("should have errored")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
|
||||
github.com/Azure/go-autorest v10.12.0+incompatible
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20180810175552-4a21cbd618b4 // indirect
|
||||
github.com/Bowery/prompt v0.0.0-20180817134258-8a1d5376df1c // indirect
|
||||
github.com/ChrisTrenkamp/goxpath v0.0.0-20170625215350-4fe035839290
|
||||
github.com/DataDog/datadog-go v0.0.0-20180822151419-281ae9f2d895 // indirect
|
||||
github.com/Jeffail/gabs v1.1.1 // indirect
|
||||
@@ -19,6 +20,7 @@ require (
|
||||
github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20170113022742-e6dbea820a9f
|
||||
github.com/antchfx/xpath v0.0.0-20170728053731-b5c552e1acbd // indirect
|
||||
github.com/antchfx/xquery v0.0.0-20170730121040-eb8c3c172607 // indirect
|
||||
github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6 // indirect
|
||||
github.com/approvals/go-approval-tests v0.0.0-20160714161514-ad96e53bea43
|
||||
github.com/armon/go-metrics v0.0.0-20180713145231-3c58d8115a78 // indirect
|
||||
github.com/armon/go-radix v0.0.0-20160115234725-4239b77079c7 // indirect
|
||||
@@ -34,10 +36,11 @@ require (
|
||||
github.com/circonus-labs/circonusllhist v0.1.3 // indirect
|
||||
github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 // indirect
|
||||
github.com/creack/goselect v0.0.0-20180210034346-528c74964609 // indirect
|
||||
github.com/dchest/safefile v0.0.0-20151022103144-855e8d98f185 // indirect
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f // indirect
|
||||
github.com/denverdino/aliyungo v0.0.0-20180417075537-ebad04655e03
|
||||
github.com/denverdino/aliyungo v0.0.0-20190220033614-36e2ae938978
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/digitalocean/godo v0.0.0-20170407151542-4c04abe183f4
|
||||
github.com/digitalocean/godo v1.7.3
|
||||
github.com/dnaeon/go-vcr v1.0.0 // indirect
|
||||
github.com/docker/docker v0.0.0-20170406124027-fa3e2d5ab9b5 // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
@@ -90,9 +93,11 @@ require (
|
||||
github.com/hashicorp/vault-plugin-secrets-kv v0.0.0-20181106190520-2236f141171e // indirect
|
||||
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb
|
||||
github.com/hetznercloud/hcloud-go v1.12.0
|
||||
github.com/hyperonecom/h1-client-go v0.0.0-20190122232013-cf38e8387775
|
||||
github.com/jefferai/jsonx v0.0.0-20160721235117-9cc31c3135ee // indirect
|
||||
github.com/joyent/triton-go v0.0.0-20180116165742-545edbe0d564
|
||||
github.com/jtolds/gls v4.2.1+incompatible // indirect
|
||||
github.com/kardianos/govendor v1.0.9 // indirect
|
||||
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1
|
||||
github.com/keybase/go-crypto v0.0.0-20181127160227-255a5089e85a // indirect
|
||||
github.com/klauspost/compress v0.0.0-20160131094358-f86d2e6d8a77 // indirect
|
||||
@@ -114,7 +119,7 @@ require (
|
||||
github.com/mitchellh/cli v0.0.0-20170908181043-65fcae5817c8
|
||||
github.com/mitchellh/copystructure v1.0.0 // indirect
|
||||
github.com/mitchellh/go-fs v0.0.0-20180402234041-7b48fa161ea7
|
||||
github.com/mitchellh/go-homedir v0.0.0-20151025052427-d682a8f0cf13 // indirect
|
||||
github.com/mitchellh/go-homedir v0.0.0-20151025052427-d682a8f0cf13
|
||||
github.com/mitchellh/go-testing-interface v1.0.0 // indirect
|
||||
github.com/mitchellh/go-vnc v0.0.0-20150629162542-723ed9867aed
|
||||
github.com/mitchellh/iochan v0.0.0-20150529224432-87b45ffd0e95
|
||||
@@ -122,6 +127,7 @@ require (
|
||||
github.com/mitchellh/panicwrap v0.0.0-20170106182340-fce601fe5557
|
||||
github.com/mitchellh/prefixedio v0.0.0-20151214002211-6e6954073784
|
||||
github.com/mitchellh/reflectwalk v1.0.0
|
||||
github.com/mna/pigeon v1.0.0 // indirect
|
||||
github.com/moul/anonuuid v0.0.0-20160222162117-609b752a95ef // indirect
|
||||
github.com/moul/gotty-client v0.0.0-20180327180212-b26a57ebc215 // indirect
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 // indirect
|
||||
@@ -160,13 +166,14 @@ require (
|
||||
github.com/ugorji/go v0.0.0-20151218193438-646ae4a518c1
|
||||
github.com/ulikunitz/xz v0.0.0-20180703112113-636d36a76670
|
||||
github.com/vmware/govmomi v0.0.0-20170707011325-c2105a174311
|
||||
github.com/xanzy/go-cloudstack v2.1.4+incompatible
|
||||
github.com/xanzy/go-cloudstack v2.4.1+incompatible
|
||||
golang.org/x/crypto v0.0.0-20180322175230-88942b9c40a4
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 // indirect
|
||||
golang.org/x/tools v0.0.0-20190221204921-83362c3779f5 // indirect
|
||||
google.golang.org/api v0.0.0-20180818000503-e21acd801f91
|
||||
google.golang.org/grpc v1.17.0 // indirect
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9 // indirect
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/1and1/oneandone-cloudserver-sdk-go v1.0.1 h1:RMTyvS5bjvSWiUcfqfr/E2pxHEMrALvU+E12n6biymg=
|
||||
github.com/1and1/oneandone-cloudserver-sdk-go v1.0.1/go.mod h1:61apmbkVJH4kg+38ftT+/l0XxdUCVnHggqcOTqZRSEE=
|
||||
@@ -9,6 +10,8 @@ github.com/Azure/go-autorest v10.12.0+incompatible h1:6YphwUK+oXbzvCc1fd5VrnxCek
|
||||
github.com/Azure/go-autorest v10.12.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20180810175552-4a21cbd618b4 h1:pSm8mp0T2OH2CPmPDPtwHPr3VAQaOwVF/JbllOPP4xA=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20180810175552-4a21cbd618b4/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/Bowery/prompt v0.0.0-20180817134258-8a1d5376df1c h1:fAMg70P5ydy1uiIj6CdA69h6nmQKbv18VlVOXhKNrcM=
|
||||
github.com/Bowery/prompt v0.0.0-20180817134258-8a1d5376df1c/go.mod h1:4/6eNcqZ09BZ9wLK3tZOjBA1nDj+B0728nlX5YRlSmQ=
|
||||
github.com/ChrisTrenkamp/goxpath v0.0.0-20170625215350-4fe035839290 h1:K9I21XUHNbYD3GNMmJBN0UKJCpdP+glftwNZ7Bo8kqY=
|
||||
github.com/ChrisTrenkamp/goxpath v0.0.0-20170625215350-4fe035839290/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4=
|
||||
github.com/DataDog/datadog-go v0.0.0-20180822151419-281ae9f2d895 h1:dmc/C8bpE5VkQn65PNbbyACDC8xw8Hpp/NEurdPmQDQ=
|
||||
@@ -35,6 +38,8 @@ github.com/antchfx/xpath v0.0.0-20170728053731-b5c552e1acbd h1:S3Fr6QnkpW9VRjiEY
|
||||
github.com/antchfx/xpath v0.0.0-20170728053731-b5c552e1acbd/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
|
||||
github.com/antchfx/xquery v0.0.0-20170730121040-eb8c3c172607 h1:BFFG6KP8ASFBg2ptWsJn8p8RDufBjBDKIxLU7BTYGOM=
|
||||
github.com/antchfx/xquery v0.0.0-20170730121040-eb8c3c172607/go.mod h1:LzD22aAzDP8/dyiCKFp31He4m2GPjl0AFyzDtZzUu9M=
|
||||
github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6 h1:uZuxRZCz65cG1o6K/xUqImNcYKtmk9ylqaH0itMSvzA=
|
||||
github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q=
|
||||
github.com/approvals/go-approval-tests v0.0.0-20160714161514-ad96e53bea43 h1:ePCAQPf5tUc5IMcUvu6euhSGna7jzs7eiXtJXHig6Zc=
|
||||
github.com/approvals/go-approval-tests v0.0.0-20160714161514-ad96e53bea43/go.mod h1:S6puKjZ9ZeqUPBv2hEBnMZGcM2J6mOsDRQcmxkMAND0=
|
||||
github.com/armon/go-metrics v0.0.0-20180713145231-3c58d8115a78 h1:mdRSArcFLfW0VoL34LZAKSz6LkkK4jFxVx2xYavACMg=
|
||||
@@ -70,17 +75,23 @@ github.com/creack/goselect v0.0.0-20180210034346-528c74964609 h1:FSxXMd2wCHj6Gqg
|
||||
github.com/creack/goselect v0.0.0-20180210034346-528c74964609/go.mod h1:gHrIcH/9UZDn2qgeTUeW5K9eZsVYCH6/60J/FHysWyE=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dchest/safefile v0.0.0-20151022103144-855e8d98f185 h1:3T8ZyTDp5QxTx3NU48JVb2u+75xc040fofcBaN+6jPA=
|
||||
github.com/dchest/safefile v0.0.0-20151022103144-855e8d98f185/go.mod h1:cFRxtTwTOJkz2x3rQUNCYKWC93yP1VKjR8NUhqFxZNU=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f h1:WH0w/R4Yoey+04HhFxqZ6VX6I0d7RMyw5aXQ9UTvQPs=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc=
|
||||
github.com/denverdino/aliyungo v0.0.0-20180417075537-ebad04655e03 h1:NYqwUkb/ypX8OZmBDpPPbZ4npAPjlGFXZ4aSOyRzRL8=
|
||||
github.com/denverdino/aliyungo v0.0.0-20180417075537-ebad04655e03/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=
|
||||
github.com/denverdino/aliyungo v0.0.0-20190220033614-36e2ae938978 h1:oyfbRmu7YnytN4bXhvJPY1HPgUL52j5AxtgGDU0bMVs=
|
||||
github.com/denverdino/aliyungo v0.0.0-20190220033614-36e2ae938978/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/digitalocean/godo v0.0.0-20170407151542-4c04abe183f4 h1:34XBbvedApvUMZY6E6Q95ksnpl+FtX+KF3ywZzZY6X0=
|
||||
github.com/digitalocean/godo v0.0.0-20170407151542-4c04abe183f4/go.mod h1:h6faOIcZ8lWIwNQ+DN7b3CgX4Kwby5T+nbpNqkUIozU=
|
||||
github.com/digitalocean/godo v1.7.3 h1:0tFPilFBDsVSiAKF8hyzj/MAmRYxNc5MGhAiYZYVrpo=
|
||||
github.com/digitalocean/godo v1.7.3/go.mod h1:h6faOIcZ8lWIwNQ+DN7b3CgX4Kwby5T+nbpNqkUIozU=
|
||||
github.com/dnaeon/go-vcr v1.0.0 h1:1QZ+ahihvRvppcJnFvuoHAdnZTf1PqKjO4Ftr1cfQTo=
|
||||
github.com/dnaeon/go-vcr v1.0.0/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
|
||||
github.com/docker/docker v0.0.0-20170406124027-fa3e2d5ab9b5 h1:V/WghsgVPZl+92drOvPEwuZdHskJmrnD83V1msaV/Jc=
|
||||
github.com/docker/docker v0.0.0-20170406124027-fa3e2d5ab9b5 h1:DwY2bFs8p+xf2WaQewx2hnGdjYR5K4UAaxcNPyKkTek=
|
||||
github.com/docker/docker v0.0.0-20170406124027-fa3e2d5ab9b5/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
@@ -140,7 +151,7 @@ github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/websocket v0.0.0-20170319172727-a91eba7f9777 h1:JIM+OacoOJRU30xpjMf8sulYqjr0ViA3WDrTX6j/yDI=
|
||||
github.com/gorilla/websocket v0.0.0-20170319172727-a91eba7f9777/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gotestyourself/gotestyourself v2.2.0+incompatible h1:1yOKgt0XYKUg1HOKunGOSt2ocU4bxLCjmIHt0vRtVHM=
|
||||
github.com/gotestyourself/gotestyourself v2.2.0+incompatible h1:AQwinXlbQR2HvPjQZOmDhRqsv5mZf+Jb1RnSLxcqZcI=
|
||||
github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY=
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
|
||||
@@ -169,7 +180,7 @@ github.com/hashicorp/go-plugin v0.0.0-20181030172320-54b6ff97d818 h1:wA1XRGBHMdp
|
||||
github.com/hashicorp/go-plugin v0.0.0-20181030172320-54b6ff97d818/go.mod h1:Ft7ju2vWzhO0ETMKUVo12XmXmII6eSUS4rsPTkY/siA=
|
||||
github.com/hashicorp/go-retryablehttp v0.0.0-20180718195005-e651d75abec6 h1:qCv4319q2q7XKn0MQbi8p37hsJ+9Xo8e6yojA73JVxk=
|
||||
github.com/hashicorp/go-retryablehttp v0.0.0-20180718195005-e651d75abec6/go.mod h1:fXcdFsQoipQa7mwORhKad5jmDCeSy/RCGzWA08PO0lM=
|
||||
github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90 h1:9HVkPxOpo+yO93Ah4yrO67d/qh0fbLLWbKqhYjyHq9A=
|
||||
github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90 h1:VBj0QYQ0u2MCJzBfeYXGexnAl17GsH1yidnoxCqqD9E=
|
||||
github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90/go.mod h1:o4zcYY1e0GEZI6eSEr+43QDYmuGglw1qSO6qdHUHCgg=
|
||||
github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86 h1:7YOlAIO2YWnJZkQp7B5eFykaIY7C9JndqAFQyVV5BhM=
|
||||
github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
@@ -195,6 +206,8 @@ github.com/hetznercloud/hcloud-go v1.12.0 h1:ugZO8a8ADekqSWi7xWlcs6pxr4QE0tw5Vny
|
||||
github.com/hetznercloud/hcloud-go v1.12.0/go.mod h1:g5pff0YNAZywQaivY/CmhUYFVp7oP0nu3MiODC2W4Hw=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/hyperonecom/h1-client-go v0.0.0-20190122232013-cf38e8387775 h1:MIteIoIQ5nFoOmwEHPDsqng8d0dtKj3lCnQCwGvtxXc=
|
||||
github.com/hyperonecom/h1-client-go v0.0.0-20190122232013-cf38e8387775/go.mod h1:R9rU87RxxmcD3DkspW9JqGBXiJyg5MA+WNCtJrBtnXs=
|
||||
github.com/jefferai/jsonx v0.0.0-20160721235117-9cc31c3135ee h1:AQ/QmCk6x8ECPpf2pkPtA4lyncEEBbs8VFnVXPYKhIs=
|
||||
github.com/jefferai/jsonx v0.0.0-20160721235117-9cc31c3135ee/go.mod h1:N0t2vlmpe8nyZB5ouIbJQPDSR+mH6oe7xHB9VZHSUzM=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
|
||||
@@ -203,6 +216,8 @@ github.com/joyent/triton-go v0.0.0-20180116165742-545edbe0d564 h1:+HMa2xWQOm+9eb
|
||||
github.com/joyent/triton-go v0.0.0-20180116165742-545edbe0d564/go.mod h1:U+RSyWxWd04xTqnuOQxnai7XGS2PrPY2cfGoDKtMHjA=
|
||||
github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE=
|
||||
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kardianos/govendor v1.0.9 h1:WOH3FcVI9eOgnIZYg96iwUwrL4eOVx+aQ66oyX2R8Yc=
|
||||
github.com/kardianos/govendor v1.0.9/go.mod h1:yvmR6q9ZZ7nSF5Wvh40v0wfP+3TwwL8zYQp+itoZSVM=
|
||||
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 h1:PJPDf8OUfOK1bb/NeTKd4f1QXZItOX389VN3B6qC8ro=
|
||||
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
|
||||
github.com/keybase/go-crypto v0.0.0-20181127160227-255a5089e85a h1:X/UFlwD2/UV0RCy+8ITi4DmxJwk83YUH7bXwkJIHHMo=
|
||||
@@ -268,6 +283,8 @@ github.com/mitchellh/prefixedio v0.0.0-20151214002211-6e6954073784 h1:+DAetXqxv/
|
||||
github.com/mitchellh/prefixedio v0.0.0-20151214002211-6e6954073784/go.mod h1:kB1naBgV9ORnkiTVeyJOI1DavaJkG4oNIq0Af6ZVKUo=
|
||||
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
|
||||
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/mna/pigeon v1.0.0 h1:n46IoStjdzjaXuyBH53j9HZ8CVqGWpC7P5/v8dP4qEY=
|
||||
github.com/mna/pigeon v1.0.0/go.mod h1:Iym28+kJVnC1hfQvv5MUtI6AiFFzvQjHcvI4RFTG/04=
|
||||
github.com/moul/anonuuid v0.0.0-20160222162117-609b752a95ef h1:E/seV1Rtsnr2juBw1Dfz4iDPT3/5s1H/BATx+ePmSyo=
|
||||
github.com/moul/anonuuid v0.0.0-20160222162117-609b752a95ef/go.mod h1:LgKrp0Iss/BVwquptq4eIe6HPr0s3t1WHT5x0qOh14U=
|
||||
github.com/moul/gotty-client v0.0.0-20180327180212-b26a57ebc215 h1:y6FZWUBBt1iPmJyGbGza3ncvVBMKzgd32oFChRZR7Do=
|
||||
@@ -355,27 +372,38 @@ github.com/ulikunitz/xz v0.0.0-20180703112113-636d36a76670 h1:HQWT4ta3wW5GZ790Ga
|
||||
github.com/ulikunitz/xz v0.0.0-20180703112113-636d36a76670/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
||||
github.com/vmware/govmomi v0.0.0-20170707011325-c2105a174311 h1:s5pyxd5S6wRs2WpEE0xRfWUF46Wbz44h203KnbX0ecI=
|
||||
github.com/vmware/govmomi v0.0.0-20170707011325-c2105a174311/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU=
|
||||
github.com/xanzy/go-cloudstack v2.1.4+incompatible h1:5c72sRFakVv8wH/HsQFg+xr37CmNQU2UbJfaBjW5f0c=
|
||||
github.com/xanzy/go-cloudstack v2.1.4+incompatible/go.mod h1:s3eL3z5pNXF5FVybcT+LIVdId8pYn709yv6v5mrkrQE=
|
||||
github.com/xanzy/go-cloudstack v2.4.1+incompatible h1:Oc4xa2+I94h1g/QJ+nHoq597nJz2KXzxuQx/weOx0AU=
|
||||
github.com/xanzy/go-cloudstack v2.4.1+incompatible/go.mod h1:s3eL3z5pNXF5FVybcT+LIVdId8pYn709yv6v5mrkrQE=
|
||||
golang.org/x/crypto v0.0.0-20180322175230-88942b9c40a4 h1:AJCW0rhPjFKEAoValWpqnRKxX8YV0Xvqfw+dOexCTPc=
|
||||
golang.org/x/crypto v0.0.0-20180322175230-88942b9c40a4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc h1:a3CU5tJYVj92DY2LaA1kUkrsqD5/3mLDhx2NcNqyW+0=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd h1:HuTn7WObtcDo9uEEU7rEqL0jYthdXAmZ6PP+meazmaU=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 h1:+DCIGbF/swA92ohVg0//6X2IVY3KZs6p9mix0ziNYJM=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190221204921-83362c3779f5 h1:ev5exjGDsOo0NPTB0qdCcE53BfWl1IICJlhgXgfT9fM=
|
||||
golang.org/x/tools v0.0.0-20190221204921-83362c3779f5/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
google.golang.org/api v0.0.0-20180818000503-e21acd801f91 h1:MgYYgjaWMS2qQiDwCznfbqNmEOdSULlvjCvSCvIe/Wo=
|
||||
google.golang.org/api v0.0.0-20180818000503-e21acd801f91/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
@@ -407,7 +435,7 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/vmihailenco/msgpack.v2 v2.9.1 h1:kb0VV7NuIojvRfzwslQeP3yArBqJHW9tOl4t38VS1jM=
|
||||
gopkg.in/vmihailenco/msgpack.v2 v2.9.1/go.mod h1:/3Dn1Npt9+MYyLpYYXjInO/5jvMLamn+AEGwNEOatn8=
|
||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gotest.tools v2.2.0+incompatible h1:y0IMTfclpMdsdIbr6uwmJn5/WZ7vFuObxDMdrylFM3A=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
||||
@@ -66,6 +66,9 @@ type Config struct {
|
||||
WinRMInsecure bool `mapstructure:"winrm_insecure"`
|
||||
WinRMUseNTLM bool `mapstructure:"winrm_use_ntlm"`
|
||||
WinRMTransportDecorator func() winrm.Transporter
|
||||
|
||||
// Delay
|
||||
PauseBeforeConnect time.Duration `mapstructure:"pause_before_connecting"`
|
||||
}
|
||||
|
||||
// ReadSSHPrivateKeyFile returns the SSH private key bytes
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/packer/communicator/none"
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
@@ -44,6 +45,18 @@ type StepConnect struct {
|
||||
substep multistep.Step
|
||||
}
|
||||
|
||||
func (s *StepConnect) pause(pauseLen time.Duration, ctx context.Context) bool {
|
||||
// Use a select to determine if we get cancelled during the wait
|
||||
log.Printf("Pausing before connecting...")
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return true
|
||||
case <-time.After(pauseLen):
|
||||
}
|
||||
log.Printf("Pause over; connecting...")
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *StepConnect) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
@@ -94,7 +107,19 @@ func (s *StepConnect) Run(ctx context.Context, state multistep.StateBag) multist
|
||||
}
|
||||
|
||||
s.substep = step
|
||||
return s.substep.Run(ctx, state)
|
||||
action := s.substep.Run(ctx, state)
|
||||
if action == multistep.ActionHalt {
|
||||
return action
|
||||
}
|
||||
|
||||
if s.Config.PauseBeforeConnect > 0 {
|
||||
cancelled := s.pause(s.Config.PauseBeforeConnect, ctx)
|
||||
if cancelled {
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepConnect) Cleanup(state multistep.StateBag) {
|
||||
|
||||
+10
-5
@@ -180,13 +180,18 @@ func (c *Core) Build(n string) (Build, error) {
|
||||
for _, rawPs := range c.Template.PostProcessors {
|
||||
current := make([]coreBuildPostProcessor, 0, len(rawPs))
|
||||
for _, rawP := range rawPs {
|
||||
// If we skip, ignore
|
||||
rawP.OnlyExcept.Except = append(rawP.OnlyExcept.Except, c.except...)
|
||||
if rawP.OnlyExcept.Skip(rawName) {
|
||||
if rawP.Skip(rawName) {
|
||||
continue
|
||||
}
|
||||
if rawP.OnlyExcept.Skip(rawP.Name) {
|
||||
break
|
||||
// -except skips post-processor & build
|
||||
foundExcept := false
|
||||
for _, except := range c.except {
|
||||
if except != "" && except == rawP.Name {
|
||||
foundExcept = true
|
||||
}
|
||||
}
|
||||
if foundExcept {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the post-processor
|
||||
|
||||
+46
-1
@@ -343,7 +343,7 @@ func (u *MachineReadableUi) ProgressBar() ProgressBar {
|
||||
return new(NoopProgressBar)
|
||||
}
|
||||
|
||||
// TimestampedUi is a UI that wraps another UI implementation and prefixes
|
||||
// TimestampedUi is a UI that wraps another UI implementation and
|
||||
// prefixes each message with an RFC3339 timestamp
|
||||
type TimestampedUi struct {
|
||||
Ui Ui
|
||||
@@ -376,3 +376,48 @@ func (u *TimestampedUi) ProgressBar() ProgressBar { return u.Ui.ProgressBar() }
|
||||
func (u *TimestampedUi) timestampLine(string string) string {
|
||||
return fmt.Sprintf("%v: %v", time.Now().Format(time.RFC3339), string)
|
||||
}
|
||||
|
||||
// Safe is a UI that wraps another UI implementation and
|
||||
// provides concurrency-safe access
|
||||
type SafeUi struct {
|
||||
Sem chan int
|
||||
Ui Ui
|
||||
}
|
||||
|
||||
var _ Ui = new(SafeUi)
|
||||
|
||||
func (u *SafeUi) Ask(s string) (string, error) {
|
||||
u.Sem <- 1
|
||||
ret, err := u.Ui.Ask(s)
|
||||
<-u.Sem
|
||||
|
||||
return ret, err
|
||||
}
|
||||
|
||||
func (u *SafeUi) Say(s string) {
|
||||
u.Sem <- 1
|
||||
u.Ui.Say(s)
|
||||
<-u.Sem
|
||||
}
|
||||
|
||||
func (u *SafeUi) Message(s string) {
|
||||
u.Sem <- 1
|
||||
u.Ui.Message(s)
|
||||
<-u.Sem
|
||||
}
|
||||
|
||||
func (u *SafeUi) Error(s string) {
|
||||
u.Sem <- 1
|
||||
u.Ui.Error(s)
|
||||
<-u.Sem
|
||||
}
|
||||
|
||||
func (u *SafeUi) Machine(t string, args ...string) {
|
||||
u.Sem <- 1
|
||||
u.Ui.Machine(t, args...)
|
||||
<-u.Sem
|
||||
}
|
||||
|
||||
func (u *SafeUi) ProgressBar() ProgressBar {
|
||||
return new(NoopProgressBar)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
package digitaloceanimport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"golang.org/x/oauth2"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
"github.com/digitalocean/godo"
|
||||
|
||||
"github.com/hashicorp/packer/builder/digitalocean"
|
||||
"github.com/hashicorp/packer/common"
|
||||
"github.com/hashicorp/packer/helper/config"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/hashicorp/packer/template/interpolate"
|
||||
)
|
||||
|
||||
const BuilderId = "packer.post-processor.digitalocean-import"
|
||||
|
||||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
|
||||
APIToken string `mapstructure:"api_token"`
|
||||
SpacesKey string `mapstructure:"spaces_key"`
|
||||
SpacesSecret string `mapstructure:"spaces_secret"`
|
||||
|
||||
SpacesRegion string `mapstructure:"spaces_region"`
|
||||
SpaceName string `mapstructure:"space_name"`
|
||||
ObjectName string `mapstructure:"space_object_name"`
|
||||
SkipClean bool `mapstructure:"skip_clean"`
|
||||
Tags []string `mapstructure:"image_tags"`
|
||||
Name string `mapstructure:"image_name"`
|
||||
Description string `mapstructure:"image_description"`
|
||||
Distribution string `mapstructure:"image_distribution"`
|
||||
ImageRegions []string `mapstructure:"image_regions"`
|
||||
Timeout time.Duration `mapstructure:"timeout"`
|
||||
|
||||
ctx interpolate.Context
|
||||
}
|
||||
|
||||
type PostProcessor struct {
|
||||
config Config
|
||||
}
|
||||
|
||||
type apiTokenSource struct {
|
||||
AccessToken string
|
||||
}
|
||||
|
||||
type logger struct {
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
func (t *apiTokenSource) Token() (*oauth2.Token, error) {
|
||||
return &oauth2.Token{
|
||||
AccessToken: t.AccessToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l logger) Log(args ...interface{}) {
|
||||
l.logger.Println(args...)
|
||||
}
|
||||
|
||||
func (p *PostProcessor) Configure(raws ...interface{}) error {
|
||||
err := config.Decode(&p.config, &config.DecodeOpts{
|
||||
Interpolate: true,
|
||||
InterpolateContext: &p.config.ctx,
|
||||
InterpolateFilter: &interpolate.RenderFilter{
|
||||
Exclude: []string{"space_object_name"},
|
||||
},
|
||||
}, raws...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.config.SpacesKey == "" {
|
||||
p.config.SpacesKey = os.Getenv("DIGITALOCEAN_SPACES_ACCESS_KEY")
|
||||
}
|
||||
|
||||
if p.config.SpacesSecret == "" {
|
||||
p.config.SpacesSecret = os.Getenv("DIGITALOCEAN_SPACES_SECRET_KEY")
|
||||
}
|
||||
|
||||
if p.config.APIToken == "" {
|
||||
p.config.APIToken = os.Getenv("DIGITALOCEAN_API_TOKEN")
|
||||
}
|
||||
|
||||
if p.config.ObjectName == "" {
|
||||
p.config.ObjectName = "packer-import-{{timestamp}}"
|
||||
}
|
||||
|
||||
if p.config.Distribution == "" {
|
||||
p.config.Distribution = "Unkown"
|
||||
}
|
||||
|
||||
if p.config.Timeout == 0 {
|
||||
p.config.Timeout = 20 * time.Minute
|
||||
}
|
||||
|
||||
errs := new(packer.MultiError)
|
||||
|
||||
if err = interpolate.Validate(p.config.ObjectName, &p.config.ctx); err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Error parsing space_object_name template: %s", err))
|
||||
}
|
||||
|
||||
requiredArgs := map[string]*string{
|
||||
"api_token": &p.config.APIToken,
|
||||
"spaces_key": &p.config.SpacesKey,
|
||||
"spaces_secret": &p.config.SpacesSecret,
|
||||
"spaces_region": &p.config.SpacesRegion,
|
||||
"space_name": &p.config.SpaceName,
|
||||
"image_name": &p.config.Name,
|
||||
"image_regions": &p.config.ImageRegions[0],
|
||||
}
|
||||
for key, ptr := range requiredArgs {
|
||||
if *ptr == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("%s must be set", key))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs.Errors) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
packer.LogSecretFilter.Set(p.config.SpacesKey, p.config.SpacesSecret, p.config.APIToken)
|
||||
log.Println(p.config)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, error) {
|
||||
var err error
|
||||
|
||||
p.config.ObjectName, err = interpolate.Render(p.config.ObjectName, &p.config.ctx)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("Error rendering space_object_name template: %s", err)
|
||||
}
|
||||
log.Printf("Rendered space_object_name as %s", p.config.ObjectName)
|
||||
|
||||
source := ""
|
||||
artifacts := artifact.Files()
|
||||
log.Println("Looking for image in artifact")
|
||||
if len(artifacts) > 1 {
|
||||
validSuffix := []string{"raw", "img", "qcow2", "vhdx", "vdi", "vmdk", "tar.bz2", "tar.xz", "tar.gz"}
|
||||
for _, path := range artifact.Files() {
|
||||
for _, suffix := range validSuffix {
|
||||
if strings.HasSuffix(path, suffix) {
|
||||
source = path
|
||||
break
|
||||
}
|
||||
}
|
||||
if source != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
source = artifact.Files()[0]
|
||||
}
|
||||
|
||||
if source == "" {
|
||||
return nil, false, fmt.Errorf("Image file not found")
|
||||
}
|
||||
|
||||
spacesCreds := credentials.NewStaticCredentials(p.config.SpacesKey, p.config.SpacesSecret, "")
|
||||
spacesEndpoint := fmt.Sprintf("https://%s.digitaloceanspaces.com", p.config.SpacesRegion)
|
||||
spacesConfig := &aws.Config{
|
||||
Credentials: spacesCreds,
|
||||
Endpoint: aws.String(spacesEndpoint),
|
||||
Region: aws.String(p.config.SpacesRegion),
|
||||
LogLevel: aws.LogLevel(aws.LogDebugWithSigning),
|
||||
Logger: &logger{
|
||||
logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
},
|
||||
}
|
||||
sess := session.New(spacesConfig)
|
||||
|
||||
ui.Message(fmt.Sprintf("Uploading %s to spaces://%s/%s", source, p.config.SpaceName, p.config.ObjectName))
|
||||
err = uploadImageToSpaces(source, p, sess)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
ui.Message(fmt.Sprintf("Completed upload of %s to spaces://%s/%s", source, p.config.SpaceName, p.config.ObjectName))
|
||||
|
||||
client := godo.NewClient(oauth2.NewClient(oauth2.NoContext, &apiTokenSource{
|
||||
AccessToken: p.config.APIToken,
|
||||
}))
|
||||
|
||||
ui.Message(fmt.Sprintf("Started import of spaces://%s/%s", p.config.SpaceName, p.config.ObjectName))
|
||||
image, err := importImageFromSpaces(p, client)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
ui.Message(fmt.Sprintf("Waiting for import of image %s to complete (may take a while)", p.config.Name))
|
||||
err = waitUntilImageAvailable(client, image.ID, p.config.Timeout)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("Import of image %s failed with error: %s", p.config.Name, err)
|
||||
}
|
||||
ui.Message(fmt.Sprintf("Import of image %s complete", p.config.Name))
|
||||
|
||||
if len(p.config.ImageRegions) > 1 {
|
||||
// Remove the first region from the slice as the image is already there.
|
||||
regions := p.config.ImageRegions
|
||||
regions[0] = regions[len(regions)-1]
|
||||
regions[len(regions)-1] = ""
|
||||
regions = regions[:len(regions)-1]
|
||||
|
||||
ui.Message(fmt.Sprintf("Distributing image %s to additional regions: %v", p.config.Name, regions))
|
||||
err = distributeImageToRegions(client, image.ID, regions, p.config.Timeout)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Adding created image ID %v to output artifacts", image.ID)
|
||||
artifact = &digitalocean.Artifact{
|
||||
SnapshotName: image.Name,
|
||||
SnapshotId: image.ID,
|
||||
RegionNames: p.config.ImageRegions,
|
||||
Client: client,
|
||||
}
|
||||
|
||||
if !p.config.SkipClean {
|
||||
ui.Message(fmt.Sprintf("Deleting import source spaces://%s/%s", p.config.SpaceName, p.config.ObjectName))
|
||||
err = deleteImageFromSpaces(p, sess)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
return artifact, false, nil
|
||||
}
|
||||
|
||||
func uploadImageToSpaces(source string, p *PostProcessor, s *session.Session) (err error) {
|
||||
file, err := os.Open(source)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to open %s: %s", source, err)
|
||||
}
|
||||
|
||||
uploader := s3manager.NewUploader(s)
|
||||
_, err = uploader.Upload(&s3manager.UploadInput{
|
||||
Body: file,
|
||||
Bucket: &p.config.SpaceName,
|
||||
Key: &p.config.ObjectName,
|
||||
ACL: aws.String("public-read"),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to upload %s: %s", source, err)
|
||||
}
|
||||
|
||||
file.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func importImageFromSpaces(p *PostProcessor, client *godo.Client) (image *godo.Image, err error) {
|
||||
log.Printf("Importing custom image from spaces://%s/%s", p.config.SpaceName, p.config.ObjectName)
|
||||
|
||||
url := fmt.Sprintf("https://%s.%s.digitaloceanspaces.com/%s", p.config.SpaceName, p.config.SpacesRegion, p.config.ObjectName)
|
||||
createRequest := &godo.CustomImageCreateRequest{
|
||||
Name: p.config.Name,
|
||||
Url: url,
|
||||
Region: p.config.ImageRegions[0],
|
||||
Distribution: p.config.Distribution,
|
||||
Description: p.config.Description,
|
||||
Tags: p.config.Tags,
|
||||
}
|
||||
|
||||
image, _, err = client.Images.Create(context.TODO(), createRequest)
|
||||
if err != nil {
|
||||
return image, fmt.Errorf("Failed to import from spaces://%s/%s: %s", p.config.SpaceName, p.config.ObjectName, err)
|
||||
}
|
||||
|
||||
return image, nil
|
||||
}
|
||||
|
||||
func waitUntilImageAvailable(client *godo.Client, imageId int, timeout time.Duration) (err error) {
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
|
||||
result := make(chan error, 1)
|
||||
go func() {
|
||||
attempts := 0
|
||||
for {
|
||||
attempts += 1
|
||||
|
||||
log.Printf("Waiting for image to become available... (attempt: %d)", attempts)
|
||||
image, _, err := client.Images.GetByID(context.TODO(), imageId)
|
||||
if err != nil {
|
||||
result <- err
|
||||
return
|
||||
}
|
||||
|
||||
if image.Status == "available" {
|
||||
result <- nil
|
||||
return
|
||||
}
|
||||
|
||||
if image.ErrorMessage != "" {
|
||||
result <- fmt.Errorf("%v", image.ErrorMessage)
|
||||
return
|
||||
}
|
||||
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
log.Printf("Waiting for up to %d seconds for image to become available", timeout/time.Second)
|
||||
select {
|
||||
case err := <-result:
|
||||
return err
|
||||
case <-time.After(timeout):
|
||||
err := fmt.Errorf("Timeout while waiting to for action to become available")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func distributeImageToRegions(client *godo.Client, imageId int, regions []string, timeout time.Duration) (err error) {
|
||||
for _, region := range regions {
|
||||
transferRequest := &godo.ActionRequest{
|
||||
"type": "transfer",
|
||||
"region": region,
|
||||
}
|
||||
log.Printf("Transferring image to %s", region)
|
||||
action, _, err := client.ImageActions.Transfer(context.TODO(), imageId, transferRequest)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error transferring image: %s", err)
|
||||
}
|
||||
|
||||
if err := digitalocean.WaitForImageState(godo.ActionCompleted, imageId, action.ID, client, timeout); err != nil {
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error transferring image: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteImageFromSpaces(p *PostProcessor, s *session.Session) (err error) {
|
||||
s3conn := s3.New(s)
|
||||
_, err = s3conn.DeleteObject(&s3.DeleteObjectInput{
|
||||
Bucket: &p.config.SpaceName,
|
||||
Key: &p.config.ObjectName,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to delete spaces://%s/%s: %s", p.config.SpaceName, p.config.ObjectName, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package digitaloceanimport
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
||||
func testConfig() map[string]interface{} {
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
|
||||
func testPP(t *testing.T) *PostProcessor {
|
||||
var p PostProcessor
|
||||
if err := p.Configure(testConfig()); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
return &p
|
||||
}
|
||||
|
||||
func testUi() *packer.BasicUi {
|
||||
return &packer.BasicUi{
|
||||
Reader: new(bytes.Buffer),
|
||||
Writer: new(bytes.Buffer),
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostProcessor_ImplementsPostProcessor(t *testing.T) {
|
||||
var _ packer.PostProcessor = new(PostProcessor)
|
||||
}
|
||||
@@ -15,10 +15,19 @@ import (
|
||||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
|
||||
Paths []string `mapstructure:"paths"`
|
||||
KeepOriginalImage bool `mapstructure:"keep_input_artifact"`
|
||||
AccountFile string `mapstructure:"account_file"`
|
||||
|
||||
ctx interpolate.Context
|
||||
DiskSizeGb int64 `mapstructure:"disk_size"`
|
||||
DiskType string `mapstructure:"disk_type"`
|
||||
KeepOriginalImage bool `mapstructure:"keep_input_artifact"`
|
||||
MachineType string `mapstructure:"machine_type"`
|
||||
Network string `mapstructure:"network"`
|
||||
Paths []string `mapstructure:"paths"`
|
||||
Subnetwork string `mapstructure:"subnetwork"`
|
||||
Zone string `mapstructure:"zone"`
|
||||
|
||||
Account googlecompute.AccountFile
|
||||
ctx interpolate.Context
|
||||
}
|
||||
|
||||
type PostProcessor struct {
|
||||
@@ -35,12 +44,38 @@ func (p *PostProcessor) Configure(raws ...interface{}) error {
|
||||
return err
|
||||
}
|
||||
|
||||
errs := new(packer.MultiError)
|
||||
|
||||
if len(p.config.Paths) == 0 {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("paths must be specified"))
|
||||
}
|
||||
|
||||
// Set defaults.
|
||||
if p.config.DiskSizeGb == 0 {
|
||||
p.config.DiskSizeGb = 200
|
||||
}
|
||||
|
||||
if p.config.DiskType == "" {
|
||||
p.config.DiskType = "pd-ssd"
|
||||
}
|
||||
|
||||
if p.config.MachineType == "" {
|
||||
p.config.MachineType = "n1-highcpu-4"
|
||||
}
|
||||
|
||||
if p.config.Network == "" && p.config.Subnetwork == "" {
|
||||
p.config.Network = "default"
|
||||
}
|
||||
|
||||
if len(errs.Errors) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, error) {
|
||||
ui.Say("Starting googlecompute-export...")
|
||||
ui.Say(fmt.Sprintf("Exporting image to destinations: %v", p.config.Paths))
|
||||
if artifact.BuilderId() != googlecompute.BuilderId {
|
||||
err := fmt.Errorf(
|
||||
"Unknown artifact type: %s\nCan only export from Google Compute Engine builder artifacts.",
|
||||
@@ -48,79 +83,90 @@ func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (pac
|
||||
return nil, p.config.KeepOriginalImage, err
|
||||
}
|
||||
|
||||
result := &Artifact{paths: p.config.Paths}
|
||||
builderAccountFile := artifact.State("AccountFilePath").(string)
|
||||
builderImageName := artifact.State("ImageName").(string)
|
||||
builderProjectId := artifact.State("ProjectId").(string)
|
||||
builderZone := artifact.State("BuildZone").(string)
|
||||
|
||||
if len(p.config.Paths) > 0 {
|
||||
accountKeyFilePath := artifact.State("AccountFilePath").(string)
|
||||
imageName := artifact.State("ImageName").(string)
|
||||
imageSizeGb := artifact.State("ImageSizeGb").(int64)
|
||||
projectId := artifact.State("ProjectId").(string)
|
||||
zone := artifact.State("BuildZone").(string)
|
||||
ui.Say(fmt.Sprintf("Exporting image %v to destination: %v", builderImageName, p.config.Paths))
|
||||
|
||||
// Set up instance configuration.
|
||||
instanceName := fmt.Sprintf("%s-exporter", artifact.Id())
|
||||
metadata := map[string]string{
|
||||
"image_name": imageName,
|
||||
"name": instanceName,
|
||||
"paths": strings.Join(p.config.Paths, " "),
|
||||
"startup-script": StartupScript,
|
||||
"zone": zone,
|
||||
}
|
||||
exporterConfig := googlecompute.Config{
|
||||
InstanceName: instanceName,
|
||||
SourceImageProjectId: "debian-cloud",
|
||||
SourceImage: "debian-8-jessie-v20160629",
|
||||
DiskName: instanceName,
|
||||
DiskSizeGb: imageSizeGb + 10,
|
||||
DiskType: "pd-standard",
|
||||
Metadata: metadata,
|
||||
MachineType: "n1-standard-4",
|
||||
Zone: zone,
|
||||
Network: "default",
|
||||
RawStateTimeout: "5m",
|
||||
Scopes: []string{
|
||||
"https://www.googleapis.com/auth/userinfo.email",
|
||||
"https://www.googleapis.com/auth/compute",
|
||||
"https://www.googleapis.com/auth/devstorage.full_control",
|
||||
},
|
||||
}
|
||||
exporterConfig.CalcTimeout()
|
||||
if p.config.Zone == "" {
|
||||
p.config.Zone = builderZone
|
||||
}
|
||||
|
||||
// Set up credentials and GCE driver.
|
||||
if accountKeyFilePath != "" {
|
||||
err := googlecompute.ProcessAccountFile(&exporterConfig.Account, accountKeyFilePath)
|
||||
if err != nil {
|
||||
return nil, p.config.KeepOriginalImage, err
|
||||
}
|
||||
}
|
||||
driver, err := googlecompute.NewDriverGCE(ui, projectId, &exporterConfig.Account)
|
||||
// Set up credentials for GCE driver.
|
||||
if builderAccountFile != "" {
|
||||
err := googlecompute.ProcessAccountFile(&p.config.Account, builderAccountFile)
|
||||
if err != nil {
|
||||
return nil, p.config.KeepOriginalImage, err
|
||||
}
|
||||
|
||||
// Set up the state.
|
||||
state := new(multistep.BasicStateBag)
|
||||
state.Put("config", &exporterConfig)
|
||||
state.Put("driver", driver)
|
||||
state.Put("ui", ui)
|
||||
|
||||
// Build the steps.
|
||||
steps := []multistep.Step{
|
||||
&googlecompute.StepCreateSSHKey{
|
||||
Debug: p.config.PackerDebug,
|
||||
DebugKeyPath: fmt.Sprintf("gce_%s.pem", p.config.PackerBuildName),
|
||||
},
|
||||
&googlecompute.StepCreateInstance{
|
||||
Debug: p.config.PackerDebug,
|
||||
},
|
||||
new(googlecompute.StepWaitStartupScript),
|
||||
new(googlecompute.StepTeardownInstance),
|
||||
}
|
||||
|
||||
// Run the steps.
|
||||
p.runner = common.NewRunner(steps, p.config.PackerConfig, ui)
|
||||
p.runner.Run(state)
|
||||
}
|
||||
if p.config.AccountFile != "" {
|
||||
err := googlecompute.ProcessAccountFile(&p.config.Account, p.config.AccountFile)
|
||||
if err != nil {
|
||||
return nil, p.config.KeepOriginalImage, err
|
||||
}
|
||||
}
|
||||
|
||||
// Set up exporter instance configuration.
|
||||
exporterName := fmt.Sprintf("%s-exporter", artifact.Id())
|
||||
exporterMetadata := map[string]string{
|
||||
"image_name": builderImageName,
|
||||
"name": exporterName,
|
||||
"paths": strings.Join(p.config.Paths, " "),
|
||||
"startup-script": StartupScript,
|
||||
"zone": p.config.Zone,
|
||||
}
|
||||
exporterConfig := googlecompute.Config{
|
||||
DiskName: exporterName,
|
||||
DiskSizeGb: p.config.DiskSizeGb,
|
||||
DiskType: p.config.DiskType,
|
||||
InstanceName: exporterName,
|
||||
MachineType: p.config.MachineType,
|
||||
Metadata: exporterMetadata,
|
||||
Network: p.config.Network,
|
||||
RawStateTimeout: "5m",
|
||||
SourceImageFamily: "debian-9-worker",
|
||||
SourceImageProjectId: "compute-image-tools",
|
||||
Subnetwork: p.config.Subnetwork,
|
||||
Zone: p.config.Zone,
|
||||
Scopes: []string{
|
||||
"https://www.googleapis.com/auth/compute",
|
||||
"https://www.googleapis.com/auth/devstorage.full_control",
|
||||
"https://www.googleapis.com/auth/userinfo.email",
|
||||
},
|
||||
}
|
||||
exporterConfig.CalcTimeout()
|
||||
|
||||
driver, err := googlecompute.NewDriverGCE(ui, builderProjectId, &p.config.Account)
|
||||
if err != nil {
|
||||
return nil, p.config.KeepOriginalImage, err
|
||||
}
|
||||
|
||||
// Set up the state.
|
||||
state := new(multistep.BasicStateBag)
|
||||
state.Put("config", &exporterConfig)
|
||||
state.Put("driver", driver)
|
||||
state.Put("ui", ui)
|
||||
|
||||
// Build the steps.
|
||||
steps := []multistep.Step{
|
||||
&googlecompute.StepCreateSSHKey{
|
||||
Debug: p.config.PackerDebug,
|
||||
DebugKeyPath: fmt.Sprintf("gce_%s.pem", p.config.PackerBuildName),
|
||||
},
|
||||
&googlecompute.StepCreateInstance{
|
||||
Debug: p.config.PackerDebug,
|
||||
},
|
||||
new(googlecompute.StepWaitStartupScript),
|
||||
new(googlecompute.StepTeardownInstance),
|
||||
}
|
||||
|
||||
// Run the steps.
|
||||
p.runner = common.NewRunner(steps, p.config.PackerConfig, ui)
|
||||
p.runner.Run(state)
|
||||
|
||||
result := &Artifact{paths: p.config.Paths}
|
||||
|
||||
return result, p.config.KeepOriginalImage, nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package googlecomputeexport
|
||||
|
||||
var StartupScript string = `#!/bin/sh
|
||||
var StartupScript string = `#!/bin/bash
|
||||
|
||||
GetMetadata () {
|
||||
echo "$(curl -f -H "Metadata-Flavor: Google" http://metadata/computeMetadata/v1/instance/attributes/$1 2> /dev/null)"
|
||||
@@ -8,11 +8,11 @@ GetMetadata () {
|
||||
IMAGENAME=$(GetMetadata image_name)
|
||||
NAME=$(GetMetadata name)
|
||||
DISKNAME=${NAME}-toexport
|
||||
PATHS=$(GetMetadata paths)
|
||||
PATHS=($(GetMetadata paths))
|
||||
ZONE=$(GetMetadata zone)
|
||||
|
||||
Exit () {
|
||||
for i in ${PATHS}; do
|
||||
for i in ${PATHS[@]}; do
|
||||
LOGDEST="${i}.exporter.log"
|
||||
echo "Uploading exporter log to ${LOGDEST}..."
|
||||
gsutil -h "Content-Type:text/plain" cp /var/log/daemon.log ${LOGDEST}
|
||||
@@ -40,17 +40,15 @@ if ! gcloud compute instances attach-disk ${NAME} --disk ${DISKNAME} --device-na
|
||||
Exit 1
|
||||
fi
|
||||
|
||||
echo "Dumping disk..."
|
||||
if ! dd if=/dev/disk/by-id/google-toexport of=disk.raw bs=4096 conv=sparse; then
|
||||
echo "Failed to dump disk to image."
|
||||
echo "GCEExport: Running export tool."
|
||||
gce_export -gcs_path "${PATHS[0]}" -disk /dev/disk/by-id/google-toexport -y
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "ExportFailed: Failed to export disk source to ${PATHS[0]}."
|
||||
Exit 1
|
||||
fi
|
||||
|
||||
echo "Compressing and tar'ing disk image..."
|
||||
if ! tar -czf root.tar.gz disk.raw; then
|
||||
echo "Failed to tar disk image."
|
||||
Exit 1
|
||||
fi
|
||||
echo "ExportSuccess"
|
||||
sync
|
||||
|
||||
echo "Detaching disk..."
|
||||
if ! gcloud compute instances detach-disk ${NAME} --disk ${DISKNAME} --zone ${ZONE}; then
|
||||
@@ -64,10 +62,10 @@ if ! gcloud compute disks delete ${DISKNAME} --zone ${ZONE}; then
|
||||
FAIL=1
|
||||
fi
|
||||
|
||||
for i in ${PATHS}; do
|
||||
echo "Uploading tar'ed disk image to ${i}..."
|
||||
if ! gsutil -o GSUtil:parallel_composite_upload_threshold=100M cp root.tar.gz ${i}; then
|
||||
echo "Failed to upload image to ${i}."
|
||||
for i in ${PATHS[@]:1}; do
|
||||
echo "Copying archive image to ${i}..."
|
||||
if ! gsutil -o GSUtil:parallel_composite_upload_threshold=100M cp ${PATHS[0]} ${i}; then
|
||||
echo "Failed to copy image to ${i}."
|
||||
FAIL=1
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -13,13 +13,9 @@ import (
|
||||
"github.com/hashicorp/packer/builder/googlecompute"
|
||||
"github.com/hashicorp/packer/common"
|
||||
"github.com/hashicorp/packer/helper/config"
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/hashicorp/packer/post-processor/compress"
|
||||
"github.com/hashicorp/packer/template/interpolate"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/jwt"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
@@ -38,12 +34,12 @@ type Config struct {
|
||||
KeepOriginalImage bool `mapstructure:"keep_input_artifact"`
|
||||
SkipClean bool `mapstructure:"skip_clean"`
|
||||
|
||||
ctx interpolate.Context
|
||||
Account googlecompute.AccountFile
|
||||
ctx interpolate.Context
|
||||
}
|
||||
|
||||
type PostProcessor struct {
|
||||
config Config
|
||||
runner multistep.Runner
|
||||
}
|
||||
|
||||
func (p *PostProcessor) Configure(raws ...interface{}) error {
|
||||
@@ -60,24 +56,29 @@ func (p *PostProcessor) Configure(raws ...interface{}) error {
|
||||
return err
|
||||
}
|
||||
|
||||
errs := new(packer.MultiError)
|
||||
|
||||
// Set defaults
|
||||
if p.config.GCSObjectName == "" {
|
||||
p.config.GCSObjectName = "packer-import-{{timestamp}}.tar.gz"
|
||||
}
|
||||
|
||||
errs := new(packer.MultiError)
|
||||
|
||||
// Check and render gcs_object_name
|
||||
if err = interpolate.Validate(p.config.GCSObjectName, &p.config.ctx); err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Error parsing gcs_object_name template: %s", err))
|
||||
}
|
||||
|
||||
if p.config.AccountFile != "" {
|
||||
if err := googlecompute.ProcessAccountFile(&p.config.Account, p.config.AccountFile); err != nil {
|
||||
errs = packer.MultiErrorAppend(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
templates := map[string]*string{
|
||||
"bucket": &p.config.Bucket,
|
||||
"image_name": &p.config.ImageName,
|
||||
"project_id": &p.config.ProjectId,
|
||||
"account_file": &p.config.AccountFile,
|
||||
"bucket": &p.config.Bucket,
|
||||
"image_name": &p.config.ImageName,
|
||||
"project_id": &p.config.ProjectId,
|
||||
}
|
||||
for key, ptr := range templates {
|
||||
if *ptr == "" {
|
||||
@@ -94,7 +95,10 @@ func (p *PostProcessor) Configure(raws ...interface{}) error {
|
||||
}
|
||||
|
||||
func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, error) {
|
||||
var err error
|
||||
client, err := googlecompute.NewClientGCE(&p.config.Account)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if artifact.BuilderId() != compress.BuilderId {
|
||||
err = fmt.Errorf(
|
||||
@@ -108,18 +112,18 @@ func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (pac
|
||||
return nil, false, fmt.Errorf("Error rendering gcs_object_name template: %s", err)
|
||||
}
|
||||
|
||||
rawImageGcsPath, err := UploadToBucket(p.config.AccountFile, ui, artifact, p.config.Bucket, p.config.GCSObjectName)
|
||||
rawImageGcsPath, err := UploadToBucket(client, ui, artifact, p.config.Bucket, p.config.GCSObjectName)
|
||||
if err != nil {
|
||||
return nil, p.config.KeepOriginalImage, err
|
||||
}
|
||||
|
||||
gceImageArtifact, err := CreateGceImage(p.config.AccountFile, ui, p.config.ProjectId, rawImageGcsPath, p.config.ImageName, p.config.ImageDescription, p.config.ImageFamily, p.config.ImageLabels, p.config.ImageGuestOsFeatures)
|
||||
gceImageArtifact, err := CreateGceImage(client, ui, p.config.ProjectId, rawImageGcsPath, p.config.ImageName, p.config.ImageDescription, p.config.ImageFamily, p.config.ImageLabels, p.config.ImageGuestOsFeatures)
|
||||
if err != nil {
|
||||
return nil, p.config.KeepOriginalImage, err
|
||||
}
|
||||
|
||||
if !p.config.SkipClean {
|
||||
err = DeleteFromBucket(p.config.AccountFile, ui, p.config.Bucket, p.config.GCSObjectName)
|
||||
err = DeleteFromBucket(client, ui, p.config.Bucket, p.config.GCSObjectName)
|
||||
if err != nil {
|
||||
return nil, p.config.KeepOriginalImage, err
|
||||
}
|
||||
@@ -128,24 +132,7 @@ func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (pac
|
||||
return gceImageArtifact, p.config.KeepOriginalImage, nil
|
||||
}
|
||||
|
||||
func UploadToBucket(accountFile string, ui packer.Ui, artifact packer.Artifact, bucket string, gcsObjectName string) (string, error) {
|
||||
var client *http.Client
|
||||
var account googlecompute.AccountFile
|
||||
|
||||
err := googlecompute.ProcessAccountFile(&account, accountFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var DriverScopes = []string{"https://www.googleapis.com/auth/devstorage.full_control"}
|
||||
conf := jwt.Config{
|
||||
Email: account.ClientEmail,
|
||||
PrivateKey: []byte(account.PrivateKey),
|
||||
Scopes: DriverScopes,
|
||||
TokenURL: "https://accounts.google.com/o/oauth2/token",
|
||||
}
|
||||
|
||||
client = conf.Client(oauth2.NoContext)
|
||||
func UploadToBucket(client *http.Client, ui packer.Ui, artifact packer.Artifact, bucket string, gcsObjectName string) (string, error) {
|
||||
service, err := storage.New(client)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -162,7 +149,7 @@ func UploadToBucket(accountFile string, ui packer.Ui, artifact packer.Artifact,
|
||||
}
|
||||
|
||||
if source == "" {
|
||||
return "", fmt.Errorf("No tar.gz file found in list of articats")
|
||||
return "", fmt.Errorf("No tar.gz file found in list of artifacts")
|
||||
}
|
||||
|
||||
artifactFile, err := os.Open(source)
|
||||
@@ -178,28 +165,10 @@ func UploadToBucket(accountFile string, ui packer.Ui, artifact packer.Artifact,
|
||||
return "", err
|
||||
}
|
||||
|
||||
return "https://storage.googleapis.com/" + bucket + "/" + gcsObjectName, nil
|
||||
return storageObject.SelfLink, nil
|
||||
}
|
||||
|
||||
func CreateGceImage(accountFile string, ui packer.Ui, project string, rawImageURL string, imageName string, imageDescription string, imageFamily string, imageLabels map[string]string, imageGuestOsFeatures []string) (packer.Artifact, error) {
|
||||
var client *http.Client
|
||||
var account googlecompute.AccountFile
|
||||
|
||||
err := googlecompute.ProcessAccountFile(&account, accountFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var DriverScopes = []string{"https://www.googleapis.com/auth/compute", "https://www.googleapis.com/auth/devstorage.full_control"}
|
||||
conf := jwt.Config{
|
||||
Email: account.ClientEmail,
|
||||
PrivateKey: []byte(account.PrivateKey),
|
||||
Scopes: DriverScopes,
|
||||
TokenURL: "https://accounts.google.com/o/oauth2/token",
|
||||
}
|
||||
|
||||
client = conf.Client(oauth2.NoContext)
|
||||
|
||||
func CreateGceImage(client *http.Client, ui packer.Ui, project string, rawImageURL string, imageName string, imageDescription string, imageFamily string, imageLabels map[string]string, imageGuestOsFeatures []string) (packer.Artifact, error) {
|
||||
service, err := compute.New(client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -253,24 +222,7 @@ func CreateGceImage(accountFile string, ui packer.Ui, project string, rawImageUR
|
||||
return &Artifact{paths: []string{op.TargetLink}}, nil
|
||||
}
|
||||
|
||||
func DeleteFromBucket(accountFile string, ui packer.Ui, bucket string, gcsObjectName string) error {
|
||||
var client *http.Client
|
||||
var account googlecompute.AccountFile
|
||||
|
||||
err := googlecompute.ProcessAccountFile(&account, accountFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var DriverScopes = []string{"https://www.googleapis.com/auth/devstorage.full_control"}
|
||||
conf := jwt.Config{
|
||||
Email: account.ClientEmail,
|
||||
PrivateKey: []byte(account.PrivateKey),
|
||||
Scopes: DriverScopes,
|
||||
TokenURL: "https://accounts.google.com/o/oauth2/token",
|
||||
}
|
||||
|
||||
client = conf.Client(oauth2.NoContext)
|
||||
func DeleteFromBucket(client *http.Client, ui packer.Ui, bucket string, gcsObjectName string) error {
|
||||
service, err := storage.New(client)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -10,12 +10,13 @@ type ArtifactFile struct {
|
||||
}
|
||||
|
||||
type Artifact struct {
|
||||
BuildName string `json:"name"`
|
||||
BuilderType string `json:"builder_type"`
|
||||
BuildTime int64 `json:"build_time"`
|
||||
ArtifactFiles []ArtifactFile `json:"files"`
|
||||
ArtifactId string `json:"artifact_id"`
|
||||
PackerRunUUID string `json:"packer_run_uuid"`
|
||||
BuildName string `json:"name"`
|
||||
BuilderType string `json:"builder_type"`
|
||||
BuildTime int64 `json:"build_time"`
|
||||
ArtifactFiles []ArtifactFile `json:"files"`
|
||||
ArtifactId string `json:"artifact_id"`
|
||||
PackerRunUUID string `json:"packer_run_uuid"`
|
||||
CustomData map[string]string `json:"custom_data"`
|
||||
}
|
||||
|
||||
func (a *Artifact) BuilderId() string {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user