Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b23c8addf | |||
| aa658e982b | |||
| d29c5cd231 | |||
| f5850199ba | |||
| dbf978f8d1 | |||
| 4c537105ef | |||
| 1bad19f58b | |||
| 06b5b4304d | |||
| 5315aaba0d | |||
| 3a899892dc | |||
| ef0a077168 | |||
| de0c59cc18 | |||
| 53ab6b9607 | |||
| 6f8afdeaf6 | |||
| 1f63abc622 | |||
| 46a2b81a1d | |||
| 7fe487cd4f | |||
| 06b83d148b | |||
| 9035b51165 | |||
| a775a58129 | |||
| 694275c7cd | |||
| 27a2270593 | |||
| 8c30dc78ac | |||
| c4cf8553c8 | |||
| a511b603e2 | |||
| 81edbb4173 | |||
| 147eea955e | |||
| 67c71aa836 | |||
| 535d2a7cfc | |||
| 5736dbd3aa | |||
| b57fa3cbdb | |||
| a59bbd892d | |||
| fb8b82fca5 | |||
| e26f7e87e0 | |||
| 25e7b90a96 | |||
| c0ef20bb80 | |||
| 6746ffc133 | |||
| 682cd97991 | |||
| 8dd3850482 | |||
| e89f182b33 | |||
| 25a310cf45 | |||
| bf1a2f4899 | |||
| 250077a0ff | |||
| 89a29e7b21 | |||
| 13a4c34bc5 | |||
| 175408dcb2 | |||
| 3043eddfaf | |||
| 0058929186 | |||
| fc62adfe5d | |||
| cc8d0ad2eb | |||
| 45c88e2aea | |||
| 290b9ef836 | |||
| a1c96d3b97 | |||
| 0271d9a15e | |||
| ede100bb7b | |||
| 2da53f54f4 | |||
| 5477d7166b | |||
| ab5b094d73 | |||
| eb8f6c9769 | |||
| f2e909356e | |||
| 952b2c921f | |||
| a613446205 | |||
| 907ca4d951 | |||
| 822c52c46f | |||
| 082dc6c14d | |||
| 797255fca6 | |||
| 05e1654eb8 | |||
| 12547d3203 | |||
| 8024dde44d | |||
| 0b16317401 | |||
| 06515fd195 | |||
| 47ee7ca977 | |||
| b6a6c8571b |
@@ -1,3 +1,44 @@
|
||||
## 0.3.7 (September 9, 2013)
|
||||
|
||||
BACKWARDS INCOMPATIBILITIES:
|
||||
|
||||
* The "event_delay" option for the DigitalOcean builder is now gone.
|
||||
The builder automatically waits for events to go away. Run your templates
|
||||
through `packer fix` to get rid of these.
|
||||
|
||||
FEATURES:
|
||||
|
||||
* **NEW PROVISIONER:** `puppet-masterless`. You can now provision with
|
||||
a masterless Puppet setup. [GH-234]
|
||||
* New globally available template function: `uuid`. Generates a new random
|
||||
UUID.
|
||||
* New globally available template function: `isotime`. Generates the
|
||||
current time in ISO standard format.
|
||||
* New Amazon template function: `clean_ami_name`. Substitutes '-' for
|
||||
characters that are illegal to use in an AMI name.
|
||||
|
||||
IMPROVEMENTS:
|
||||
|
||||
* builder/amazon/all: Ability to specify the format of the temporary
|
||||
keypair created. [GH-389]
|
||||
* builder/amazon/all: Support the NoDevice flag for block mappings. [GH-396]
|
||||
* builder/digitalocean: Retry on any pending event errors.
|
||||
* builder/openstack: Can now specify a project. [GH-382]
|
||||
* builder/virtualbox: Can now attach hard drive over SATA. [GH-391]
|
||||
* provisioner/file: Can now upload directories. [GH-251]
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
* core: Detect if SCP is not enabled on the other side. [GH-386]
|
||||
* builder/amazon/all: When copying AMI to multiple regions, copy
|
||||
the metadata (tags and attributes) as well. [GH-388]
|
||||
* builder/amazon/all: Fix panic case where eventually consistent
|
||||
instance state caused an index out of bounds.
|
||||
* builder/virtualbox: The `vm_name` setting now properly sets the OVF
|
||||
name of the output. [GH-401]
|
||||
* builder/vmware: Autoanswer VMware dialogs. [GH-393]
|
||||
* command/inspect: Fix weird output for default values for optional vars.
|
||||
|
||||
## 0.3.6 (September 2, 2013)
|
||||
|
||||
FEATURES:
|
||||
|
||||
@@ -90,3 +90,6 @@ $ bin/packer
|
||||
You can run tests by typing `make test`.
|
||||
|
||||
This will run tests for Packer core along with all the core builders and commands and such that come with Packer.
|
||||
|
||||
If you make any changes to the code, run `make format` in order to automatically
|
||||
format the code according to Go standards.
|
||||
|
||||
@@ -53,6 +53,7 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
return err
|
||||
}
|
||||
b.config.tpl.UserVars = b.config.PackerUserVars
|
||||
b.config.tpl.Funcs(awscommon.TemplateFuncs)
|
||||
|
||||
// Defaults
|
||||
if b.config.ChrootMounts == nil {
|
||||
@@ -189,15 +190,14 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
&StepEarlyCleanup{},
|
||||
&StepSnapshot{},
|
||||
&StepRegisterAMI{},
|
||||
&awscommon.StepAMIRegionCopy{
|
||||
Regions: b.config.AMIRegions,
|
||||
},
|
||||
&awscommon.StepModifyAMIAttributes{
|
||||
Description: b.config.AMIDescription,
|
||||
Users: b.config.AMIUsers,
|
||||
Groups: b.config.AMIGroups,
|
||||
},
|
||||
&awscommon.StepAMIRegionCopy{
|
||||
Regions: b.config.AMIRegions,
|
||||
Tags: b.config.AMITags,
|
||||
},
|
||||
&awscommon.StepCreateTags{
|
||||
Tags: b.config.AMITags,
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/aws"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
@@ -51,9 +52,10 @@ func (a *Artifact) String() string {
|
||||
func (a *Artifact) Destroy() error {
|
||||
errors := make([]error, 0)
|
||||
|
||||
for _, imageId := range a.Amis {
|
||||
log.Printf("Deregistering image ID: %s", imageId)
|
||||
if _, err := a.Conn.DeregisterImage(imageId); err != nil {
|
||||
for region, imageId := range a.Amis {
|
||||
log.Printf("Deregistering image ID (%s) from region (%s)", imageId, region)
|
||||
regionconn := ec2.New(a.Conn.Auth, aws.Regions[region])
|
||||
if _, err := regionconn.DeregisterImage(imageId); err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,11 +13,12 @@ type BlockDevice struct {
|
||||
VolumeSize int64 `mapstructure:"volume_size"`
|
||||
DeleteOnTermination bool `mapstructure:"delete_on_termination"`
|
||||
IOPS int64 `mapstructure:"iops"`
|
||||
NoDevice bool `mapstructure:"no_device"`
|
||||
}
|
||||
|
||||
type BlockDevices struct {
|
||||
AMIMappings []BlockDevice `mapstructure:"ami_block_device_mappings,squash"`
|
||||
LaunchMappings []BlockDevice `mapstructure:"launch_block_device_mappings,squash"`
|
||||
AMIMappings []BlockDevice `mapstructure:"ami_block_device_mappings"`
|
||||
LaunchMappings []BlockDevice `mapstructure:"launch_block_device_mappings"`
|
||||
}
|
||||
|
||||
func buildBlockDevices(b []BlockDevice) []ec2.BlockDeviceMapping {
|
||||
@@ -32,6 +33,7 @@ func buildBlockDevices(b []BlockDevice) []ec2.BlockDeviceMapping {
|
||||
VolumeSize: blockDevice.VolumeSize,
|
||||
DeleteOnTermination: blockDevice.DeleteOnTermination,
|
||||
IOPS: blockDevice.IOPS,
|
||||
NoDevice: blockDevice.NoDevice,
|
||||
})
|
||||
}
|
||||
return blockDevices
|
||||
|
||||
@@ -40,6 +40,12 @@ func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) StateRefreshFunc {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
if len(resp.Reservations) == 0 || len(resp.Reservations[0].Instances) == 0 {
|
||||
// Sometimes AWS just has consistency issues and doesn't see
|
||||
// our instance yet. Return an empty state.
|
||||
return nil, "", nil
|
||||
}
|
||||
|
||||
i = &resp.Reservations[0].Instances[0]
|
||||
return i, i.State.Name, nil
|
||||
}
|
||||
@@ -57,27 +63,31 @@ func WaitForState(conf *StateChangeConf) (i interface{}, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
if currentState == conf.Target {
|
||||
return
|
||||
}
|
||||
|
||||
if conf.StepState != nil {
|
||||
if _, ok := conf.StepState.GetOk(multistep.StateCancelled); ok {
|
||||
return nil, errors.New("interrupted")
|
||||
// Check states only if we were able to refresh to an instance
|
||||
// that exists.
|
||||
if i != nil {
|
||||
if currentState == conf.Target {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, allowed := range conf.Pending {
|
||||
if currentState == allowed {
|
||||
found = true
|
||||
break
|
||||
if conf.StepState != nil {
|
||||
if _, ok := conf.StepState.GetOk(multistep.StateCancelled); ok {
|
||||
return nil, errors.New("interrupted")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
fmt.Errorf("unexpected state '%s', wanted target '%s'", currentState, conf.Target)
|
||||
return
|
||||
found := false
|
||||
for _, allowed := range conf.Pending {
|
||||
if currentState == allowed {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
fmt.Errorf("unexpected state '%s', wanted target '%s'", currentState, conf.Target)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
@@ -11,17 +11,18 @@ import (
|
||||
// RunConfig contains configuration for running an instance from a source
|
||||
// AMI and details on how to access that launched image.
|
||||
type RunConfig struct {
|
||||
SourceAmi string `mapstructure:"source_ami"`
|
||||
IamInstanceProfile string `mapstructure:"iam_instance_profile"`
|
||||
InstanceType string `mapstructure:"instance_type"`
|
||||
UserData string `mapstructure:"user_data"`
|
||||
UserDataFile string `mapstructure:"user_data_file"`
|
||||
RawSSHTimeout string `mapstructure:"ssh_timeout"`
|
||||
SSHUsername string `mapstructure:"ssh_username"`
|
||||
SSHPort int `mapstructure:"ssh_port"`
|
||||
SecurityGroupId string `mapstructure:"security_group_id"`
|
||||
SubnetId string `mapstructure:"subnet_id"`
|
||||
VpcId string `mapstructure:"vpc_id"`
|
||||
SourceAmi string `mapstructure:"source_ami"`
|
||||
IamInstanceProfile string `mapstructure:"iam_instance_profile"`
|
||||
InstanceType string `mapstructure:"instance_type"`
|
||||
UserData string `mapstructure:"user_data"`
|
||||
UserDataFile string `mapstructure:"user_data_file"`
|
||||
RawSSHTimeout string `mapstructure:"ssh_timeout"`
|
||||
SSHUsername string `mapstructure:"ssh_username"`
|
||||
SSHPort int `mapstructure:"ssh_port"`
|
||||
SecurityGroupId string `mapstructure:"security_group_id"`
|
||||
SubnetId string `mapstructure:"subnet_id"`
|
||||
TemporaryKeyPairName string `mapstructure:"temporary_key_pair_name"`
|
||||
VpcId string `mapstructure:"vpc_id"`
|
||||
|
||||
// Unexported fields that are calculated from others
|
||||
sshTimeout time.Duration
|
||||
@@ -45,6 +46,10 @@ func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error {
|
||||
c.RawSSHTimeout = "1m"
|
||||
}
|
||||
|
||||
if c.TemporaryKeyPairName == "" {
|
||||
c.TemporaryKeyPairName = "packer {{uuid}}"
|
||||
}
|
||||
|
||||
// Validation
|
||||
var err error
|
||||
errs := make([]error, 0)
|
||||
@@ -69,14 +74,15 @@ func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error {
|
||||
}
|
||||
|
||||
templates := map[string]*string{
|
||||
"iam_instance_profile": &c.IamInstanceProfile,
|
||||
"instance_type": &c.InstanceType,
|
||||
"ssh_timeout": &c.RawSSHTimeout,
|
||||
"security_group_id": &c.SecurityGroupId,
|
||||
"ssh_username": &c.SSHUsername,
|
||||
"source_ami": &c.SourceAmi,
|
||||
"subnet_id": &c.SubnetId,
|
||||
"vpc_id": &c.VpcId,
|
||||
"iam_instance_profile": &c.IamInstanceProfile,
|
||||
"instance_type": &c.InstanceType,
|
||||
"ssh_timeout": &c.RawSSHTimeout,
|
||||
"security_group_id": &c.SecurityGroupId,
|
||||
"ssh_username": &c.SSHUsername,
|
||||
"source_ami": &c.SourceAmi,
|
||||
"subnet_id": &c.SubnetId,
|
||||
"temporary_key_pair_name": &c.TemporaryKeyPairName,
|
||||
"vpc_id": &c.VpcId,
|
||||
}
|
||||
|
||||
for n, ptr := range templates {
|
||||
|
||||
@@ -126,3 +126,15 @@ func TestRunConfigPrepare_UserDataFile(t *testing.T) {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigPrepare_TemporaryKeyPairName(t *testing.T) {
|
||||
c := testConfig()
|
||||
c.TemporaryKeyPairName = ""
|
||||
if err := c.Prepare(nil); len(err) != 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if c.TemporaryKeyPairName == "" {
|
||||
t.Fatal("keypair empty")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
type StepAMIRegionCopy struct {
|
||||
Regions []string
|
||||
Tags map[string]string
|
||||
}
|
||||
|
||||
func (s *StepAMIRegionCopy) Run(state multistep.StateBag) multistep.StepAction {
|
||||
@@ -49,25 +48,6 @@ func (s *StepAMIRegionCopy) Run(state multistep.StateBag) multistep.StepAction {
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Need to re-apply Tags since they are not copied with the AMI
|
||||
if len(s.Tags) > 0 {
|
||||
ui.Say(fmt.Sprintf("Adding tags to AMI (%s)...", resp.ImageId))
|
||||
|
||||
var ec2Tags []ec2.Tag
|
||||
for key, value := range s.Tags {
|
||||
ui.Message(fmt.Sprintf("Adding tag: \"%s\": \"%s\"", key, value))
|
||||
ec2Tags = append(ec2Tags, ec2.Tag{key, value})
|
||||
}
|
||||
|
||||
_, err := regionconn.CreateTags([]string{resp.ImageId}, ec2Tags)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error adding tags to AMI (%s): %s", resp.ImageId, err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
amis[region] = resp.ImageId
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/aws"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
@@ -15,23 +16,25 @@ func (s *StepCreateTags) Run(state multistep.StateBag) multistep.StepAction {
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
amis := state.Get("amis").(map[string]string)
|
||||
ami := amis[ec2conn.Region.Name]
|
||||
|
||||
if len(s.Tags) > 0 {
|
||||
ui.Say(fmt.Sprintf("Adding tags to AMI (%s)...", ami))
|
||||
for region, ami := range amis {
|
||||
ui.Say(fmt.Sprintf("Adding tags to AMI (%s)...", ami))
|
||||
|
||||
var ec2Tags []ec2.Tag
|
||||
for key, value := range s.Tags {
|
||||
ui.Message(fmt.Sprintf("Adding tag: \"%s\": \"%s\"", key, value))
|
||||
ec2Tags = append(ec2Tags, ec2.Tag{key, value})
|
||||
}
|
||||
var ec2Tags []ec2.Tag
|
||||
for key, value := range s.Tags {
|
||||
ui.Message(fmt.Sprintf("Adding tag: \"%s\": \"%s\"", key, value))
|
||||
ec2Tags = append(ec2Tags, ec2.Tag{key, value})
|
||||
}
|
||||
|
||||
_, err := ec2conn.CreateTags([]string{ami}, ec2Tags)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error adding tags to AMI (%s): %s", ami, err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
regionconn := ec2.New(ec2conn.Auth, aws.Regions[region])
|
||||
_, err := regionconn.CreateTags([]string{ami}, ec2Tags)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error adding tags to AMI (%s): %s", ami, err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"cgl.tideland.biz/identifier"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
)
|
||||
@@ -15,6 +12,7 @@ import (
|
||||
type StepKeyPair struct {
|
||||
Debug bool
|
||||
DebugKeyPath string
|
||||
KeyPairName string
|
||||
|
||||
keyName string
|
||||
}
|
||||
@@ -23,20 +21,18 @@ func (s *StepKeyPair) Run(state multistep.StateBag) multistep.StepAction {
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
ui.Say("Creating temporary keypair for this instance...")
|
||||
keyName := fmt.Sprintf("packer %s", hex.EncodeToString(identifier.NewUUID().Raw()))
|
||||
log.Printf("temporary keypair name: %s", keyName)
|
||||
keyResp, err := ec2conn.CreateKeyPair(keyName)
|
||||
ui.Say(fmt.Sprintf("Creating temporary keypair: %s", s.KeyPairName))
|
||||
keyResp, err := ec2conn.CreateKeyPair(s.KeyPairName)
|
||||
if err != nil {
|
||||
state.Put("error", fmt.Errorf("Error creating temporary keypair: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Set the keyname so we know to delete it later
|
||||
s.keyName = keyName
|
||||
s.keyName = s.KeyPairName
|
||||
|
||||
// Set some state data for use in future steps
|
||||
state.Put("keyPair", keyName)
|
||||
state.Put("keyPair", s.keyName)
|
||||
state.Put("privateKey", keyResp.KeyMaterial)
|
||||
|
||||
// If we're in debug mode, output the private key to the working
|
||||
|
||||
@@ -2,6 +2,7 @@ package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/aws"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
@@ -18,7 +19,6 @@ func (s *StepModifyAMIAttributes) Run(state multistep.StateBag) multistep.StepAc
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
amis := state.Get("amis").(map[string]string)
|
||||
ami := amis[ec2conn.Region.Name]
|
||||
|
||||
// Determine if there is any work to do.
|
||||
valid := false
|
||||
@@ -59,15 +59,18 @@ func (s *StepModifyAMIAttributes) Run(state multistep.StateBag) multistep.StepAc
|
||||
}
|
||||
}
|
||||
|
||||
ui.Say("Modifying AMI attributes...")
|
||||
for name, opts := range options {
|
||||
ui.Message(fmt.Sprintf("Modifying: %s", name))
|
||||
_, err := ec2conn.ModifyImageAttribute(ami, opts)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error modify AMI attributes: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
for region, ami := range amis {
|
||||
ui.Say(fmt.Sprintf("Modifying attributes on AMI (%s)...", ami))
|
||||
regionconn := ec2.New(ec2conn.Auth, aws.Regions[region])
|
||||
for name, opts := range options {
|
||||
ui.Message(fmt.Sprintf("Modifying: %s", name))
|
||||
_, err := regionconn.ModifyImageAttribute(ami, opts)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error modify AMI attributes: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
func isalphanumeric(b byte) bool {
|
||||
if '0' <= b && b <= '9' {
|
||||
return true
|
||||
}
|
||||
if 'a' <= b && b <= 'z' {
|
||||
return true
|
||||
}
|
||||
if 'A' <= b && b <= 'Z' {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Clean up AMI name by replacing invalid characters with "-"
|
||||
func templateCleanAMIName(s string) string {
|
||||
allowed := []byte{'(', ')', ',', '/', '-', '_'}
|
||||
b := []byte(s)
|
||||
newb := make([]byte, len(b))
|
||||
for i, c := range b {
|
||||
if isalphanumeric(c) || bytes.IndexByte(allowed, c) != -1 {
|
||||
newb[i] = c
|
||||
} else {
|
||||
newb[i] = '-'
|
||||
}
|
||||
}
|
||||
return string(newb[:])
|
||||
}
|
||||
|
||||
var TemplateFuncs = template.FuncMap{
|
||||
"clean_ami_name": templateCleanAMIName,
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAMITemplatePrepare_clean(t *testing.T) {
|
||||
origName := "AMZamz09(),/-_:&^$%"
|
||||
expected := "AMZamz09(),/-_-----"
|
||||
|
||||
name := templateCleanAMIName(origName)
|
||||
|
||||
if name != expected {
|
||||
t.Fatalf("template names do not match: expected %s got %s\n", expected, name)
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
return err
|
||||
}
|
||||
b.config.tpl.UserVars = b.config.PackerUserVars
|
||||
b.config.tpl.Funcs(awscommon.TemplateFuncs)
|
||||
|
||||
// Accumulate any errors
|
||||
errs := common.CheckUnusedConfig(md)
|
||||
@@ -84,6 +85,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
&awscommon.StepKeyPair{
|
||||
Debug: b.config.PackerDebug,
|
||||
DebugKeyPath: fmt.Sprintf("ec2_%s.pem", b.config.PackerBuildName),
|
||||
KeyPairName: b.config.TemporaryKeyPairName,
|
||||
},
|
||||
&awscommon.StepSecurityGroup{
|
||||
SecurityGroupId: b.config.SecurityGroupId,
|
||||
@@ -109,15 +111,14 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
&common.StepProvision{},
|
||||
&stepStopInstance{},
|
||||
&stepCreateAMI{},
|
||||
&awscommon.StepAMIRegionCopy{
|
||||
Regions: b.config.AMIRegions,
|
||||
},
|
||||
&awscommon.StepModifyAMIAttributes{
|
||||
Description: b.config.AMIDescription,
|
||||
Users: b.config.AMIUsers,
|
||||
Groups: b.config.AMIGroups,
|
||||
},
|
||||
&awscommon.StepAMIRegionCopy{
|
||||
Regions: b.config.AMIRegions,
|
||||
Tags: b.config.AMITags,
|
||||
},
|
||||
&awscommon.StepCreateTags{
|
||||
Tags: b.config.AMITags,
|
||||
},
|
||||
|
||||
@@ -56,6 +56,7 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
return err
|
||||
}
|
||||
b.config.tpl.UserVars = b.config.PackerUserVars
|
||||
b.config.tpl.Funcs(awscommon.TemplateFuncs)
|
||||
|
||||
if b.config.BundleDestination == "" {
|
||||
b.config.BundleDestination = "/tmp"
|
||||
@@ -187,6 +188,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
&awscommon.StepKeyPair{
|
||||
Debug: b.config.PackerDebug,
|
||||
DebugKeyPath: fmt.Sprintf("ec2_%s.pem", b.config.PackerBuildName),
|
||||
KeyPairName: b.config.TemporaryKeyPairName,
|
||||
},
|
||||
&awscommon.StepSecurityGroup{
|
||||
SecurityGroupId: b.config.SecurityGroupId,
|
||||
@@ -214,16 +216,15 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
&StepBundleVolume{},
|
||||
&StepUploadBundle{},
|
||||
&StepRegisterAMI{},
|
||||
&awscommon.StepAMIRegionCopy{
|
||||
Regions: b.config.AMIRegions,
|
||||
},
|
||||
&awscommon.StepModifyAMIAttributes{
|
||||
Description: b.config.AMIDescription,
|
||||
Users: b.config.AMIUsers,
|
||||
Groups: b.config.AMIGroups,
|
||||
ProductCodes: b.config.AMIProductCodes,
|
||||
},
|
||||
&awscommon.StepAMIRegionCopy{
|
||||
Regions: b.config.AMIRegions,
|
||||
Tags: b.config.AMITags,
|
||||
},
|
||||
&awscommon.StepCreateTags{
|
||||
Tags: b.config.AMITags,
|
||||
},
|
||||
|
||||
+46
-34
@@ -14,6 +14,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const DIGITALOCEAN_API_URL = "https://api.digitalocean.com"
|
||||
@@ -191,46 +192,57 @@ func NewRequest(d DigitalOceanClient, path string, params url.Values) (map[strin
|
||||
|
||||
url := fmt.Sprintf("%s/%s?%s", DIGITALOCEAN_API_URL, path, params.Encode())
|
||||
|
||||
var decodedResponse map[string]interface{}
|
||||
|
||||
// Do some basic scrubbing so sensitive information doesn't appear in logs
|
||||
scrubbedUrl := strings.Replace(url, d.ClientID, "CLIENT_ID", -1)
|
||||
scrubbedUrl = strings.Replace(scrubbedUrl, d.APIKey, "API_KEY", -1)
|
||||
log.Printf("sending new request to digitalocean: %s", scrubbedUrl)
|
||||
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return decodedResponse, err
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return decodedResponse, err
|
||||
}
|
||||
|
||||
log.Printf("response from digitalocean: %s", body)
|
||||
|
||||
err = json.Unmarshal(body, &decodedResponse)
|
||||
|
||||
// Check for bad JSON
|
||||
if err != nil {
|
||||
err = errors.New(fmt.Sprintf("Failed to decode JSON response (HTTP %v) from DigitalOcean: %s",
|
||||
resp.StatusCode, body))
|
||||
return decodedResponse, err
|
||||
}
|
||||
|
||||
// Check for errors sent by digitalocean
|
||||
status := decodedResponse["status"]
|
||||
if status != "OK" {
|
||||
// Get the actual error message if there is one
|
||||
if status == "ERROR" {
|
||||
status = decodedResponse["error_message"]
|
||||
var lastErr error
|
||||
for attempts := 1; attempts < 10; attempts++ {
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = errors.New(fmt.Sprintf("Received bad response (HTTP %v) from DigitalOcean: %s", resp.StatusCode, status))
|
||||
return decodedResponse, err
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Printf("response from digitalocean: %s", body)
|
||||
|
||||
var decodedResponse map[string]interface{}
|
||||
err = json.Unmarshal(body, &decodedResponse)
|
||||
if err != nil {
|
||||
err = errors.New(fmt.Sprintf("Failed to decode JSON response (HTTP %v) from DigitalOcean: %s",
|
||||
resp.StatusCode, body))
|
||||
return decodedResponse, err
|
||||
}
|
||||
|
||||
// Check for errors sent by digitalocean
|
||||
status := decodedResponse["status"].(string)
|
||||
if status == "OK" {
|
||||
return decodedResponse, nil
|
||||
}
|
||||
|
||||
if status == "ERROR" {
|
||||
status = decodedResponse["error_message"].(string)
|
||||
}
|
||||
|
||||
lastErr = errors.New(fmt.Sprintf("Received error from DigitalOcean (%d): %s",
|
||||
resp.StatusCode, status))
|
||||
log.Println(lastErr)
|
||||
if strings.Contains(status, "a pending event") {
|
||||
// Retry, DigitalOcean sends these dumb "pending event"
|
||||
// errors all the time.
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
// Some other kind of error. Just return.
|
||||
return decodedResponse, lastErr
|
||||
}
|
||||
|
||||
return decodedResponse, nil
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
@@ -34,13 +34,11 @@ type config struct {
|
||||
SSHPort uint `mapstructure:"ssh_port"`
|
||||
|
||||
RawSSHTimeout string `mapstructure:"ssh_timeout"`
|
||||
RawEventDelay string `mapstructure:"event_delay"`
|
||||
RawStateTimeout string `mapstructure:"state_timeout"`
|
||||
|
||||
// These are unexported since they're set by other fields
|
||||
// being set.
|
||||
sshTimeout time.Duration
|
||||
eventDelay time.Duration
|
||||
stateTimeout time.Duration
|
||||
|
||||
tpl *packer.ConfigTemplate
|
||||
@@ -113,12 +111,6 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
b.config.RawSSHTimeout = "1m"
|
||||
}
|
||||
|
||||
if b.config.RawEventDelay == "" {
|
||||
// Default to 5 second delays after creating events
|
||||
// to allow DO to process
|
||||
b.config.RawEventDelay = "5s"
|
||||
}
|
||||
|
||||
if b.config.RawStateTimeout == "" {
|
||||
// Default to 6 minute timeouts waiting for
|
||||
// desired state. i.e waiting for droplet to become active
|
||||
@@ -131,7 +123,6 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
"snapshot_name": &b.config.SnapshotName,
|
||||
"ssh_username": &b.config.SSHUsername,
|
||||
"ssh_timeout": &b.config.RawSSHTimeout,
|
||||
"event_delay": &b.config.RawEventDelay,
|
||||
"state_timeout": &b.config.RawStateTimeout,
|
||||
}
|
||||
|
||||
@@ -162,13 +153,6 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
}
|
||||
b.config.sshTimeout = sshTimeout
|
||||
|
||||
eventDelay, err := time.ParseDuration(b.config.RawEventDelay)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Failed parsing event_delay: %s", err))
|
||||
}
|
||||
b.config.eventDelay = eventDelay
|
||||
|
||||
stateTimeout, err := time.ParseDuration(b.config.RawStateTimeout)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
|
||||
@@ -258,38 +258,6 @@ func TestBuilderPrepare_SSHTimeout(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_EventDelay(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test default
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.RawEventDelay != "5s" {
|
||||
t.Errorf("invalid: %d", b.config.RawEventDelay)
|
||||
}
|
||||
|
||||
// Test set
|
||||
config["event_delay"] = "10s"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
// Test bad
|
||||
config["event_delay"] = "tubes"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_StateTimeout(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
type stepCreateDroplet struct {
|
||||
@@ -56,22 +54,7 @@ func (s *stepCreateDroplet) Cleanup(state multistep.StateBag) {
|
||||
// Destroy the droplet we just created
|
||||
ui.Say("Destroying droplet...")
|
||||
|
||||
// Sleep arbitrarily before sending destroy request
|
||||
// Otherwise we get "pending event" errors, even though there isn't
|
||||
// one.
|
||||
log.Printf("Sleeping for %v, event_delay", c.RawEventDelay)
|
||||
time.Sleep(c.eventDelay)
|
||||
|
||||
var err error
|
||||
for i := 0; i < 5; i++ {
|
||||
err = client.DestroyDroplet(s.dropletId)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
err := client.DestroyDroplet(s.dropletId)
|
||||
if err != nil {
|
||||
curlstr := fmt.Sprintf("curl '%v/droplets/%v/destroy?client_id=%v&api_key=%v'",
|
||||
DIGITALOCEAN_API_URL, s.dropletId, c.ClientID, c.APIKey)
|
||||
|
||||
@@ -16,7 +16,7 @@ func (s *stepDropletInfo) Run(state multistep.StateBag) multistep.StepAction {
|
||||
|
||||
ui.Say("Waiting for droplet to become active...")
|
||||
|
||||
err := waitForDropletState("active", dropletId, client, c)
|
||||
err := waitForDropletState("active", dropletId, client, c.stateTimeout)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for droplet to become active: %s", err)
|
||||
state.Put("error", err)
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
type stepPowerOff struct{}
|
||||
@@ -16,15 +15,22 @@ func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
dropletId := state.Get("droplet_id").(uint)
|
||||
|
||||
// Sleep arbitrarily before sending power off request
|
||||
// Otherwise we get "pending event" errors, even though there isn't
|
||||
// one.
|
||||
log.Printf("Sleeping for %v, event_delay", c.RawEventDelay)
|
||||
time.Sleep(c.eventDelay)
|
||||
_, status, err := client.DropletStatus(dropletId)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error checking droplet state: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Poweroff the droplet so it can be snapshot
|
||||
err := client.PowerOffDroplet(dropletId)
|
||||
if status == "off" {
|
||||
// Droplet is already off, don't do anything
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
// Pull the plug on the Droplet
|
||||
ui.Say("Forcefully shutting down Droplet...")
|
||||
err = client.PowerOffDroplet(dropletId)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error powering off droplet: %s", err)
|
||||
state.Put("error", err)
|
||||
@@ -33,13 +39,12 @@ func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction {
|
||||
}
|
||||
|
||||
log.Println("Waiting for poweroff event to complete...")
|
||||
|
||||
// This arbitrary sleep is because we can't wait for the state
|
||||
// of the droplet to be 'off', as stepShutdown should already
|
||||
// have accomplished that, and the state indicator is the same.
|
||||
// We just have to assume that this event will process quickly.
|
||||
log.Printf("Sleeping for %v, event_delay", c.RawEventDelay)
|
||||
time.Sleep(c.eventDelay)
|
||||
err = waitForDropletState("off", dropletId, client, c.stateTimeout)
|
||||
if err != nil {
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
@@ -12,33 +12,58 @@ type stepShutdown struct{}
|
||||
|
||||
func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction {
|
||||
client := state.Get("client").(*DigitalOceanClient)
|
||||
c := state.Get("config").(config)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
dropletId := state.Get("droplet_id").(uint)
|
||||
|
||||
// Sleep arbitrarily before sending the request
|
||||
// Otherwise we get "pending event" errors, even though there isn't
|
||||
// one.
|
||||
log.Printf("Sleeping for %v, event_delay", c.RawEventDelay)
|
||||
time.Sleep(c.eventDelay)
|
||||
|
||||
// Gracefully power off the droplet. We have to retry this a number
|
||||
// of times because sometimes it says it completed when it actually
|
||||
// did absolutely nothing (*ALAKAZAM!* magic!). We give up after
|
||||
// a pretty arbitrary amount of time.
|
||||
ui.Say("Gracefully shutting down droplet...")
|
||||
err := client.ShutdownDroplet(dropletId)
|
||||
|
||||
if err != nil {
|
||||
// If we get an error the first time, actually report it
|
||||
err := fmt.Errorf("Error shutting down droplet: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
ui.Say("Waiting for droplet to shutdown...")
|
||||
// A channel we use as a flag to end our goroutines
|
||||
done := make(chan struct{})
|
||||
shutdownRetryDone := make(chan struct{})
|
||||
|
||||
err = waitForDropletState("off", dropletId, client, c)
|
||||
// Make sure we wait for the shutdown retry goroutine to end
|
||||
// before moving on.
|
||||
defer func() {
|
||||
close(done)
|
||||
<-shutdownRetryDone
|
||||
}()
|
||||
|
||||
// Start a goroutine that just keeps trying to shut down the
|
||||
// droplet.
|
||||
go func() {
|
||||
defer close(shutdownRetryDone)
|
||||
|
||||
for attempts := 2; attempts > 0; attempts++ {
|
||||
log.Printf("ShutdownDroplet attempt #%d...", attempts)
|
||||
err := client.ShutdownDroplet(dropletId)
|
||||
if err != nil {
|
||||
log.Printf("Shutdown retry error: %s", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-time.After(20 * time.Second):
|
||||
// Retry!
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
err = waitForDropletState("off", dropletId, client, 2*time.Minute)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for droplet to become 'off': %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
log.Printf("Error waiting for graceful off: %s", err)
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
|
||||
@@ -26,7 +26,7 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
|
||||
}
|
||||
|
||||
ui.Say("Waiting for snapshot to complete...")
|
||||
err = waitForDropletState("active", dropletId, client, c)
|
||||
err = waitForDropletState("active", dropletId, client, c.stateTimeout)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for snapshot to complete: %s", err)
|
||||
state.Put("error", err)
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
package digitalocean
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// waitForState simply blocks until the droplet is in
|
||||
// a state we expect, while eventually timing out.
|
||||
func waitForDropletState(desiredState string, dropletId uint, client *DigitalOceanClient, c config) error {
|
||||
active := make(chan bool, 1)
|
||||
func waitForDropletState(desiredState string, dropletId uint, client *DigitalOceanClient, timeout time.Duration) error {
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
|
||||
result := make(chan error, 1)
|
||||
go func() {
|
||||
attempts := 0
|
||||
for {
|
||||
@@ -19,36 +21,35 @@ func waitForDropletState(desiredState string, dropletId uint, client *DigitalOce
|
||||
log.Printf("Checking droplet status... (attempt: %d)", attempts)
|
||||
_, status, err := client.DropletStatus(dropletId)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
break
|
||||
result <- err
|
||||
return
|
||||
}
|
||||
|
||||
if status == desiredState {
|
||||
break
|
||||
result <- nil
|
||||
return
|
||||
}
|
||||
|
||||
// Wait 3 seconds in between
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
|
||||
active <- true
|
||||
// Verify we shouldn't exit
|
||||
select {
|
||||
case <-done:
|
||||
// We finished, so just exit the goroutine
|
||||
return
|
||||
default:
|
||||
// Keep going
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
log.Printf("Waiting for up to %s for droplet to become %s", c.RawStateTimeout, desiredState)
|
||||
timeout := time.After(c.stateTimeout)
|
||||
|
||||
ActiveWaitLoop:
|
||||
for {
|
||||
select {
|
||||
case <-active:
|
||||
// We connected. Just break the loop.
|
||||
break ActiveWaitLoop
|
||||
case <-timeout:
|
||||
err := errors.New("Timeout while waiting to for droplet to become active")
|
||||
return err
|
||||
}
|
||||
log.Printf("Waiting for up to %d seconds for droplet to become %s", timeout, desiredState)
|
||||
select {
|
||||
case err := <-result:
|
||||
return err
|
||||
case <-time.After(timeout):
|
||||
err := fmt.Errorf("Timeout while waiting to for droplet to become '%s'", desiredState)
|
||||
return err
|
||||
}
|
||||
|
||||
// If we got this far, there were no errors
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
type AccessConfig struct {
|
||||
Username string `mapstructure:"username"`
|
||||
Password string `mapstructure:"password"`
|
||||
Project string `mapstructure:"project"`
|
||||
Provider string `mapstructure:"provider"`
|
||||
RawRegion string `mapstructure:"region"`
|
||||
}
|
||||
@@ -20,6 +21,7 @@ type AccessConfig struct {
|
||||
func (c *AccessConfig) Auth() (gophercloud.AccessProvider, error) {
|
||||
username := c.Username
|
||||
password := c.Password
|
||||
project := c.Project
|
||||
provider := c.Provider
|
||||
|
||||
if username == "" {
|
||||
@@ -28,6 +30,9 @@ func (c *AccessConfig) Auth() (gophercloud.AccessProvider, error) {
|
||||
if password == "" {
|
||||
password = os.Getenv("SDK_PASSWORD")
|
||||
}
|
||||
if project == "" {
|
||||
project = os.Getenv("SDK_PROJECT")
|
||||
}
|
||||
if provider == "" {
|
||||
provider = os.Getenv("SDK_PROVIDER")
|
||||
}
|
||||
@@ -38,6 +43,10 @@ func (c *AccessConfig) Auth() (gophercloud.AccessProvider, error) {
|
||||
AllowReauth: true,
|
||||
}
|
||||
|
||||
if project != "" {
|
||||
authoptions.TenantName = project
|
||||
}
|
||||
|
||||
return gophercloud.Authenticate(provider, authoptions)
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ type config struct {
|
||||
GuestAdditionsURL string `mapstructure:"guest_additions_url"`
|
||||
GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"`
|
||||
GuestOSType string `mapstructure:"guest_os_type"`
|
||||
HardDriveInterface string `mapstructure:"hard_drive_interface"`
|
||||
Headless bool `mapstructure:"headless"`
|
||||
HTTPDir string `mapstructure:"http_directory"`
|
||||
HTTPPortMin uint `mapstructure:"http_port_min"`
|
||||
@@ -89,6 +90,10 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
b.config.GuestAdditionsPath = "VBoxGuestAdditions.iso"
|
||||
}
|
||||
|
||||
if b.config.HardDriveInterface == "" {
|
||||
b.config.HardDriveInterface = "ide"
|
||||
}
|
||||
|
||||
if b.config.GuestOSType == "" {
|
||||
b.config.GuestOSType = "Other"
|
||||
}
|
||||
@@ -141,6 +146,7 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
templates := map[string]*string{
|
||||
"guest_additions_sha256": &b.config.GuestAdditionsSHA256,
|
||||
"guest_os_type": &b.config.GuestOSType,
|
||||
"hard_drive_interface": &b.config.HardDriveInterface,
|
||||
"http_directory": &b.config.HTTPDir,
|
||||
"iso_checksum": &b.config.ISOChecksum,
|
||||
"iso_checksum_type": &b.config.ISOChecksumType,
|
||||
@@ -209,6 +215,11 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
errs, errors.New("invalid format, only 'ovf' or 'ova' are allowed"))
|
||||
}
|
||||
|
||||
if b.config.HardDriveInterface != "ide" && b.config.HardDriveInterface != "sata" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("hard_drive_interface can only be ide or sata"))
|
||||
}
|
||||
|
||||
if b.config.HTTPPortMin > b.config.HTTPPortMax {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("http_port_min must be less than http_port_max"))
|
||||
|
||||
@@ -252,6 +252,38 @@ func TestBuilderPrepare_GuestAdditionsURL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_HardDriveInterface(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test a default boot_wait
|
||||
delete(config, "hard_drive_interface")
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if b.config.HardDriveInterface != "ide" {
|
||||
t.Fatalf("bad: %s", b.config.HardDriveInterface)
|
||||
}
|
||||
|
||||
// Test with a bad
|
||||
config["hard_drive_interface"] = "fake"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test with a good
|
||||
config["hard_drive_interface"] = "sata"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_HTTPPort(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
@@ -39,7 +39,9 @@ func (s *stepCreateDisk) Run(state multistep.StateBag) multistep.StepAction {
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Add the IDE controller so we can later attach the disk
|
||||
// Add the IDE controller so we can later attach the disk.
|
||||
// When the hard disk controller is not IDE, this device is still used
|
||||
// by VirtualBox to deliver the guest extensions.
|
||||
controllerName := "IDE Controller"
|
||||
err = driver.VBoxManage("storagectl", vmName, "--name", controllerName, "--add", "ide")
|
||||
if err != nil {
|
||||
@@ -49,6 +51,25 @@ func (s *stepCreateDisk) Run(state multistep.StateBag) multistep.StepAction {
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Add a SATA controller if we were asked to use SATA. We still attach
|
||||
// the IDE controller above because some other things (disks) require
|
||||
// that.
|
||||
if config.HardDriveInterface == "sata" {
|
||||
controllerName = "SATA Controller"
|
||||
command = []string{
|
||||
"storagectl", vmName,
|
||||
"--name", controllerName,
|
||||
"--add", "sata",
|
||||
"--sataportcount", "1",
|
||||
}
|
||||
if err := driver.VBoxManage(command...); err != nil {
|
||||
err := fmt.Errorf("Error creating disk controller: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
// Attach the disk to the controller
|
||||
command = []string{
|
||||
"storageattach", vmName,
|
||||
|
||||
@@ -50,7 +50,7 @@ func (s *stepExport) Run(state multistep.StateBag) multistep.StepAction {
|
||||
}
|
||||
|
||||
// Export the VM to an OVF
|
||||
outputPath := filepath.Join(config.OutputDir, "packer."+config.Format)
|
||||
outputPath := filepath.Join(config.OutputDir, vmName+"."+config.Format)
|
||||
|
||||
command = []string{
|
||||
"export",
|
||||
|
||||
@@ -88,6 +88,9 @@ func (stepCreateVMX) Run(state multistep.StateBag) multistep.StepAction {
|
||||
vmxData["floppy0.fileName"] = floppyPathRaw.(string)
|
||||
}
|
||||
|
||||
// Set this so that no dialogs ever appear from Packer.
|
||||
vmxData["msg.autoAnswer"] = "true"
|
||||
|
||||
vmxPath := filepath.Join(config.OutputDir, config.VMName+".vmx")
|
||||
if err := WriteVMX(vmxPath, vmxData); err != nil {
|
||||
err := fmt.Errorf("Error creating VMX file: %s", err)
|
||||
|
||||
@@ -84,7 +84,7 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
||||
}
|
||||
|
||||
padding := strings.Repeat(" ", max-len(k))
|
||||
output := fmt.Sprintf(" %s%s = %s", k, padding, v)
|
||||
output := fmt.Sprintf(" %s%s = %s", k, padding, v.Default)
|
||||
|
||||
ui.Machine("template-variable", k, v.Default, "0")
|
||||
ui.Say(output)
|
||||
|
||||
@@ -285,14 +285,16 @@ func (c *comm) scpSession(scpCommand string, f func(io.Writer, *bufio.Reader) er
|
||||
|
||||
// Start the sink mode on the other side
|
||||
// TODO(mitchellh): There are probably issues with shell escaping the path
|
||||
log.Println("Starting remote scp process: %s", scpCommand)
|
||||
log.Println("Starting remote scp process: ", scpCommand)
|
||||
if err := session.Start(scpCommand); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Call our callback that executes in the context of SCP
|
||||
// Call our callback that executes in the context of SCP. We ignore
|
||||
// EOF errors if they occur because it usually means that SCP prematurely
|
||||
// ended on the other side.
|
||||
log.Println("Started SCP session, beginning transfers...")
|
||||
if err := f(stdinW, stdoutR); err != nil {
|
||||
if err := f(stdinW, stdoutR); err != nil && err != io.EOF {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ const defaultConfig = `
|
||||
"provisioners": {
|
||||
"chef-solo": "packer-provisioner-chef-solo",
|
||||
"file": "packer-provisioner-file",
|
||||
"puppet-masterless": "packer-provisioner-puppet-masterless",
|
||||
"shell": "packer-provisioner-shell",
|
||||
"salt-masterless": "packer-provisioner-salt-masterless"
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ type Communicator interface {
|
||||
// is a trailing slash on the source "/". For example: "/tmp/src" as
|
||||
// the source will create a "src" directory in the destination unless
|
||||
// a trailing slash is added. This is identical behavior to rsync(1).
|
||||
UploadDir(string, string, []string) error
|
||||
UploadDir(dst string, src string, exclude []string) error
|
||||
|
||||
// Download downloads a file from the machine from the given remote path
|
||||
// with the contents writing to the given writer. This method will
|
||||
|
||||
@@ -2,6 +2,8 @@ package packer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cgl.tideland.biz/identifier"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"text/template"
|
||||
@@ -26,8 +28,10 @@ func NewConfigTemplate() (*ConfigTemplate, error) {
|
||||
|
||||
result.root = template.New("configTemplateRoot")
|
||||
result.root.Funcs(template.FuncMap{
|
||||
"isotime": templateISOTime,
|
||||
"timestamp": templateTimestamp,
|
||||
"user": result.templateUser,
|
||||
"uuid": templateUuid,
|
||||
})
|
||||
|
||||
return result, nil
|
||||
@@ -59,6 +63,11 @@ func (t *ConfigTemplate) Validate(s string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add additional functions to the template
|
||||
func (t *ConfigTemplate) Funcs(funcs template.FuncMap) {
|
||||
t.root.Funcs(funcs)
|
||||
}
|
||||
|
||||
func (t *ConfigTemplate) nextTemplateName() string {
|
||||
name := fmt.Sprintf("tpl%d", t.i)
|
||||
t.i++
|
||||
@@ -76,6 +85,14 @@ func (t *ConfigTemplate) templateUser(n string) (string, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func templateISOTime() string {
|
||||
return time.Now().UTC().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func templateTimestamp() string {
|
||||
return strconv.FormatInt(time.Now().UTC().Unix(), 10)
|
||||
}
|
||||
|
||||
func templateUuid() string {
|
||||
return hex.EncodeToString(identifier.NewUUID().Raw())
|
||||
}
|
||||
|
||||
@@ -7,6 +7,28 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestConfigTemplateProcess_isotime(t *testing.T) {
|
||||
tpl, err := NewConfigTemplate()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
result, err := tpl.Process(`{{isotime}}`, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
val, err := time.Parse(time.RFC3339, result)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
currentTime := time.Now().UTC()
|
||||
if currentTime.Sub(val) > 2*time.Second {
|
||||
t.Fatalf("val: %d (current: %d)", val, currentTime)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigTemplateProcess_timestamp(t *testing.T) {
|
||||
tpl, err := NewConfigTemplate()
|
||||
if err != nil {
|
||||
@@ -47,6 +69,22 @@ func TestConfigTemplateProcess_user(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigTemplateProcess_uuid(t *testing.T) {
|
||||
tpl, err := NewConfigTemplate()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
result, err := tpl.Process(`{{uuid}}`, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if len(result) != 32 {
|
||||
t.Fatalf("err: %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigTemplateValidate(t *testing.T) {
|
||||
tpl, err := NewConfigTemplate()
|
||||
if err != nil {
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import (
|
||||
var GitCommit string
|
||||
|
||||
// The version of packer.
|
||||
const Version = "0.3.6"
|
||||
const Version = "0.3.7"
|
||||
|
||||
// Any pre-release marker for the version. If this is "" (empty string),
|
||||
// then it means that it is a final release. Otherwise, this is the
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/packer/packer/plugin"
|
||||
"github.com/mitchellh/packer/provisioner/puppet-masterless"
|
||||
)
|
||||
|
||||
func main() {
|
||||
plugin.ServeProvisioner(new(puppetmasterless.Provisioner))
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package main
|
||||
@@ -72,6 +72,17 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
|
||||
|
||||
func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
|
||||
ui.Say(fmt.Sprintf("Uploading %s => %s", p.config.Source, p.config.Destination))
|
||||
info, err := os.Stat(p.config.Source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If we're uploading a directory, short circuit and do that
|
||||
if info.IsDir() {
|
||||
return comm.UploadDir(p.config.Destination, p.config.Source, nil)
|
||||
}
|
||||
|
||||
// We're uploading a file...
|
||||
f, err := os.Open(p.config.Source)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
// This package implements a provisioner for Packer that executes
|
||||
// Puppet on the remote machine, configured to apply a local manifest
|
||||
// versus connecting to a Puppet master.
|
||||
package puppetmasterless
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
tpl *packer.ConfigTemplate
|
||||
|
||||
// The command used to execute Puppet.
|
||||
ExecuteCommand string `mapstructure:"execute_command"`
|
||||
|
||||
// Additional facts to set when executing Puppet
|
||||
Facter map[string]string
|
||||
|
||||
// Path to a hiera configuration file to upload and use.
|
||||
HieraConfigPath string `mapstructure:"hiera_config_path"`
|
||||
|
||||
// An array of local paths of modules to upload.
|
||||
ModulePaths []string `mapstructure:"module_paths"`
|
||||
|
||||
// The main manifest file to apply to kick off the entire thing.
|
||||
ManifestFile string `mapstructure:"manifest_file"`
|
||||
|
||||
// If true, `sudo` will NOT be used to execute Puppet.
|
||||
PreventSudo bool `mapstructure:"prevent_sudo"`
|
||||
|
||||
// The directory where files will be uploaded. Packer requires write
|
||||
// permissions in this directory.
|
||||
StagingDir string `mapstructure:"staging_directory"`
|
||||
}
|
||||
|
||||
type Provisioner struct {
|
||||
config Config
|
||||
}
|
||||
|
||||
type ExecuteTemplate struct {
|
||||
FacterVars string
|
||||
HasHieraConfigPath bool
|
||||
HieraConfigPath string
|
||||
ModulePath string
|
||||
ManifestFile string
|
||||
Sudo bool
|
||||
}
|
||||
|
||||
func (p *Provisioner) Prepare(raws ...interface{}) error {
|
||||
md, err := common.DecodeConfig(&p.config, raws...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.config.tpl, err = packer.NewConfigTemplate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.config.tpl.UserVars = p.config.PackerUserVars
|
||||
|
||||
// Accumulate any errors
|
||||
errs := common.CheckUnusedConfig(md)
|
||||
|
||||
// Set some defaults
|
||||
if p.config.ExecuteCommand == "" {
|
||||
p.config.ExecuteCommand = "{{.FacterVars}}{{if .Sudo}} sudo -E {{end}}" +
|
||||
"puppet apply --verbose --modulepath='{{.ModulePath}}' " +
|
||||
"{{if .HasHieraConfigPath}}--hiera_config='{{.HieraConfigPath}}' {{end}}" +
|
||||
"--detailed-exitcodes " +
|
||||
"{{.ManifestFile}}"
|
||||
}
|
||||
|
||||
if p.config.StagingDir == "" {
|
||||
p.config.StagingDir = "/tmp/packer-puppet-masterless"
|
||||
}
|
||||
|
||||
// Templates
|
||||
templates := map[string]*string{
|
||||
"staging_dir": &p.config.StagingDir,
|
||||
}
|
||||
|
||||
for n, ptr := range templates {
|
||||
var err error
|
||||
*ptr, err = p.config.tpl.Process(*ptr, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Error processing %s: %s", n, err))
|
||||
}
|
||||
}
|
||||
|
||||
sliceTemplates := map[string][]string{
|
||||
"module_paths": p.config.ModulePaths,
|
||||
}
|
||||
|
||||
for n, slice := range sliceTemplates {
|
||||
for i, elem := range slice {
|
||||
var err error
|
||||
slice[i], err = p.config.tpl.Process(elem, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Error processing %s[%d]: %s", n, i, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validates := map[string]*string{
|
||||
"execute_command": &p.config.ExecuteCommand,
|
||||
}
|
||||
|
||||
for n, ptr := range validates {
|
||||
if err := p.config.tpl.Validate(*ptr); err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Error parsing %s: %s", n, err))
|
||||
}
|
||||
}
|
||||
|
||||
newFacts := make(map[string]string)
|
||||
for k, v := range p.config.Facter {
|
||||
k, err := p.config.tpl.Process(k, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("Error processing facter key %s: %s", k, err))
|
||||
continue
|
||||
}
|
||||
|
||||
v, err := p.config.tpl.Process(v, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("Error processing facter value '%s': %s", v, err))
|
||||
continue
|
||||
}
|
||||
|
||||
newFacts[k] = v
|
||||
}
|
||||
|
||||
p.config.Facter = newFacts
|
||||
|
||||
// Validation
|
||||
if p.config.HieraConfigPath != "" {
|
||||
info, err := os.Stat(p.config.ManifestFile)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("hiera_config_path is invalid: %s", err))
|
||||
} else if info.IsDir() {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("hiera_config_path must point to a file"))
|
||||
}
|
||||
}
|
||||
|
||||
if p.config.ManifestFile == "" {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("A manifest_file must be specified."))
|
||||
} else {
|
||||
info, err := os.Stat(p.config.ManifestFile)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("manifest_file is invalid: %s", err))
|
||||
} else if info.IsDir() {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("manifest_file must point to a file"))
|
||||
}
|
||||
}
|
||||
|
||||
for i, path := range p.config.ModulePaths {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("module_path[%d] is invalid: %s", i, err))
|
||||
} else if !info.IsDir() {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("module_path[%d] must point to a directory"))
|
||||
}
|
||||
}
|
||||
|
||||
if errs != nil && len(errs.Errors) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
|
||||
ui.Say("Provisioning with Puppet...")
|
||||
ui.Message("Creating Puppet staging directory...")
|
||||
if err := p.createDir(ui, comm, p.config.StagingDir); err != nil {
|
||||
return fmt.Errorf("Error creating staging directory: %s", err)
|
||||
}
|
||||
|
||||
// Upload hiera config if set
|
||||
remoteHieraConfigPath := ""
|
||||
if p.config.HieraConfigPath != "" {
|
||||
var err error
|
||||
remoteHieraConfigPath, err = p.uploadHieraConfig(ui, comm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error uploading hiera config: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Upload all modules
|
||||
modulePaths := make([]string, 0, len(p.config.ModulePaths))
|
||||
for i, path := range p.config.ModulePaths {
|
||||
ui.Message(fmt.Sprintf("Uploading local modules from: %s", path))
|
||||
targetPath := fmt.Sprintf("%s/module-%d", p.config.StagingDir, i)
|
||||
if err := p.uploadDirectory(ui, comm, targetPath, path); err != nil {
|
||||
return fmt.Errorf("Error uploading modules: %s", err)
|
||||
}
|
||||
|
||||
modulePaths = append(modulePaths, targetPath)
|
||||
}
|
||||
|
||||
// Upload manifests
|
||||
ui.Message("Uploading manifests...")
|
||||
remoteManifestFile, err := p.uploadManifests(ui, comm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error uploading manifests: %s", err)
|
||||
}
|
||||
|
||||
// Compile the facter variables
|
||||
facterVars := make([]string, 0, len(p.config.Facter))
|
||||
for k, v := range p.config.Facter {
|
||||
facterVars = append(facterVars, fmt.Sprintf("FACTER_%s='%s'", k, v))
|
||||
}
|
||||
|
||||
// Execute Puppet
|
||||
command, err := p.config.tpl.Process(p.config.ExecuteCommand, &ExecuteTemplate{
|
||||
FacterVars: strings.Join(facterVars, " "),
|
||||
HasHieraConfigPath: remoteHieraConfigPath != "",
|
||||
HieraConfigPath: remoteHieraConfigPath,
|
||||
ManifestFile: remoteManifestFile,
|
||||
ModulePath: strings.Join(modulePaths, ":"),
|
||||
Sudo: !p.config.PreventSudo,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := &packer.RemoteCmd{
|
||||
Command: command,
|
||||
}
|
||||
|
||||
ui.Message(fmt.Sprintf("Running Puppet: %s", command))
|
||||
if err := cmd.StartWithUi(comm, ui); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cmd.ExitStatus != 0 {
|
||||
return fmt.Errorf("Puppet exited with a non-zero exit status: %d", cmd.ExitStatus)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provisioner) Cancel() {
|
||||
// Just hard quit. It isn't a big deal if what we're doing keeps
|
||||
// running on the other side.
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func (p *Provisioner) uploadHieraConfig(ui packer.Ui, comm packer.Communicator) (string, error) {
|
||||
ui.Message("Uploading hiera configuration...")
|
||||
f, err := os.Open(p.config.HieraConfigPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
path := fmt.Sprintf("%s/hiera.yaml", p.config.StagingDir)
|
||||
if err := comm.Upload(path, f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func (p *Provisioner) uploadManifests(ui packer.Ui, comm packer.Communicator) (string, error) {
|
||||
// Create the remote manifests directory...
|
||||
ui.Message("Uploading manifests...")
|
||||
remoteManifestsPath := fmt.Sprintf("%s/manifests", p.config.StagingDir)
|
||||
if err := p.createDir(ui, comm, remoteManifestsPath); err != nil {
|
||||
return "", fmt.Errorf("Error creating manifests directory: %s", err)
|
||||
}
|
||||
|
||||
// Upload the main manifest
|
||||
f, err := os.Open(p.config.ManifestFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
manifestFilename := filepath.Base(p.config.ManifestFile)
|
||||
remoteManifestFile := fmt.Sprintf("%s/%s", remoteManifestsPath, manifestFilename)
|
||||
if err := comm.Upload(remoteManifestFile, f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return remoteManifestFile, nil
|
||||
}
|
||||
|
||||
func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error {
|
||||
cmd := &packer.RemoteCmd{
|
||||
Command: fmt.Sprintf("mkdir -p '%s'", dir),
|
||||
}
|
||||
|
||||
if err := cmd.StartWithUi(comm, ui); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cmd.ExitStatus != 0 {
|
||||
return fmt.Errorf("Non-zero exit status.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, dst string, src string) error {
|
||||
if err := p.createDir(ui, comm, dst); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Make sure there is a trailing "/" so that the directory isn't
|
||||
// created on the other side.
|
||||
if src[len(src)-1] != '/' {
|
||||
src = src + "/"
|
||||
}
|
||||
|
||||
return comm.UploadDir(dst, src, nil)
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package puppetmasterless
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testConfig() map[string]interface{} {
|
||||
tf, err := ioutil.TempFile("", "packer")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"manifest_file": tf.Name(),
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvisioner_Impl(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = &Provisioner{}
|
||||
if _, ok := raw.(packer.Provisioner); !ok {
|
||||
t.Fatalf("must be a Provisioner")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvisionerPrepare_hieraConfigPath(t *testing.T) {
|
||||
config := testConfig()
|
||||
|
||||
delete(config, "hiera_config_path")
|
||||
p := new(Provisioner)
|
||||
err := p.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
// Test with a good one
|
||||
tf, err := ioutil.TempFile("", "packer")
|
||||
if err != nil {
|
||||
t.Fatalf("error tempfile: %s", err)
|
||||
}
|
||||
defer os.Remove(tf.Name())
|
||||
|
||||
config["hiera_config_path"] = tf.Name()
|
||||
p = new(Provisioner)
|
||||
err = p.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvisionerPrepare_manifestFile(t *testing.T) {
|
||||
config := testConfig()
|
||||
|
||||
delete(config, "manifest_file")
|
||||
p := new(Provisioner)
|
||||
err := p.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should be an error")
|
||||
}
|
||||
|
||||
// Test with a good one
|
||||
tf, err := ioutil.TempFile("", "packer")
|
||||
if err != nil {
|
||||
t.Fatalf("error tempfile: %s", err)
|
||||
}
|
||||
defer os.Remove(tf.Name())
|
||||
|
||||
config["manifest_file"] = tf.Name()
|
||||
p = new(Provisioner)
|
||||
err = p.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvisionerPrepare_modulePaths(t *testing.T) {
|
||||
config := testConfig()
|
||||
|
||||
delete(config, "module_paths")
|
||||
p := new(Provisioner)
|
||||
err := p.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
// Test with bad paths
|
||||
config["module_paths"] = []string{"i-should-not-exist"}
|
||||
p = new(Provisioner)
|
||||
err = p.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should be an error")
|
||||
}
|
||||
|
||||
// Test with a good one
|
||||
td, err := ioutil.TempDir("", "packer")
|
||||
if err != nil {
|
||||
t.Fatalf("error: %s", err)
|
||||
}
|
||||
defer os.RemoveAll(td)
|
||||
|
||||
config["module_paths"] = []string{td}
|
||||
p = new(Provisioner)
|
||||
err = p.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
+8
-2
@@ -20,6 +20,12 @@ cd $DIR
|
||||
GIT_COMMIT=$(git rev-parse HEAD)
|
||||
GIT_DIRTY=$(test -n "`git status --porcelain`" && echo "+CHANGES" || true)
|
||||
|
||||
# If we're building on Windows, specify an extension
|
||||
EXTENSION=""
|
||||
if [ "$(go env GOOS)" = "windows" ]; then
|
||||
EXTENSION=".exe"
|
||||
fi
|
||||
|
||||
# If we're building a race-enabled build, then set that up.
|
||||
if [ ! -z $PACKER_RACE ]; then
|
||||
echo -e "${OK_COLOR}--> Building with race detection enabled${NO_COLOR}"
|
||||
@@ -35,7 +41,7 @@ go build \
|
||||
${PACKER_RACE} \
|
||||
-ldflags "-X github.com/mitchellh/packer/packer.GitCommit ${GIT_COMMIT}${GIT_DIRTY}" \
|
||||
-v \
|
||||
-o bin/packer .
|
||||
-o bin/packer${EXTENSION} .
|
||||
|
||||
# Go over each plugin and build it
|
||||
for PLUGIN in $(find ./plugin -mindepth 1 -maxdepth 1 -type d); do
|
||||
@@ -45,5 +51,5 @@ for PLUGIN in $(find ./plugin -mindepth 1 -maxdepth 1 -type d); do
|
||||
${PACKER_RACE} \
|
||||
-ldflags "-X github.com/mitchellh/packer/packer.GitCommit ${GIT_COMMIT}${GIT_DIRTY}" \
|
||||
-v \
|
||||
-o bin/packer-${PLUGIN_NAME} ${PLUGIN}
|
||||
-o bin/packer-${PLUGIN_NAME}${EXTENSION} ${PLUGIN}
|
||||
done
|
||||
|
||||
@@ -62,7 +62,7 @@ Optional:
|
||||
device mappings to the AMI. The block device mappings allow for keys:
|
||||
"device\_name" (string), "virtual\_name" (string), "snapshot\_id" (string),
|
||||
"volume\_type" (string), "volume\_size" (int), "delete\_on\_termination"
|
||||
(bool), and "iops" (int).
|
||||
(bool), "no\_device" (bool), and "iops" (int).
|
||||
|
||||
* `ami_description` (string) - The description to set for the resulting
|
||||
AMI(s). By default this description is empty.
|
||||
|
||||
@@ -77,8 +77,8 @@ Optional:
|
||||
device mappings to the AMI. The block device mappings allow for keys:
|
||||
"device\_name" (string), "virtual\_name" (string), "snapshot\_id" (string),
|
||||
"volume\_type" (string), "volume\_size" (int), "delete\_on\_termination"
|
||||
(bool), and "iops" (int). See [amazon-ebs](/docs/builders/amazon-ebs.html)
|
||||
for an example template.
|
||||
(bool), "no\_device" (bool), and "iops" (int).
|
||||
See [amazon-ebs](/docs/builders/amazon-ebs.html) for an example template.
|
||||
|
||||
* `ami_description` (string) - The description to set for the resulting
|
||||
AMI(s). By default this description is empty.
|
||||
|
||||
@@ -51,6 +51,9 @@ Required:
|
||||
|
||||
Optional:
|
||||
|
||||
* `project` (string) - The project name to boot the instance into. Some
|
||||
OpenStack installations require this. By default this is empty.
|
||||
|
||||
* `ssh_port` (int) - The port that SSH will be available on. Defaults to port
|
||||
22.
|
||||
|
||||
|
||||
@@ -108,6 +108,10 @@ Optional:
|
||||
how to optimize the virtual hardware to work best with that operating
|
||||
system.
|
||||
|
||||
* `hard_drive_interface` (string) - The type of controller that the primary
|
||||
hard drive is attached to, defaults to "ide". When set to "sata", the
|
||||
drive is attached to an AHCI SATA controller.
|
||||
|
||||
* `headless` (bool) - Packer defaults to building VirtualBox
|
||||
virtual machines by launching a GUI that shows the console of the
|
||||
machine being built. When this value is set to true, the machine will
|
||||
@@ -187,7 +191,7 @@ Optional:
|
||||
By default this is ".vbox_version", which will generally upload it into
|
||||
the home directory.
|
||||
|
||||
* `vm_name` (string) - This is the name of the VMX file for the new virtual
|
||||
* `vm_name` (string) - This is the name of the OVF file for the new virtual
|
||||
machine, without the file extension. By default this is "packer-BUILDNAME",
|
||||
where "BUILDNAME" is the name of the build.
|
||||
|
||||
|
||||
@@ -161,9 +161,9 @@ on certain cache keys, and is given exclusive access to that key for the
|
||||
duration of the lock. This locking mechanism allows multiple builders to
|
||||
share cache data even though they're running in parallel.
|
||||
|
||||
For example, both the VMware and VirtualBox support downloading an operating
|
||||
system ISO from the internet. Most of the time, this ISO is identical. The
|
||||
locking mechanisms of the cache allow one of the builders to download it
|
||||
For example, both the VMware and VirtualBox builders support downloading an
|
||||
operating system ISO from the internet. Most of the time, this ISO is identical.
|
||||
The locking mechanisms of the cache allow one of the builders to download it
|
||||
only once, but allow both builders to share the downloaded file.
|
||||
|
||||
The [documentation for packer.Cache](#) is
|
||||
|
||||
@@ -12,6 +12,8 @@ recommended usage of the file provisioner is to use it to upload files,
|
||||
and then use [shell provisioner](/docs/provisioners/shell.html) to move
|
||||
them to the proper place, set permissions, etc.
|
||||
|
||||
The file provisioner can upload both single files and complete directories.
|
||||
|
||||
## Basic Example
|
||||
|
||||
<pre class="prettyprint">
|
||||
@@ -26,10 +28,38 @@ them to the proper place, set permissions, etc.
|
||||
|
||||
The available configuration options are listed below. All elements are required.
|
||||
|
||||
* `source` (string) - The path to a local file to upload to the machine. The
|
||||
path can be absolute or relative. If it is relative, it is relative to the
|
||||
working directory when Packer is executed.
|
||||
* `source` (string) - The path to a local file or directory to upload to the
|
||||
machine. The path can be absolute or relative. If it is relative, it is
|
||||
relative to the working directory when Packer is executed. If this is a
|
||||
directory, the existence of a trailing slash is important. Read below on
|
||||
uploading directories.
|
||||
|
||||
* `destination` (string) - The path where the file will be uploaded to in the
|
||||
machine. This value must be a writable location and any parent directories
|
||||
must already exist.
|
||||
|
||||
## Directory Uploads
|
||||
|
||||
The file provisioner is also able to upload a complete directory to the
|
||||
remote machine. When uploading a directory, there are a few important things
|
||||
you should know.
|
||||
|
||||
First, the destination directory must already exist. If you need to
|
||||
create it, use a shell provisioner just prior to the file provisioner
|
||||
in order to create the directory.
|
||||
|
||||
Next, the existence of a trailing slash on the source path will determine
|
||||
whether the directory name will be embedded within the destination, or
|
||||
whether the destination will be created. An example explains this best:
|
||||
|
||||
If the source is `/foo` (no trailing slash), and the destination is
|
||||
`/tmp`, then the contents of `/foo` on the local machine will be uploaded
|
||||
to `/tmp/foo` on the remote machine. The `foo` directory on the remote
|
||||
machine will be created by Packer.
|
||||
|
||||
If the source, however, is `/foo/` (a trailing slash is present), and
|
||||
the destination is `/tmp`, then the contents of `/foo` will be uploaded
|
||||
directly into `/tmp` directly.
|
||||
|
||||
This behavior was adopted from the standard behavior of rsync. Note that
|
||||
under the covers, rsync may or may not be used.
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
---
|
||||
layout: "docs"
|
||||
page_title: "Puppet (Masterless) Provisioner"
|
||||
---
|
||||
|
||||
# Puppet (Masterless) Provisioner
|
||||
|
||||
Type: `puppet-masterless`
|
||||
|
||||
The masterless Puppet provisioner configures Puppet to run on the machines
|
||||
by Packer from local modules and manifest files. Modules and manifests
|
||||
can be uploaded from your local machine to the remote machine or can simply
|
||||
use remote paths (perhaps obtained using something like the shell provisioner).
|
||||
Puppet is run in masterless mode, meaning it never communicates to a Puppet
|
||||
master.
|
||||
|
||||
<div class="alert alert-info alert-block">
|
||||
<strong>Note that Puppet will <em>not</em> be installed automatically
|
||||
by this provisioner.</strong> This provisioner expects that Puppet is already
|
||||
installed on the machine. It is common practice to use the
|
||||
<a href="/docs/provisioners/shell.html">shell provisioner</a> before the
|
||||
Puppet provisioner to do this.
|
||||
</div>
|
||||
|
||||
## Basic Example
|
||||
|
||||
The example below is fully functional and expects the configured manifest
|
||||
file to exist relative to your working directory:
|
||||
|
||||
<pre class="prettyprint">
|
||||
{
|
||||
"type": "puppet-masterless",
|
||||
"manifest_file": "site.pp"
|
||||
}
|
||||
</pre>
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
The reference of available configuration options is listed below.
|
||||
|
||||
Required parameters:
|
||||
|
||||
* `manifest_file` (string) - The manifest file for Puppet to use in order
|
||||
to compile and run a catalog. This file must exist on your local system
|
||||
and will be uploaded to the remote machine.
|
||||
|
||||
Optional parameters:
|
||||
|
||||
* `execute_command` (string) - The command used to execute Puppet. This has
|
||||
various [configuration template variables](/docs/templates/configuration-templates.html)
|
||||
available. See below for more information.
|
||||
|
||||
* `facter` (object, string keys and values) - Additonal
|
||||
[facts](http://puppetlabs.com/puppet/related-projects/facter) to make
|
||||
available when Puppet is running.
|
||||
|
||||
* `hiera_config_path` (string) - The path to a local file with hiera
|
||||
configuration to be uploaded to the remote machine. Hiera data directories
|
||||
must be uploaded using the file provisioner separately.
|
||||
|
||||
* `module_paths` (array of strings) - This is an array of paths to module
|
||||
directories on your local filesystem. These will be uploaded to the remote
|
||||
machine. By default, this is empty.
|
||||
|
||||
* `prevent_sudo` (boolean) - By default, the configured commands that are
|
||||
executed to run Puppet are executed with `sudo`. If this is true,
|
||||
then the sudo will be omitted.
|
||||
|
||||
* `staging_directory` (string) - This is the directory where all the configuration
|
||||
of Puppet by Packer will be placed. By default this is "/tmp/packer-puppet-masterless".
|
||||
This directory doesn't need to exist but must have proper permissions so that
|
||||
the SSH user that Packer uses is able to create directories and write into
|
||||
this folder. If the permissions are not correct, use a shell provisioner
|
||||
prior to this to configure it properly.
|
||||
|
||||
## Execute Command
|
||||
|
||||
By default, Packer uses the following command (broken across multiple lines
|
||||
for readability) to execute Puppet:
|
||||
|
||||
```
|
||||
{{.FacterVars}}{{if .Sudo} sudo -E {{end}}puppet apply \
|
||||
--verbose \
|
||||
--modulepath='{{.ModulePath}}' \
|
||||
{{if .HasHieraConfigPath}}--hiera_config='{{.HieraConfigPath}}' {{end}} \
|
||||
{{.ManifestFile}}
|
||||
```
|
||||
|
||||
This command can be customized using the `execute_command` configuration.
|
||||
As you can see from the default value above, the value of this configuration
|
||||
can contain various template variables, defined below:
|
||||
|
||||
* `FacterVars` - Shell-friendly string of environmental variables used
|
||||
to set custom facts configured for this provisioner.
|
||||
* `HasHieraConfigPath` - Boolean true if there is a hiera config path set.
|
||||
* `HieraConfigPath` - The path to a hiera configuration file.
|
||||
* `ManifestFile` - The path on the remote machine to the manifest file
|
||||
for Puppet to use.
|
||||
* `ModulePath` - The paths to the module directories.
|
||||
* `Sudo` - A boolean of whether to `sudo` the command or not, depending on
|
||||
the value of the `prevent_sudo` configuration.
|
||||
|
||||
@@ -59,7 +59,7 @@ Optional parameters:
|
||||
`environment_vars`, if configured.
|
||||
|
||||
* `inline_shebang` (string) - The
|
||||
[shebang](http://en.wikipedia.org/wiki/Shebang_(Unix)) value to use when
|
||||
[shebang](http://en.wikipedia.org/wiki/Shebang_%28Unix%29) value to use when
|
||||
running commands specified by `inline`. By default, this is `/bin/sh`.
|
||||
If you're not using `inline`, then this configuration has no effect.
|
||||
|
||||
|
||||
@@ -53,4 +53,13 @@ While some configuration settings have local variables specific to only that
|
||||
configuration, a set of functions are available globally for use in _any string_
|
||||
in Packer templates. These are listed below for reference.
|
||||
|
||||
* ``isotime`` - UTC time in RFC-3339 format.
|
||||
* ``timestamp`` - The current Unix timestamp in UTC.
|
||||
|
||||
## Amazon Specific Functions
|
||||
|
||||
Specific to Amazon builders:
|
||||
|
||||
* ``clean_ami_name`` - AMI names can only contain certain characters. This
|
||||
function will replace illegal characters with a '-" character. Example usage
|
||||
since ":" is not a legal AMI name is: `{{isotime | clean_ami_name}}`.
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
<li><a href="/docs/provisioners/shell.html">Shell Scripts</a></li>
|
||||
<li><a href="/docs/provisioners/file.html">File Uploads</a></li>
|
||||
<li><a href="/docs/provisioners/chef-solo.html">Chef Solo</a></li>
|
||||
<li><a href="/docs/provisioners/puppet-masterless.html">Puppet</a></li>
|
||||
<li><a href="/docs/provisioners/salt-masterless.html">Salt</a></li>
|
||||
<li><a href="/docs/provisioners/custom.html">Custom</a></li>
|
||||
</ul>
|
||||
|
||||
Reference in New Issue
Block a user