Compare commits

...

73 Commits

Author SHA1 Message Date
Mitchell Hashimoto 0b23c8addf v0.3.7 2013-09-09 14:48:42 -07:00
Mitchell Hashimoto aa658e982b provisioner/puppet-masterless: detailed exit codes 2013-09-09 14:46:25 -07:00
Mitchell Hashimoto d29c5cd231 provisioner/puppet-masterless: better messaging 2013-09-09 14:03:40 -07:00
Mitchell Hashimoto f5850199ba provisioner/file: support uploading directories [GH-251] 2013-09-09 14:00:06 -07:00
Mitchell Hashimoto dbf978f8d1 provisioners/puppet-masterless: support hiera configs 2013-09-09 13:24:17 -07:00
Mitchell Hashimoto 4c537105ef provisioner/puppet-masterless: support custom facts 2013-09-08 23:00:11 -07:00
Mitchell Hashimoto 1bad19f58b Merge pull request #406 from andrewschleifer/edits
website: accidently a word
2013-09-08 22:13:22 -07:00
Andrew Schleifer 06b5b4304d accidently a word
specifically, "builders"
2013-09-08 22:27:14 -05:00
Mitchell Hashimoto 5315aaba0d website: document the Puppet provisioner 2013-09-07 23:56:00 -07:00
Mitchell Hashimoto 3a899892dc provisioner/puppet-masterless: more sane messaging 2013-09-07 23:43:06 -07:00
Mitchell Hashimoto ef0a077168 provisioner/puppet-masterless: more validation and tests 2013-09-07 23:31:28 -07:00
Mitchell Hashimoto de0c59cc18 provisioner/puppet-masterless: rework internals, use SCP 2013-09-07 23:08:56 -07:00
Mitchell Hashimoto 53ab6b9607 Update CHANGELOG 2013-09-07 22:28:06 -07:00
Mitchell Hashimoto 6f8afdeaf6 Rename puppet provisioner to puppet-masterless 2013-09-07 22:27:25 -07:00
Mitchell Hashimoto 1f63abc622 Merge pull request #234 from jamtur01/master
Added basic Puppet provisioner
2013-09-07 22:24:16 -07:00
Mark Peek 46a2b81a1d Update CHANGELOG to include clean_ami_name 2013-09-07 18:57:00 -07:00
Mark Peek 7fe487cd4f website: add no_device to amazon block device doc 2013-09-07 18:53:34 -07:00
Mark Peek 06b83d148b website: document isotime and clean_ami_name 2013-09-07 18:50:01 -07:00
Mark Peek 9035b51165 builder/amazon: add clean_ami_name template function
Add a clean_ami_name template function which will translate illegal
characters in an AMI name to '-'. Example usage would be:
    "ami_name": "Ubuntu 12.04 {{isotime | clean_ami_name}}"
2013-09-07 18:42:50 -07:00
Mitchell Hashimoto a775a58129 Update CHANGELOG 2013-09-07 18:14:17 -07:00
Mitchell Hashimoto 694275c7cd Merge pull request #401 from mackrorysd/master
builder/virtualbox: Using the vm_name parameter to name OVA/OVF files and their attachments
2013-09-07 18:12:57 -07:00
Sean Mackrory 27a2270593 Using the vm_name parameter to name OVA/OVF files and their attachments 2013-09-06 23:38:31 -07:00
Mitchell Hashimoto 8c30dc78ac builder/amazon/common: don't need squash here 2013-09-06 11:48:38 -07:00
Mitchell Hashimoto c4cf8553c8 builder/amazon/common: support NoDevice [GH-396] 2013-09-06 11:44:57 -07:00
Mitchell Hashimoto a511b603e2 scripts: build.sh appends .exe on Windows [GH-397] 2013-09-06 11:34:01 -07:00
Mitchell Hashimoto 81edbb4173 communicator/ssh: detect if SCP exited with failure [GH-386] 2013-09-05 19:55:09 -07:00
Mitchell Hashimoto 147eea955e communicator/ssh: prettify log output 2013-09-05 17:22:37 -07:00
Mitchell Hashimoto 67c71aa836 builder/amazon/common: if instance query returns none, ignore 2013-09-05 17:19:23 -07:00
Mitchell Hashimoto 535d2a7cfc Update CHANGELOG 2013-09-05 17:14:50 -07:00
Mitchell Hashimoto 5736dbd3aa Merge pull request #388 from jmassara/ami_region_copy
builder/amazon/all: AMI region copy fixes
2013-09-05 17:14:16 -07:00
Mitchell Hashimoto b57fa3cbdb Update CHANGELOG 2013-09-05 17:12:44 -07:00
Mitchell Hashimoto a59bbd892d Merge branch 'master' of https://github.com/armon/packer into armon-master
Conflicts:
	packer/config_template.go
2013-09-05 17:12:15 -07:00
Mitchell Hashimoto fb8b82fca5 builder/vmware: always set msg.AutoAnswer 2013-09-05 13:44:57 -07:00
Mitchell Hashimoto e26f7e87e0 builder/vmware: auto-answer dialogs [GH-393] 2013-09-05 13:43:01 -07:00
Mitchell Hashimoto 25e7b90a96 Merge pull request #392 from ttres/patch-1
website: minor URL typo
2013-09-05 13:39:10 -07:00
ttres c0ef20bb80 Minor URL typo
URLs with special characters should escape them.
2013-09-05 16:27:38 -03:00
Mitchell Hashimoto 6746ffc133 builder/amazon/all: TemporaryKeyPairName
/cc @whostolebenfrog - I actually simplified things quite a bit. I added
a "uuid" global template function so it just uses that now. I renamed it
so that it is clear it is a temporary keypair.
2013-09-05 12:23:08 -07:00
Mitchell Hashimoto 682cd97991 Update CHANGELOG 2013-09-05 12:20:57 -07:00
Mitchell Hashimoto 8dd3850482 packer: uuid function 2013-09-05 12:19:56 -07:00
Mitchell Hashimoto e89f182b33 Update CHANGELOG 2013-09-05 12:10:40 -07:00
Mitchell Hashimoto 25a310cf45 Merge pull request #389 from whostolebenfrog/master
builder/amazon/ebs: Allow customization of temporary ssh key name
2013-09-05 12:10:03 -07:00
Mitchell Hashimoto bf1a2f4899 builder/virtualbox: process hdd interface as template, validate 2013-09-05 12:07:58 -07:00
Mitchell Hashimoto 250077a0ff Update CHANGELOG 2013-09-05 12:02:18 -07:00
Mitchell Hashimoto 89a29e7b21 Merge pull request #391 from jbronn/hard_drive_interface
builder/virtualbox: Add `hard_drive_interface` option
2013-09-05 12:01:39 -07:00
Armon Dadgar 13a4c34bc5 Adding support for isotime template variable 2013-09-05 11:09:37 -07:00
Justin Bronn 175408dcb2 Document the hard_drive_interface option. 2013-09-05 11:08:55 -07:00
Justin Bronn 3043eddfaf Enable customization of VirtualBox disk controller with hard_drive_interface option (SATA only). 2013-09-05 11:00:08 -07:00
Ben Griffiths 0058929186 Allow customization of SSH Key pair pattern
Defined in a template using ssh_keypair_pattern. Defaults to "packer %s"
2013-09-05 13:28:31 +01:00
Mitchell Hashimoto fc62adfe5d builder/digitalocean: retry longer and don't retry shutdown 2013-09-04 23:01:37 -07:00
Mitchell Hashimoto cc8d0ad2eb builder/digitalocean: retry pending events a lot more 2013-09-04 23:00:54 -07:00
Mitchell Hashimoto 45c88e2aea builder/digitalocean: get compilation working again 2013-09-04 22:53:37 -07:00
Mitchell Hashimoto 290b9ef836 builder/digitalocean: give up on graceful shutdown more quickly 2013-09-04 22:51:28 -07:00
Mitchell Hashimoto a1c96d3b97 builder/digitalocean: proper timeouts so poweroff retries 2013-09-04 22:26:05 -07:00
Mitchell Hashimoto 0271d9a15e builder/digitalocean: properly cleanup goroutines for status checking 2013-09-04 22:21:59 -07:00
Mitchell Hashimoto ede100bb7b builder/digitalocean: actually, we want to try hard on shutdown 2013-09-04 22:14:30 -07:00
Mitchell Hashimoto 2da53f54f4 builder/digitalocean: break loop if we reach off state
Otherwise we get stuck in a long loop. Whoops.
2013-09-04 22:07:19 -07:00
Mitchell Hashimoto 5477d7166b builder/digitalocean: retry power off a number of times
See comment.
2013-09-04 21:59:58 -07:00
Mitchell Hashimoto ab5b094d73 builder/digitalocean: looser pending event string matching.
Most APIs return "has a pending event" but power off seems to return
"already a pending event". WTF. Also, just wtf at the DigitalOcean API.
I can't wait until they change their english and this breaks. Because
it is definitely going to happen, and it's going to be such a fun time.
2013-09-04 21:29:07 -07:00
Mitchell Hashimoto eb8f6c9769 Update CHANGELOG 2013-09-04 21:25:47 -07:00
Mitchell Hashimoto f2e909356e builder/digitalocean: retry on any pending event errors
/cc @pearkes - I hate this thing.
2013-09-04 21:20:41 -07:00
James Massara 952b2c921f Also deregister AMI if it was copied to other regions 2013-09-04 17:48:00 -07:00
James Massara a613446205 builder/amazon: Re-ordered steps for AMI region copying so tags and
launch permissions are also applied to copied AMIs
2013-09-04 16:06:06 -07:00
Mitchell Hashimoto 907ca4d951 Update README 2013-09-03 08:53:29 -07:00
Mitchell Hashimoto 822c52c46f Merge pull request #383 from marcw/readme-format
Add make format information in the README.
2013-09-03 08:52:30 -07:00
Marc Weistroff 082dc6c14d Add make format information in the contributing guidelines. 2013-09-03 14:58:32 +02:00
James Turnbull 797255fca6 Added basic Puppet provisioner 2013-09-03 08:39:49 -04:00
Mitchell Hashimoto 05e1654eb8 Update CHANGELOG 2013-09-02 17:35:00 -07:00
Mitchell Hashimoto 12547d3203 website: update website 2013-09-02 17:34:37 -07:00
Mitchell Hashimoto 8024dde44d Merge pull request #382 from justinsb/specify_openstack_project
builder/openstack: allow project to be specified
2013-09-02 17:33:57 -07:00
Mitchell Hashimoto 0b16317401 command/inspect: fix weird output 2013-09-02 15:38:57 -07:00
Mitchell Hashimoto 06515fd195 up version for dev 2013-09-02 10:42:05 -07:00
Justin Santa Barbara 47ee7ca977 Only pass the project if it is specified 2013-09-02 10:34:44 -07:00
Justin Santa Barbara b6a6c8571b Allow the Openstack project to be specified
It isn't actually optional, according to the spec
2013-09-02 10:34:44 -07:00
54 changed files with 1127 additions and 281 deletions
+41
View File
@@ -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:
+3
View File
@@ -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.
+4 -4
View File
@@ -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,
},
+5 -3
View File
@@ -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)
}
+4 -2
View File
@@ -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
+27 -17
View File
@@ -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)
+25 -19
View File
@@ -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 {
+12
View File
@@ -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
}
+16 -13
View File
@@ -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
}
}
}
+5 -9
View File
@@ -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
}
}
}
+38
View File
@@ -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)
}
}
+5 -4
View File
@@ -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,
},
+5 -4
View File
@@ -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
View File
@@ -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
}
-16
View File
@@ -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(
-32
View File
@@ -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()
+1 -18
View File
@@ -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)
+1 -1
View File
@@ -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)
+20 -15
View File
@@ -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
}
+39 -14
View File
@@ -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
+1 -1
View File
@@ -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)
+25 -24
View File
@@ -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
}
+9
View File
@@ -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)
}
+11
View File
@@ -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"))
+32
View File
@@ -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()
+22 -1
View File
@@ -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,
+1 -1
View File
@@ -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",
+3
View File
@@ -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)
+1 -1
View File
@@ -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)
+5 -3
View File
@@ -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
}
+1
View File
@@ -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"
}
+1 -1
View File
@@ -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
+17
View File
@@ -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())
}
+38
View File
@@ -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
View File
@@ -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
+11
View File
@@ -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
View File
@@ -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}}`.
+1
View File
@@ -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>