Merge remote-tracking branch 'upstream/master' into prestine

This commit is contained in:
Matthew Patton 2018-06-11 15:10:09 -04:00
commit 5298142da6
197 changed files with 16709 additions and 2283 deletions

View file

@ -1,13 +1,57 @@
## (UNRELEASED)
## 1.2.4 (May 29, 2018)
### BUG FIXES:
* builder/vmware-esxi: Remove floppy files from the remote server on cleanup. [GH-6206]
* core: When using `-on-error=[abort|ask]`, output the error to the user. [GH-6252]
* builder/amazon: Can now force the chroot builder to mount an entire block
device instead of a partition [GH-6194]
* builder/azure: windows-sql-cloud is now in the default list of projects to
check for provided images. [GH-6210]
* builder/chroot: A new template option, `nvme_device_path` has been added to
provide a workaround for users who need the amazon-chroot builder to mount
a NVMe volume on their instances. [GH-6295]
* builder/hyper-v: Fix command for mounting multiple disks [GH-6267]
* builder/hyperv: Enable IP retrieval for Server 2008 R2 hosts. [GH-6219]
* builder/hyperv: Fix bug in MAC address specification on Hyper-V. [GH-6187]
* builder/parallels-pvm: Add missing disk compaction step. [GH-6202]
* builder/vmware-esxi: Remove floppy files from the remote server on cleanup.
[GH-6206]
* communicator/winrm: Updated dependencies to fix a race condition [GH-6261]
* core: When using `-on-error=[abort|ask]`, output the error to the user.
[GH-6252]
* provisioner/puppet: Extra-Arguments are no longer prematurely
interpolated.[GH-6215]
* provisioner/shell: Remove file stat that was causing problems uploading files
[GH-6239]
### IMPROVEMENTS:
* builder/amazon: Amazon builders other than `chroot` now support T2 unlimited
instances [GH-6265]
* builder/azure: Allow device login for US government cloud. [GH-6105]
* builder/azure: Devicelogin Support for Windows [GH-6285]
* builder/azure: Enable simultaneous builds within one resource group.
[GH-6231]
* builder/azure: Faster deletion of Azure Resource Groups. [GH-6269]
* builder/azure: Updated Azure SDK to v15.0.0 [GH-6224]
* builder/hyper-v: Hyper-V builds now connect to vnc display by default when
building [GH-6243]
* builder/hyper-v: New `use_fixed_vhd_format` allows vm export in an Azure-
compatible format [GH-6101]
* builder/hyperv: New config option for specifying what secure boot template to
use, allowing secure boot of linux vms. [GH-5883]
* builder/qemu: Add support for hvf accelerator. [GH-6193]
* builder/scaleway: Fix SSH communicator connection issue. [GH-6238]
* core: Add opt-in Packer top-level command autocomplete [GH-5454]
* post-processor/shell-local: New options have been added to create feature
parity with the shell-local provisioner. This feature now works on Windows
hosts. [GH-5956]
* provisioner/chef: New config option allows user to skip cleanup of chef
client staging directory. [GH-4300]
* provisioner/shell-local: Can now access automatically-generated WinRM
password as variable [GH-6251]
* provisoner/shell-local: New options have been added to create feature parity
with the shell-local post-processor. This feature now works on Windows
hosts. [GH-5956]
## 1.2.3 (April 25, 2018)

View file

@ -4,11 +4,13 @@ VET?=$(shell ls -d */ | grep -v vendor | grep -v website)
GITSHA:=$(shell git rev-parse HEAD)
# Get the current local branch name from git (if we can, this may be blank)
GITBRANCH:=$(shell git symbolic-ref --short HEAD 2>/dev/null)
GOFMT_FILES?=$$(find . -not -path "./vendor/*" -name "*.go")
GOOS=$(shell go env GOOS)
GOARCH=$(shell go env GOARCH)
GOPATH=$(shell go env GOPATH)
# gofmt
UNFORMATTED_FILES=$(shell find . -not -path "./vendor/*" -name "*.go" | xargs gofmt -s -l)
# Get the git commit
GIT_DIRTY=$(shell test -n "`git status --porcelain`" && echo "+CHANGES" || true)
GIT_COMMIT=$(shell git rev-parse --short HEAD)
@ -58,10 +60,18 @@ dev: deps ## Build and install a development build
@cp $(GOPATH)/bin/packer pkg/$(GOOS)_$(GOARCH)
fmt: ## Format Go code
@gofmt -w -s $(GOFMT_FILES)
@gofmt -w -s $(UNFORMATTED_FILES)
fmt-check: ## Check go code formatting
$(CURDIR)/scripts/gofmtcheck.sh $(GOFMT_FILES)
@echo "==> Checking that code complies with gofmt requirements..."
@if [ ! -z "$(UNFORMATTED_FILES)" ]; then \
echo "gofmt needs to be run on the following files:"; \
echo "$(UNFORMATTED_FILES)" | xargs -n1; \
echo "You can use the command: \`make fmt\` to reformat code."; \
exit 1; \
else \
echo "Check passed."; \
fi
fmt-docs:
@find ./website/source/docs -name "*.md" -exec pandoc --wrap auto --columns 79 --atx-headers -s -f "markdown_github+yaml_metadata_block" -t "markdown_github+yaml_metadata_block" {} -o {} \;

View file

@ -33,9 +33,10 @@ type Config struct {
CommandWrapper string `mapstructure:"command_wrapper"`
CopyFiles []string `mapstructure:"copy_files"`
DevicePath string `mapstructure:"device_path"`
NVMEDevicePath string `mapstructure:"nvme_device_path"`
FromScratch bool `mapstructure:"from_scratch"`
MountOptions []string `mapstructure:"mount_options"`
MountPartition int `mapstructure:"mount_partition"`
MountPartition string `mapstructure:"mount_partition"`
MountPath string `mapstructure:"mount_path"`
PostMountCommands []string `mapstructure:"post_mount_commands"`
PreMountCommands []string `mapstructure:"pre_mount_commands"`
@ -112,8 +113,8 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
b.config.MountPath = "/mnt/packer-amazon-chroot-volumes/{{.Device}}"
}
if b.config.MountPartition == 0 {
b.config.MountPartition = 1
if b.config.MountPartition == "" {
b.config.MountPartition = "1"
}
// Accumulate any errors or warnings

View file

@ -3,8 +3,8 @@ package chroot
import (
"fmt"
sl "github.com/hashicorp/packer/common/shell-local"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/post-processor/shell-local"
"github.com/hashicorp/packer/template/interpolate"
)
@ -21,7 +21,9 @@ func RunLocalCommands(commands []string, wrappedCommand CommandWrapper, ctx inte
}
ui.Say(fmt.Sprintf("Executing command: %s", command))
comm := &shell_local.Communicator{}
comm := &sl.Communicator{
ExecuteCommand: []string{"sh", "-c", command},
}
cmd := &packer.RemoteCmd{Command: command}
if err := cmd.StartWithUi(comm, ui); err != nil {
return fmt.Errorf("Error executing command: %s", err)

View file

@ -26,7 +26,7 @@ type mountPathData struct {
// mount_device_cleanup CleanupFunc - To perform early cleanup
type StepMountDevice struct {
MountOptions []string
MountPartition int
MountPartition string
mountPath string
}
@ -35,6 +35,10 @@ func (s *StepMountDevice) Run(_ context.Context, state multistep.StateBag) multi
config := state.Get("config").(*Config)
ui := state.Get("ui").(packer.Ui)
device := state.Get("device").(string)
if config.NVMEDevicePath != "" {
// customizable device path for mounting NVME block devices on c5 and m5 HVM
device = config.NVMEDevicePath
}
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
var virtualizationType string
@ -47,6 +51,7 @@ func (s *StepMountDevice) Run(_ context.Context, state multistep.StateBag) multi
}
ctx := config.ctx
ctx.Data = &mountPathData{Device: filepath.Base(device)}
mountPath, err := interpolate.Render(config.MountPath, &ctx)
@ -75,8 +80,9 @@ func (s *StepMountDevice) Run(_ context.Context, state multistep.StateBag) multi
}
deviceMount := device
if virtualizationType == "hvm" {
deviceMount = fmt.Sprintf("%s%d", device, s.MountPartition)
if virtualizationType == "hvm" && s.MountPartition != "0" {
deviceMount = fmt.Sprintf("%s%s", device, s.MountPartition)
}
state.Put("deviceMount", deviceMount)
@ -97,7 +103,7 @@ func (s *StepMountDevice) Run(_ context.Context, state multistep.StateBag) multi
ui.Error(err.Error())
return multistep.ActionHalt
}
log.Printf("[DEBUG] (step mount) mount command is %s", mountCommand)
cmd := ShellCommand(mountCommand)
cmd.Stderr = stderr
if err := cmd.Run(); err != nil {

View file

@ -1,11 +1,11 @@
package common
import (
"errors"
"fmt"
"net"
"os"
"regexp"
"strings"
"time"
"github.com/hashicorp/packer/common/uuid"
@ -30,25 +30,26 @@ func (d *AmiFilterOptions) Empty() bool {
type RunConfig struct {
AssociatePublicIpAddress bool `mapstructure:"associate_public_ip_address"`
AvailabilityZone string `mapstructure:"availability_zone"`
DisableStopInstance bool `mapstructure:"disable_stop_instance"`
EbsOptimized bool `mapstructure:"ebs_optimized"`
EnableT2Unlimited bool `mapstructure:"enable_t2_unlimited"`
IamInstanceProfile string `mapstructure:"iam_instance_profile"`
InstanceInitiatedShutdownBehavior string `mapstructure:"shutdown_behavior"`
InstanceType string `mapstructure:"instance_type"`
RunTags map[string]string `mapstructure:"run_tags"`
SecurityGroupId string `mapstructure:"security_group_id"`
SecurityGroupIds []string `mapstructure:"security_group_ids"`
SourceAmi string `mapstructure:"source_ami"`
SourceAmiFilter AmiFilterOptions `mapstructure:"source_ami_filter"`
SpotPrice string `mapstructure:"spot_price"`
SpotPriceAutoProduct string `mapstructure:"spot_price_auto_product"`
DisableStopInstance bool `mapstructure:"disable_stop_instance"`
SecurityGroupId string `mapstructure:"security_group_id"`
SecurityGroupIds []string `mapstructure:"security_group_ids"`
TemporarySGSourceCidr string `mapstructure:"temporary_security_group_source_cidr"`
SubnetId string `mapstructure:"subnet_id"`
TemporaryKeyPairName string `mapstructure:"temporary_key_pair_name"`
TemporarySGSourceCidr string `mapstructure:"temporary_security_group_source_cidr"`
UserData string `mapstructure:"user_data"`
UserDataFile string `mapstructure:"user_data_file"`
WindowsPasswordTimeout time.Duration `mapstructure:"windows_password_timeout"`
VpcId string `mapstructure:"vpc_id"`
InstanceInitiatedShutdownBehavior string `mapstructure:"shutdown_behavior"`
WindowsPasswordTimeout time.Duration `mapstructure:"windows_password_timeout"`
// Communicator settings
Comm communicator.Config `mapstructure:",squash"`
@ -84,32 +85,39 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
c.SSHInterface != "public_dns" &&
c.SSHInterface != "private_dns" &&
c.SSHInterface != "" {
errs = append(errs, errors.New(fmt.Sprintf("Unknown interface type: %s", c.SSHInterface)))
errs = append(errs, fmt.Errorf("Unknown interface type: %s", c.SSHInterface))
}
if c.SSHKeyPairName != "" {
if c.Comm.Type == "winrm" && c.Comm.WinRMPassword == "" && c.Comm.SSHPrivateKey == "" {
errs = append(errs, errors.New("ssh_private_key_file must be provided to retrieve the winrm password when using ssh_keypair_name."))
errs = append(errs, fmt.Errorf("ssh_private_key_file must be provided to retrieve the winrm password when using ssh_keypair_name."))
} else if c.Comm.SSHPrivateKey == "" && !c.Comm.SSHAgentAuth {
errs = append(errs, errors.New("ssh_private_key_file must be provided or ssh_agent_auth enabled when ssh_keypair_name is specified."))
errs = append(errs, fmt.Errorf("ssh_private_key_file must be provided or ssh_agent_auth enabled when ssh_keypair_name is specified."))
}
}
if c.SourceAmi == "" && c.SourceAmiFilter.Empty() {
errs = append(errs, errors.New("A source_ami or source_ami_filter must be specified"))
errs = append(errs, fmt.Errorf("A source_ami or source_ami_filter must be specified"))
}
if c.InstanceType == "" {
errs = append(errs, errors.New("An instance_type must be specified"))
errs = append(errs, fmt.Errorf("An instance_type must be specified"))
}
if c.SpotPrice == "auto" {
if c.SpotPriceAutoProduct == "" {
errs = append(errs, errors.New(
errs = append(errs, fmt.Errorf(
"spot_price_auto_product must be specified when spot_price is auto"))
}
}
if c.SpotPriceAutoProduct != "" {
if c.SpotPrice != "auto" {
errs = append(errs, fmt.Errorf(
"spot_price should be set to auto when spot_price_auto_product is specified"))
}
}
if c.UserData != "" && c.UserDataFile != "" {
errs = append(errs, fmt.Errorf("Only one of user_data or user_data_file can be specified."))
} else if c.UserDataFile != "" {
@ -141,6 +149,18 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
errs = append(errs, fmt.Errorf("shutdown_behavior only accepts 'stop' or 'terminate' values."))
}
if c.EnableT2Unlimited {
if c.SpotPrice != "" {
errs = append(errs, fmt.Errorf("Error: T2 Unlimited cannot be used in conjuction with Spot Instances"))
}
firstDotIndex := strings.Index(c.InstanceType, ".")
if firstDotIndex == -1 {
errs = append(errs, fmt.Errorf("Error determining main Instance Type from: %s", c.InstanceType))
} else if c.InstanceType[0:firstDotIndex] != "t2" {
errs = append(errs, fmt.Errorf("Error: T2 Unlimited enabled with a non-T2 Instance Type: %s", c.InstanceType))
}
}
return errs
}

View file

@ -48,7 +48,7 @@ func TestRunConfigPrepare_InstanceType(t *testing.T) {
c := testConfig()
c.InstanceType = ""
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
t.Fatalf("Should error if an instance_type is not specified")
}
}
@ -56,14 +56,14 @@ func TestRunConfigPrepare_SourceAmi(t *testing.T) {
c := testConfig()
c.SourceAmi = ""
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
t.Fatalf("Should error if a source_ami (or source_ami_filter) is not specified")
}
}
func TestRunConfigPrepare_SourceAmiFilterBlank(t *testing.T) {
c := testConfigFilter()
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
t.Fatalf("Should error if source_ami_filter is empty or not specified (and source_ami is not specified)")
}
}
@ -79,17 +79,58 @@ func TestRunConfigPrepare_SourceAmiFilterGood(t *testing.T) {
}
}
func TestRunConfigPrepare_EnableT2UnlimitedGood(t *testing.T) {
c := testConfig()
// Must have a T2 instance type if T2 Unlimited is enabled
c.InstanceType = "t2.micro"
c.EnableT2Unlimited = true
err := c.Prepare(nil)
if len(err) > 0 {
t.Fatalf("err: %s", err)
}
}
func TestRunConfigPrepare_EnableT2UnlimitedBadInstanceType(t *testing.T) {
c := testConfig()
// T2 Unlimited cannot be used with instance types other than T2
c.InstanceType = "m5.large"
c.EnableT2Unlimited = true
err := c.Prepare(nil)
if len(err) != 1 {
t.Fatalf("Should error if T2 Unlimited is enabled with non-T2 instance_type")
}
}
func TestRunConfigPrepare_EnableT2UnlimitedBadWithSpotInstanceRequest(t *testing.T) {
c := testConfig()
// T2 Unlimited cannot be used with Spot Instances
c.InstanceType = "t2.micro"
c.EnableT2Unlimited = true
c.SpotPrice = "auto"
c.SpotPriceAutoProduct = "Linux/UNIX"
err := c.Prepare(nil)
if len(err) != 1 {
t.Fatalf("Should error if T2 Unlimited has been used in conjuntion with a Spot Price request")
}
}
func TestRunConfigPrepare_SpotAuto(t *testing.T) {
c := testConfig()
c.SpotPrice = "auto"
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
t.Fatalf("Should error if spot_price_auto_product is not set and spot_price is set to auto")
}
// Good - SpotPrice and SpotPriceAutoProduct are correctly set
c.SpotPriceAutoProduct = "foo"
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
c.SpotPrice = ""
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("Should error if spot_price is not set to auto and spot_price_auto_product is set")
}
}
func TestRunConfigPrepare_SSHPort(t *testing.T) {
@ -125,7 +166,7 @@ func TestRunConfigPrepare_UserData(t *testing.T) {
c.UserData = "foo"
c.UserDataFile = tf.Name()
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
t.Fatalf("Should error if user_data string and user_data_file have both been specified")
}
}
@ -137,7 +178,7 @@ func TestRunConfigPrepare_UserDataFile(t *testing.T) {
c.UserDataFile = "idontexistidontthink"
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
t.Fatalf("Should error if the file specified by user_data_file does not exist")
}
tf, err := ioutil.TempFile("", "packer")

View file

@ -24,6 +24,7 @@ type StepRunSourceInstance struct {
Ctx interpolate.Context
Debug bool
EbsOptimized bool
EnableT2Unlimited bool
ExpectedRootDevice string
IamInstanceProfile string
InstanceInitiatedShutdownBehavior string
@ -116,6 +117,11 @@ func (s *StepRunSourceInstance) Run(ctx context.Context, state multistep.StateBa
EbsOptimized: &s.EbsOptimized,
}
if s.EnableT2Unlimited {
creditOption := "unlimited"
runOpts.CreditSpecification = &ec2.CreditSpecificationRequest{CpuCredits: &creditOption}
}
// Collect tags for tagging on resource creation
var tagSpecs []*ec2.TagSpecification

View file

@ -148,6 +148,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
Ctx: b.config.ctx,
Debug: b.config.PackerDebug,
EbsOptimized: b.config.EbsOptimized,
EnableT2Unlimited: b.config.EnableT2Unlimited,
ExpectedRootDevice: "ebs",
IamInstanceProfile: b.config.IamInstanceProfile,
InstanceInitiatedShutdownBehavior: b.config.InstanceInitiatedShutdownBehavior,

View file

@ -162,6 +162,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
Ctx: b.config.ctx,
Debug: b.config.PackerDebug,
EbsOptimized: b.config.EbsOptimized,
EnableT2Unlimited: b.config.EnableT2Unlimited,
ExpectedRootDevice: "ebs",
IamInstanceProfile: b.config.IamInstanceProfile,
InstanceInitiatedShutdownBehavior: b.config.InstanceInitiatedShutdownBehavior,

View file

@ -145,6 +145,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
Ctx: b.config.ctx,
Debug: b.config.PackerDebug,
EbsOptimized: b.config.EbsOptimized,
EnableT2Unlimited: b.config.EnableT2Unlimited,
ExpectedRootDevice: "ebs",
IamInstanceProfile: b.config.IamInstanceProfile,
InstanceInitiatedShutdownBehavior: b.config.InstanceInitiatedShutdownBehavior,

View file

@ -230,6 +230,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
Ctx: b.config.ctx,
Debug: b.config.PackerDebug,
EbsOptimized: b.config.EbsOptimized,
EnableT2Unlimited: b.config.EnableT2Unlimited,
IamInstanceProfile: b.config.IamInstanceProfile,
InstanceType: b.config.InstanceType,
IsRestricted: b.config.IsChinaCloud() || b.config.IsGovCloud(),

View file

@ -10,11 +10,11 @@ import (
"strings"
"time"
packerAzureCommon "github.com/hashicorp/packer/builder/azure/common"
armstorage "github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2017-10-01/storage"
"github.com/Azure/azure-sdk-for-go/storage"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/dgrijalva/jwt-go"
packerAzureCommon "github.com/hashicorp/packer/builder/azure/common"
"github.com/hashicorp/packer/builder/azure/common/constants"
"github.com/hashicorp/packer/builder/azure/common/lin"
packerCommon "github.com/hashicorp/packer/common"
@ -52,6 +52,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
}
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
ui.Say("Running builder ...")
ctx, cancel := context.WithCancel(context.Background())
@ -90,6 +91,15 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
if err := resolver.Resolve(b.config); err != nil {
return nil, err
}
if b.config.ObjectID == "" {
b.config.ObjectID = getObjectIdFromToken(ui, spnCloud)
} else {
ui.Message("You have provided Object_ID which is no longer needed, azure packer builder determines this dynamically from the authentication token")
}
if b.config.ObjectID == "" && b.config.OSType != constants.Target_Linux {
return nil, fmt.Errorf("could not determine the ObjectID for the user, which is required for Windows builds")
}
if b.config.isManagedImage() {
group, err := azureClient.GroupsClient.Get(ctx, b.config.ManagedImageResourceGroupName)
@ -115,6 +125,9 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
return nil, fmt.Errorf("the managed image named %s already exists in the resource group %s, use the -force option to automatically delete it.", b.config.ManagedImageName, b.config.ManagedImageResourceGroupName)
}
}
} else {
// User is not using Managed Images to build, warning message here that this path is being deprecated
ui.Error("Warning: You are using Azure Packer Builder to create VHDs which is being deprecated, consider using Managed Images. Learn more http://aka.ms/packermanagedimage")
}
if b.config.BuildResourceGroupName != "" {
@ -344,6 +357,7 @@ func (b *Builder) configureStateBag(stateBag multistep.StateBag) {
stateBag.Put(constants.ArmIsManagedImage, b.config.isManagedImage())
stateBag.Put(constants.ArmManagedImageResourceGroupName, b.config.ManagedImageResourceGroupName)
stateBag.Put(constants.ArmManagedImageName, b.config.ManagedImageName)
stateBag.Put(constants.ArmAsyncResourceGroupDelete, b.config.AsyncResourceGroupDelete)
}
// Parameters that are only known at runtime after querying Azure.
@ -367,10 +381,17 @@ func (b *Builder) getServicePrincipalTokens(say func(string)) (*adal.ServicePrin
var err error
if b.config.useDeviceLogin {
servicePrincipalToken, err = packerAzureCommon.Authenticate(*b.config.cloudEnvironment, b.config.TenantID, say)
say("Getting auth token for Service management endpoint")
servicePrincipalToken, err = packerAzureCommon.Authenticate(*b.config.cloudEnvironment, b.config.TenantID, say, b.config.cloudEnvironment.ServiceManagementEndpoint)
if err != nil {
return nil, nil, err
}
say("Getting token for Vault resource")
servicePrincipalTokenVault, err = packerAzureCommon.Authenticate(*b.config.cloudEnvironment, b.config.TenantID, say, strings.TrimRight(b.config.cloudEnvironment.KeyVaultEndpoint, "/"))
if err != nil {
return nil, nil, err
}
} else {
auth := NewAuthenticate(*b.config.cloudEnvironment, b.config.ClientID, b.config.ClientSecret, b.config.TenantID)
@ -381,11 +402,39 @@ func (b *Builder) getServicePrincipalTokens(say func(string)) (*adal.ServicePrin
servicePrincipalTokenVault, err = auth.getServicePrincipalTokenWithResource(
strings.TrimRight(b.config.cloudEnvironment.KeyVaultEndpoint, "/"))
if err != nil {
return nil, nil, err
}
}
err = servicePrincipalToken.EnsureFresh()
if err != nil {
return nil, nil, err
}
err = servicePrincipalTokenVault.EnsureFresh()
if err != nil {
return nil, nil, err
}
return servicePrincipalToken, servicePrincipalTokenVault, nil
}
func getObjectIdFromToken(ui packer.Ui, token *adal.ServicePrincipalToken) string {
claims := jwt.MapClaims{}
var p jwt.Parser
var err error
_, _, err = p.ParseUnverified(token.OAuthToken(), claims)
if err != nil {
ui.Error(fmt.Sprintf("Failed to parse the token,Error: %s", err.Error()))
return ""
}
return claims["oid"].(string)
}

View file

@ -10,9 +10,9 @@ package arm
// * ARM_STORAGE_ACCOUNT
//
// The subscription in question should have a resource group
// called "packer-acceptance-test" in "West US" region. The
// called "packer-acceptance-test" in "South Central US" region. The
// storage account refered to in the above variable should
// be inside this resource group and in "West US" as well.
// be inside this resource group and in "South Central US" as well.
//
// In addition, the PACKER_ACC variable should also be set to
// a non-empty value to enable Packer acceptance tests and the
@ -23,9 +23,13 @@ package arm
import (
"testing"
"fmt"
builderT "github.com/hashicorp/packer/helper/builder/testing"
"os"
)
const DeviceLoginAcceptanceTest = "DEVICELOGIN_TEST"
func TestBuilderAcc_ManagedDisk_Windows(t *testing.T) {
builderT.Test(t, builderT.TestCase{
PreCheck: func() { testAccPreCheck(t) },
@ -34,6 +38,28 @@ func TestBuilderAcc_ManagedDisk_Windows(t *testing.T) {
})
}
func TestBuilderAcc_ManagedDisk_Windows_Build_Resource_Group(t *testing.T) {
builderT.Test(t, builderT.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Builder: &Builder{},
Template: testBuilderAccManagedDiskWindowsBuildResourceGroup,
})
}
func TestBuilderAcc_ManagedDisk_Windows_DeviceLogin(t *testing.T) {
if os.Getenv(DeviceLoginAcceptanceTest) == "" {
t.Skip(fmt.Sprintf(
"Device Login Acceptance tests skipped unless env '%s' set, as its requires manual step during execution",
DeviceLoginAcceptanceTest))
return
}
builderT.Test(t, builderT.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Builder: &Builder{},
Template: testBuilderAccManagedDiskWindowsDeviceLogin,
})
}
func TestBuilderAcc_ManagedDisk_Linux(t *testing.T) {
builderT.Test(t, builderT.TestCase{
PreCheck: func() { testAccPreCheck(t) },
@ -42,6 +68,20 @@ func TestBuilderAcc_ManagedDisk_Linux(t *testing.T) {
})
}
func TestBuilderAcc_ManagedDisk_Linux_DeviceLogin(t *testing.T) {
if os.Getenv(DeviceLoginAcceptanceTest) == "" {
t.Skip(fmt.Sprintf(
"Device Login Acceptance tests skipped unless env '%s' set, as its requires manual step during execution",
DeviceLoginAcceptanceTest))
return
}
builderT.Test(t, builderT.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Builder: &Builder{},
Template: testBuilderAccManagedDiskLinuxDeviceLogin,
})
}
func TestBuilderAcc_Blob_Windows(t *testing.T) {
builderT.Test(t, builderT.TestCase{
PreCheck: func() { testAccPreCheck(t) },
@ -65,8 +105,7 @@ const testBuilderAccManagedDiskWindows = `
"variables": {
"client_id": "{{env ` + "`ARM_CLIENT_ID`" + `}}",
"client_secret": "{{env ` + "`ARM_CLIENT_SECRET`" + `}}",
"subscription_id": "{{env ` + "`ARM_SUBSCRIPTION_ID`" + `}}",
"object_id": "{{env ` + "`ARM_OBJECT_ID`" + `}}"
"subscription_id": "{{env ` + "`ARM_SUBSCRIPTION_ID`" + `}}"
},
"builders": [{
"type": "test",
@ -74,7 +113,6 @@ const testBuilderAccManagedDiskWindows = `
"client_id": "{{user ` + "`client_id`" + `}}",
"client_secret": "{{user ` + "`client_secret`" + `}}",
"subscription_id": "{{user ` + "`subscription_id`" + `}}",
"object_id": "{{user ` + "`object_id`" + `}}",
"managed_image_resource_group_name": "packer-acceptance-test",
"managed_image_name": "testBuilderAccManagedDiskWindows-{{timestamp}}",
@ -89,8 +127,73 @@ const testBuilderAccManagedDiskWindows = `
"winrm_insecure": "true",
"winrm_timeout": "3m",
"winrm_username": "packer",
"async_resourcegroup_delete": "true",
"location": "West US",
"location": "South Central US",
"vm_size": "Standard_DS2_v2"
}]
}
`
const testBuilderAccManagedDiskWindowsBuildResourceGroup = `
{
"variables": {
"client_id": "{{env ` + "`ARM_CLIENT_ID`" + `}}",
"client_secret": "{{env ` + "`ARM_CLIENT_SECRET`" + `}}",
"subscription_id": "{{env ` + "`ARM_SUBSCRIPTION_ID`" + `}}"
},
"builders": [{
"type": "test",
"client_id": "{{user ` + "`client_id`" + `}}",
"client_secret": "{{user ` + "`client_secret`" + `}}",
"subscription_id": "{{user ` + "`subscription_id`" + `}}",
"build_resource_group_name" : "packer-acceptance-test",
"managed_image_resource_group_name": "packer-acceptance-test",
"managed_image_name": "testBuilderAccManagedDiskWindows-{{timestamp}}",
"os_type": "Windows",
"image_publisher": "MicrosoftWindowsServer",
"image_offer": "WindowsServer",
"image_sku": "2012-R2-Datacenter",
"communicator": "winrm",
"winrm_use_ssl": "true",
"winrm_insecure": "true",
"winrm_timeout": "3m",
"winrm_username": "packer",
"async_resourcegroup_delete": "true",
"vm_size": "Standard_DS2_v2"
}]
}
`
const testBuilderAccManagedDiskWindowsDeviceLogin = `
{
"variables": {
"subscription_id": "{{env ` + "`ARM_SUBSCRIPTION_ID`" + `}}"
},
"builders": [{
"type": "test",
"subscription_id": "{{user ` + "`subscription_id`" + `}}",
"managed_image_resource_group_name": "packer-acceptance-test",
"managed_image_name": "testBuilderAccManagedDiskWindowsDeviceLogin-{{timestamp}}",
"os_type": "Windows",
"image_publisher": "MicrosoftWindowsServer",
"image_offer": "WindowsServer",
"image_sku": "2012-R2-Datacenter",
"communicator": "winrm",
"winrm_use_ssl": "true",
"winrm_insecure": "true",
"winrm_timeout": "3m",
"winrm_username": "packer",
"location": "South Central US",
"vm_size": "Standard_DS2_v2"
}]
}
@ -118,7 +221,31 @@ const testBuilderAccManagedDiskLinux = `
"image_offer": "UbuntuServer",
"image_sku": "16.04-LTS",
"location": "West US",
"location": "South Central US",
"vm_size": "Standard_DS2_v2"
}]
}
`
const testBuilderAccManagedDiskLinuxDeviceLogin = `
{
"variables": {
"subscription_id": "{{env ` + "`ARM_SUBSCRIPTION_ID`" + `}}"
},
"builders": [{
"type": "test",
"subscription_id": "{{user ` + "`subscription_id`" + `}}",
"managed_image_resource_group_name": "packer-acceptance-test",
"managed_image_name": "testBuilderAccManagedDiskLinuxDeviceLogin-{{timestamp}}",
"os_type": "Linux",
"image_publisher": "Canonical",
"image_offer": "UbuntuServer",
"image_sku": "16.04-LTS",
"async_resourcegroup_delete": "true",
"location": "South Central US",
"vm_size": "Standard_DS2_v2"
}]
}
@ -157,7 +284,7 @@ const testBuilderAccBlobWindows = `
"winrm_timeout": "3m",
"winrm_username": "packer",
"location": "West US",
"location": "South Central US",
"vm_size": "Standard_DS2_v2"
}]
}
@ -188,7 +315,7 @@ const testBuilderAccBlobLinux = `
"image_offer": "UbuntuServer",
"image_sku": "16.04-LTS",
"location": "West US",
"location": "South Central US",
"vm_size": "Standard_DS2_v2"
}]
}

View file

@ -25,6 +25,7 @@ func TestStateBagShouldBePopulatedExpectedValues(t *testing.T) {
constants.ArmStorageAccountName,
constants.ArmVirtualMachineCaptureParameters,
constants.ArmPublicIPAddressName,
constants.ArmAsyncResourceGroupDelete,
}
for _, v := range expectedStateBagKeys {

View file

@ -151,6 +151,9 @@ type Config struct {
Comm communicator.Config `mapstructure:",squash"`
ctx *interpolate.Context
//Cleanup
AsyncResourceGroupDelete bool `mapstructure:"async_resourcegroup_delete"`
}
type keyVaultCertificate struct {
@ -490,9 +493,6 @@ func assertRequiredParametersSet(c *Config, errs *packer.MultiError) {
// readable by the ObjectID of the App. There may be another way to handle
// this case, but I am not currently aware of it - send feedback.
isUseDeviceLogin := func(c *Config) bool {
if c.OSType == constants.Target_Windows {
return false
}
return c.SubscriptionID != "" &&
c.ClientID == "" &&

View file

@ -2,13 +2,11 @@ package arm
import (
"fmt"
"strings"
"testing"
"time"
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2018-04-01/compute"
"github.com/hashicorp/packer/builder/azure/common/constants"
"github.com/hashicorp/packer/packer"
)
// List of configuration parameters that are required by the ARM builder.
@ -448,39 +446,6 @@ func TestUserDeviceLoginIsEnabledForLinux(t *testing.T) {
}
}
func TestUseDeviceLoginIsDisabledForWindows(t *testing.T) {
config := map[string]string{
"capture_name_prefix": "ignore",
"capture_container_name": "ignore",
"image_offer": "ignore",
"image_publisher": "ignore",
"image_sku": "ignore",
"location": "ignore",
"storage_account": "ignore",
"resource_group_name": "ignore",
"subscription_id": "ignore",
"os_type": constants.Target_Windows,
"communicator": "none",
}
_, _, err := newConfig(config, getPackerConfiguration())
if err == nil {
t.Fatal("Expected test to fail, but it succeeded")
}
multiError, _ := err.(*packer.MultiError)
if len(multiError.Errors) != 2 {
t.Errorf("Expected to find 2 errors, but found %d errors", len(multiError.Errors))
}
if !strings.Contains(err.Error(), "client_id must be specified") {
t.Error("Expected to find error for 'client_id must be specified")
}
if !strings.Contains(err.Error(), "client_secret must be specified") {
t.Error("Expected to find error for 'client_secret must be specified")
}
}
func TestConfigShouldRejectMalformedCaptureNamePrefix(t *testing.T) {
config := map[string]string{
"capture_container_name": "ignore",
@ -1264,6 +1229,73 @@ func TestConfigShouldAllowTempNameOverrides(t *testing.T) {
}
}
func TestConfigShouldAllowAsyncResourceGroupOverride(t *testing.T) {
config := map[string]interface{}{
"image_offer": "ignore",
"image_publisher": "ignore",
"image_sku": "ignore",
"location": "ignore",
"subscription_id": "ignore",
"communicator": "none",
"os_type": "linux",
"managed_image_name": "ignore",
"managed_image_resource_group_name": "ignore",
"async_resourcegroup_delete": "true",
}
c, _, err := newConfig(config, getPackerConfiguration())
if err != nil {
t.Errorf("newConfig failed with %q", err)
}
if c.AsyncResourceGroupDelete != true {
t.Errorf("expected async_resourcegroup_delete to be %q, but got %t", "async_resourcegroup_delete", c.AsyncResourceGroupDelete)
}
}
func TestConfigShouldAllowAsyncResourceGroupOverrideNoValue(t *testing.T) {
config := map[string]interface{}{
"image_offer": "ignore",
"image_publisher": "ignore",
"image_sku": "ignore",
"location": "ignore",
"subscription_id": "ignore",
"communicator": "none",
"os_type": "linux",
"managed_image_name": "ignore",
"managed_image_resource_group_name": "ignore",
}
c, _, err := newConfig(config, getPackerConfiguration())
if err != nil {
t.Errorf("newConfig failed with %q", err)
}
if c.AsyncResourceGroupDelete != false {
t.Errorf("expected async_resourcegroup_delete to be %q, but got %t", "async_resourcegroup_delete", c.AsyncResourceGroupDelete)
}
}
func TestConfigShouldAllowAsyncResourceGroupOverrideBadValue(t *testing.T) {
config := map[string]interface{}{
"image_offer": "ignore",
"image_publisher": "ignore",
"image_sku": "ignore",
"location": "ignore",
"subscription_id": "ignore",
"communicator": "none",
"os_type": "linux",
"managed_image_name": "ignore",
"managed_image_resource_group_name": "ignore",
"async_resourcegroup_delete": "asdasda",
}
c, _, err := newConfig(config, getPackerConfiguration())
if err != nil && c == nil {
t.Log("newConfig failed which is expected ", err)
}
}
func getArmBuilderConfiguration() map[string]string {
m := make(map[string]string)
for _, v := range requiredConfigValues {

View file

@ -118,14 +118,20 @@ func (s *StepCreateResourceGroup) Cleanup(state multistep.StateBag) {
ctx := context.TODO()
f, err := s.client.GroupsClient.Delete(ctx, resourceGroupName)
if err == nil {
err = f.WaitForCompletion(ctx, s.client.GroupsClient.Client)
if state.Get(constants.ArmAsyncResourceGroupDelete).(bool) {
s.say(fmt.Sprintf("\n Not waiting for Resource Group delete as requested by user. Resource Group Name is %s", resourceGroupName))
} else {
err = f.WaitForCompletion(ctx, s.client.GroupsClient.Client)
}
}
if err != nil {
ui.Error(fmt.Sprintf("Error deleting resource group. Please delete it manually.\n\n"+
"Name: %s\n"+
"Error: %s", resourceGroupName, err))
return
}
if !state.Get(constants.ArmAsyncResourceGroupDelete).(bool) {
ui.Say("Resource group has been deleted.")
}
ui.Say("Resource group has been deleted.")
}
}

View file

@ -53,7 +53,13 @@ func (s *StepDeleteResourceGroup) deleteResourceGroup(ctx context.Context, state
s.say("\nThe resource group was created by Packer, deleting ...")
f, err := s.client.GroupsClient.Delete(ctx, resourceGroupName)
if err == nil {
f.WaitForCompletion(ctx, s.client.GroupsClient.Client)
if state.Get(constants.ArmAsyncResourceGroupDelete).(bool) {
// No need to wait for the complition for delete if request is Accepted
s.say(fmt.Sprintf("\nResource Group is being deleted, not waiting for deletion due to config. Resource Group Name '%s'", resourceGroupName))
} else {
f.WaitForCompletion(ctx, s.client.GroupsClient.Client)
}
}
if err != nil {
@ -76,6 +82,7 @@ func (s *StepDeleteResourceGroup) deleteDeploymentResources(ctx context.Context,
deploymentOperation := deploymentOperations.Value()
// Sometimes an empty operation is added to the list by Azure
if deploymentOperation.Properties.TargetResource == nil {
deploymentOperations.Next()
continue
}

View file

@ -185,6 +185,7 @@ func (s *StepDeployTemplate) Cleanup(state multistep.StateBag) {
deploymentOperation := deploymentOperations.Value()
// Sometimes an empty operation is added to the list by Azure
if deploymentOperation.Properties.TargetResource == nil {
deploymentOperations.Next()
continue
}
ui.Say(fmt.Sprintf(" -> %s : '%s'",

View file

@ -35,4 +35,5 @@ const (
ArmManagedImageResourceGroupName string = "arm.ManagedImageResourceGroupName"
ArmManagedImageLocation string = "arm.ManagedImageLocation"
ArmManagedImageName string = "arm.ManagedImageName"
ArmAsyncResourceGroupDelete string = "arm.AsyncResourceGroupDelete"
)

View file

@ -7,6 +7,7 @@ import (
"os"
"path/filepath"
"regexp"
"strings"
"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2016-06-01/subscriptions"
"github.com/Azure/go-autorest/autorest"
@ -40,8 +41,10 @@ var (
// Authenticate fetches a token from the local file cache or initiates a consent
// flow and waits for token to be obtained.
func Authenticate(env azure.Environment, tenantID string, say func(string)) (*adal.ServicePrincipalToken, error) {
func Authenticate(env azure.Environment, tenantID string, say func(string), scope string) (*adal.ServicePrincipalToken, error) {
clientID, ok := clientIDs[env.Name]
var resourceid string
if !ok {
return nil, fmt.Errorf("packer-azure application not set up for Azure environment %q", env.Name)
}
@ -53,9 +56,14 @@ func Authenticate(env azure.Environment, tenantID string, say func(string)) (*ad
// for AzurePublicCloud (https://management.core.windows.net/), this old
// Service Management scope covers both ASM and ARM.
apiScope := env.ServiceManagementEndpoint
tokenPath := tokenCachePath(tenantID)
if strings.Contains(scope, "vault") {
resourceid = "vault"
} else {
resourceid = "mgmt"
}
tokenPath := tokenCachePath(tenantID + resourceid)
saveToken := mkTokenCallback(tokenPath)
saveTokenCallback := func(t adal.Token) error {
say("Azure token expired. Saving the refreshed token...")
@ -63,41 +71,18 @@ func Authenticate(env azure.Environment, tenantID string, say func(string)) (*ad
}
// Lookup the token cache file for an existing token.
spt, err := tokenFromFile(say, *oauthCfg, tokenPath, clientID, apiScope, saveTokenCallback)
spt, err := tokenFromFile(say, *oauthCfg, tokenPath, clientID, scope, saveTokenCallback)
if err != nil {
return nil, err
}
if spt != nil {
say(fmt.Sprintf("Auth token found in file: %s", tokenPath))
// NOTE(ahmetalpbalkan): The token file we found may contain an
// expired access_token. In that case, the first call to Azure SDK will
// attempt to refresh the token using refresh_token, which might have
// expired[1], in that case we will get an error and we shall remove the
// token file and initiate token flow again so that the user would not
// need removing the token cache file manually.
//
// [1]: expiration date of refresh_token is not returned in AAD /token
// response, we just know it is 14 days. Therefore users token
// will go stale every 14 days and we will delete the token file,
// re-initiate the device flow.
say("Validating the token.")
if err = validateToken(env, spt); err != nil {
say(fmt.Sprintf("Error: %v", err))
say("Stored Azure credentials expired. Please reauthenticate.")
say(fmt.Sprintf("Deleting %s", tokenPath))
if err := os.RemoveAll(tokenPath); err != nil {
return nil, fmt.Errorf("Error deleting stale token file: %v", err)
}
} else {
say("Token works.")
return spt, nil
}
return spt, nil
}
// Start an OAuth 2.0 device flow
say(fmt.Sprintf("Initiating device flow: %s", tokenPath))
spt, err = tokenFromDeviceFlow(say, *oauthCfg, clientID, apiScope)
spt, err = tokenFromDeviceFlow(say, *oauthCfg, clientID, scope)
if err != nil {
return nil, err
}
@ -183,20 +168,6 @@ func mkTokenCallback(path string) adal.TokenRefreshCallback {
}
}
// validateToken makes a call to Azure SDK with given token, essentially making
// sure if the access_token valid, if not it uses SDKs functionality to
// automatically refresh the token using refresh_token (which might have
// expired). This check is essentially to make sure refresh_token is good.
func validateToken(env azure.Environment, token *adal.ServicePrincipalToken) error {
c := subscriptions.NewClientWithBaseURI(env.ResourceManagerEndpoint)
c.Authorizer = autorest.NewBearerAuthorizer(token)
_, err := c.List(context.TODO())
if err != nil {
return fmt.Errorf("Token validity check failed: %v", err)
}
return nil
}
// FindTenantID figures out the AAD tenant ID of the subscription by making an
// unauthenticated request to the Get Subscription Details endpoint and parses
// the value from WWW-Authenticate header.

View file

@ -111,6 +111,7 @@ func (s *stepSnapshot) Run(_ context.Context, state multistep.StateBag) multiste
ui.Error(err.Error())
return multistep.ActionHalt
}
snapshotRegions = append(snapshotRegions, c.Region)
log.Printf("Snapshot image ID: %d", imageId)
state.Put("snapshot_image_id", imageId)

View file

@ -168,7 +168,7 @@ func (d *driverGCE) DeleteDisk(zone, name string) (<-chan error, error) {
}
func (d *driverGCE) GetImage(name string, fromFamily bool) (*Image, error) {
projects := []string{d.projectId, "centos-cloud", "coreos-cloud", "cos-cloud", "debian-cloud", "google-containers", "opensuse-cloud", "rhel-cloud", "suse-cloud", "ubuntu-os-cloud", "windows-cloud", "gce-nvme", "windows-sql-cloud"}
projects := []string{d.projectId, "centos-cloud", "coreos-cloud", "cos-cloud", "debian-cloud", "google-containers", "opensuse-cloud", "rhel-cloud", "suse-cloud", "ubuntu-os-cloud", "windows-cloud", "gce-nvme", "windows-sql-cloud", "rhel-sap-cloud"}
var errs error
for _, project := range projects {
image, err := d.GetImageFromProject(project, name, fromFamily)

View file

@ -1,5 +1,9 @@
package common
import (
"context"
)
// A driver is able to talk to HyperV and perform certain
// operations with it. Some of the operations on here may seem overly
// specific, but they were built specifically in mind to handle features
@ -66,7 +70,7 @@ type Driver interface {
DeleteVirtualSwitch(string) error
CreateVirtualMachine(string, string, string, string, int64, int64, int64, string, uint, bool) error
CreateVirtualMachine(string, string, string, string, int64, int64, int64, string, uint, bool, bool) error
AddVirtualMachineHardDrive(string, string, string, int64, int64, string) error
@ -82,7 +86,7 @@ type Driver interface {
SetVirtualMachineDynamicMemory(string, bool) error
SetVirtualMachineSecureBoot(string, bool) error
SetVirtualMachineSecureBoot(string, bool, string) error
SetVirtualMachineVirtualizationExtensions(string, bool) error
@ -109,4 +113,10 @@ type Driver interface {
MountFloppyDrive(string, string) error
UnmountFloppyDrive(string) error
// Connect connects to a VM specified by the name given.
Connect(string) (context.CancelFunc, error)
// Disconnect disconnects to a VM specified by the context cancel function.
Disconnect(context.CancelFunc)
}

View file

@ -1,5 +1,9 @@
package common
import (
"context"
)
type DriverMock struct {
IsRunning_Called bool
IsRunning_VmName string
@ -127,6 +131,7 @@ type DriverMock struct {
CreateVirtualMachine_SwitchName string
CreateVirtualMachine_Generation uint
CreateVirtualMachine_DifferentialDisk bool
CreateVirtualMachine_FixedVHD bool
CreateVirtualMachine_Err error
CloneVirtualMachine_Called bool
@ -160,10 +165,11 @@ type DriverMock struct {
SetVirtualMachineDynamicMemory_Enable bool
SetVirtualMachineDynamicMemory_Err error
SetVirtualMachineSecureBoot_Called bool
SetVirtualMachineSecureBoot_VmName string
SetVirtualMachineSecureBoot_Enable bool
SetVirtualMachineSecureBoot_Err error
SetVirtualMachineSecureBoot_Called bool
SetVirtualMachineSecureBoot_VmName string
SetVirtualMachineSecureBoot_TemplateName string
SetVirtualMachineSecureBoot_Enable bool
SetVirtualMachineSecureBoot_Err error
SetVirtualMachineVirtualizationExtensions_Called bool
SetVirtualMachineVirtualizationExtensions_VmName string
@ -238,6 +244,14 @@ type DriverMock struct {
UnmountFloppyDrive_Called bool
UnmountFloppyDrive_VmName string
UnmountFloppyDrive_Err error
Connect_Called bool
Connect_VmName string
Connect_Cancel context.CancelFunc
Connect_Err error
Disconnect_Called bool
Disconnect_Cancel context.CancelFunc
}
func (d *DriverMock) IsRunning(vmName string) (bool, error) {
@ -390,7 +404,7 @@ func (d *DriverMock) AddVirtualMachineHardDrive(vmName string, vhdFile string, v
return d.AddVirtualMachineHardDrive_Err
}
func (d *DriverMock) CreateVirtualMachine(vmName string, path string, harddrivePath string, vhdPath string, ram int64, diskSize int64, diskBlockSize int64, switchName string, generation uint, diffDisks bool) error {
func (d *DriverMock) CreateVirtualMachine(vmName string, path string, harddrivePath string, vhdPath string, ram int64, diskSize int64, diskBlockSize int64, switchName string, generation uint, diffDisks bool, fixedVHD bool) error {
d.CreateVirtualMachine_Called = true
d.CreateVirtualMachine_VmName = vmName
d.CreateVirtualMachine_Path = path
@ -446,10 +460,11 @@ func (d *DriverMock) SetVirtualMachineDynamicMemory(vmName string, enable bool)
return d.SetVirtualMachineDynamicMemory_Err
}
func (d *DriverMock) SetVirtualMachineSecureBoot(vmName string, enable bool) error {
func (d *DriverMock) SetVirtualMachineSecureBoot(vmName string, enable bool, templateName string) error {
d.SetVirtualMachineSecureBoot_Called = true
d.SetVirtualMachineSecureBoot_VmName = vmName
d.SetVirtualMachineSecureBoot_Enable = enable
d.SetVirtualMachineSecureBoot_TemplateName = templateName
return d.SetVirtualMachineSecureBoot_Err
}
@ -550,3 +565,14 @@ func (d *DriverMock) UnmountFloppyDrive(vmName string) error {
d.UnmountFloppyDrive_VmName = vmName
return d.UnmountFloppyDrive_Err
}
func (d *DriverMock) Connect(vmName string) (context.CancelFunc, error) {
d.Connect_Called = true
d.Connect_VmName = vmName
return d.Connect_Cancel, d.Connect_Err
}
func (d *DriverMock) Disconnect(cancel context.CancelFunc) {
d.Disconnect_Called = true
d.Disconnect_Cancel = cancel
}

View file

@ -1,6 +1,7 @@
package common
import (
"context"
"fmt"
"log"
"runtime"
@ -178,8 +179,8 @@ func (d *HypervPS4Driver) AddVirtualMachineHardDrive(vmName string, vhdFile stri
return hyperv.AddVirtualMachineHardDiskDrive(vmName, vhdFile, vhdName, vhdSizeBytes, diskBlockSize, controllerType)
}
func (d *HypervPS4Driver) CreateVirtualMachine(vmName string, path string, harddrivePath string, vhdPath string, ram int64, diskSize int64, diskBlockSize int64, switchName string, generation uint, diffDisks bool) error {
return hyperv.CreateVirtualMachine(vmName, path, harddrivePath, vhdPath, ram, diskSize, diskBlockSize, switchName, generation, diffDisks)
func (d *HypervPS4Driver) CreateVirtualMachine(vmName string, path string, harddrivePath string, vhdPath string, ram int64, diskSize int64, diskBlockSize int64, switchName string, generation uint, diffDisks bool, fixedVHD bool) error {
return hyperv.CreateVirtualMachine(vmName, path, harddrivePath, vhdPath, ram, diskSize, diskBlockSize, switchName, generation, diffDisks, fixedVHD)
}
func (d *HypervPS4Driver) CloneVirtualMachine(cloneFromVmxcPath string, cloneFromVmName string, cloneFromSnapshotName string, cloneAllSnapshots bool, vmName string, path string, harddrivePath string, ram int64, switchName string) error {
@ -202,8 +203,8 @@ func (d *HypervPS4Driver) SetVirtualMachineDynamicMemory(vmName string, enable b
return hyperv.SetVirtualMachineDynamicMemory(vmName, enable)
}
func (d *HypervPS4Driver) SetVirtualMachineSecureBoot(vmName string, enable bool) error {
return hyperv.SetVirtualMachineSecureBoot(vmName, enable)
func (d *HypervPS4Driver) SetVirtualMachineSecureBoot(vmName string, enable bool, templateName string) error {
return hyperv.SetVirtualMachineSecureBoot(vmName, enable, templateName)
}
func (d *HypervPS4Driver) SetVirtualMachineVirtualizationExtensions(vmName string, enable bool) error {
@ -347,3 +348,14 @@ func (d *HypervPS4Driver) verifyHypervPermissions() error {
return nil
}
// Connect connects to a VM specified by the name given.
func (d *HypervPS4Driver) Connect(vmName string) (context.CancelFunc, error) {
return hyperv.ConnectVirtualMachine(vmName)
}
// Disconnect disconnects to a VM specified by calling the context cancel function returned
// from Connect.
func (d *HypervPS4Driver) Disconnect(cancel context.CancelFunc) {
hyperv.DisconnectVirtualMachine(cancel)
}

View file

@ -27,6 +27,7 @@ type StepCloneVM struct {
EnableMacSpoofing bool
EnableDynamicMemory bool
EnableSecureBoot bool
SecureBootTemplate string
EnableVirtualizationExtensions bool
MacAddress string
}
@ -99,7 +100,8 @@ func (s *StepCloneVM) Run(_ context.Context, state multistep.StateBag) multistep
}
if generation == 2 {
err = driver.SetVirtualMachineSecureBoot(s.VMName, s.EnableSecureBoot)
err = driver.SetVirtualMachineSecureBoot(s.VMName, s.EnableSecureBoot, s.SecureBootTemplate)
if err != nil {
err := fmt.Errorf("Error setting secure boot: %s", err)
state.Put("error", err)

View file

@ -27,12 +27,14 @@ type StepCreateVM struct {
EnableMacSpoofing bool
EnableDynamicMemory bool
EnableSecureBoot bool
SecureBootTemplate string
EnableVirtualizationExtensions bool
AdditionalDiskSize []uint
DifferencingDisk bool
MacAddress string
SkipExport bool
OutputDir string
FixedVHD bool
}
func (s *StepCreateVM) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
@ -67,7 +69,7 @@ func (s *StepCreateVM) Run(_ context.Context, state multistep.StateBag) multiste
diskSize := int64(s.DiskSize * 1024 * 1024)
diskBlockSize := int64(s.DiskBlockSize * 1024 * 1024)
err := driver.CreateVirtualMachine(s.VMName, path, harddrivePath, vhdPath, ramSize, diskSize, diskBlockSize, s.SwitchName, s.Generation, s.DifferencingDisk)
err := driver.CreateVirtualMachine(s.VMName, path, harddrivePath, vhdPath, ramSize, diskSize, diskBlockSize, s.SwitchName, s.Generation, s.DifferencingDisk, s.FixedVHD)
if err != nil {
err := fmt.Errorf("Error creating virtual machine: %s", err)
state.Put("error", err)
@ -102,7 +104,7 @@ func (s *StepCreateVM) Run(_ context.Context, state multistep.StateBag) multiste
}
if s.Generation == 2 {
err = driver.SetVirtualMachineSecureBoot(s.VMName, s.EnableSecureBoot)
err = driver.SetVirtualMachineSecureBoot(s.VMName, s.EnableSecureBoot, s.SecureBootTemplate)
if err != nil {
err := fmt.Errorf("Error setting secure boot: %s", err)
state.Put("error", err)

View file

@ -3,13 +3,16 @@ package common
import (
"context"
"fmt"
"log"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
type StepRun struct {
vmName string
GuiCancelFunc context.CancelFunc
Headless bool
vmName string
}
func (s *StepRun) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
@ -29,6 +32,13 @@ func (s *StepRun) Run(_ context.Context, state multistep.StateBag) multistep.Ste
s.vmName = vmName
if !s.Headless {
ui.Say("Attempting to connect with vmconnect...")
s.GuiCancelFunc, err = driver.Connect(vmName)
if err != nil {
log.Printf(fmt.Sprintf("Non-fatal error starting vmconnect: %s. continuing...", err))
}
}
return multistep.ActionContinue
}
@ -40,6 +50,11 @@ func (s *StepRun) Cleanup(state multistep.StateBag) {
driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packer.Ui)
if !s.Headless && s.GuiCancelFunc != nil {
ui.Say("Disconnecting from vmconnect...")
s.GuiCancelFunc()
}
if running, _ := driver.IsRunning(s.vmName); running {
if err := driver.Stop(s.vmName); err != nil {
ui.Error(fmt.Sprintf("Error shutting down VM: %s", err))

View file

@ -24,6 +24,7 @@ const (
DefaultDiskSize = 40 * 1024 // ~40GB
MinDiskSize = 256 // 256MB
MaxDiskSize = 64 * 1024 * 1024 // 64TB
MaxVHDSize = 2040 * 1024 // 2040GB
DefaultDiskBlockSize = 32 // 32MB
MinDiskBlockSize = 1 // 1MB
@ -91,6 +92,7 @@ type Config struct {
EnableMacSpoofing bool `mapstructure:"enable_mac_spoofing"`
EnableDynamicMemory bool `mapstructure:"enable_dynamic_memory"`
EnableSecureBoot bool `mapstructure:"enable_secure_boot"`
SecureBootTemplate string `mapstructure:"secure_boot_template"`
EnableVirtualizationExtensions bool `mapstructure:"enable_virtualization_extensions"`
TempPath string `mapstructure:"temp_path"`
@ -110,6 +112,11 @@ type Config struct {
// Use differencing disk
DifferencingDisk bool `mapstructure:"differencing_disk"`
// Create the VM with a Fixed VHD format disk instead of Dynamic VHDX
FixedVHD bool `mapstructure:"use_fixed_vhd_format"`
Headless bool `mapstructure:"headless"`
ctx interpolate.Context
}
@ -270,6 +277,21 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
}
}
if b.config.Generation > 1 && b.config.FixedVHD {
err = errors.New("Fixed VHD disks are only supported on Generation 1 virtual machines.")
errs = packer.MultiErrorAppend(errs, err)
}
if !b.config.SkipCompaction && b.config.FixedVHD {
err = errors.New("Fixed VHD disks do not support compaction.")
errs = packer.MultiErrorAppend(errs, err)
}
if b.config.DifferencingDisk && b.config.FixedVHD {
err = errors.New("Fixed VHD disks are not supported with differencing disks.")
errs = packer.MultiErrorAppend(errs, err)
}
// Warnings
if b.config.ShutdownCommand == "" {
@ -373,12 +395,14 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
EnableMacSpoofing: b.config.EnableMacSpoofing,
EnableDynamicMemory: b.config.EnableDynamicMemory,
EnableSecureBoot: b.config.EnableSecureBoot,
SecureBootTemplate: b.config.SecureBootTemplate,
EnableVirtualizationExtensions: b.config.EnableVirtualizationExtensions,
AdditionalDiskSize: b.config.AdditionalDiskSize,
DifferencingDisk: b.config.DifferencingDisk,
SkipExport: b.config.SkipExport,
OutputDir: b.config.OutputDir,
MacAddress: b.config.MacAddress,
FixedVHD: b.config.FixedVHD,
},
&hypervcommon.StepEnableIntegrationService{},
@ -405,7 +429,9 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
SwitchVlanId: b.config.SwitchVlanId,
},
&hypervcommon.StepRun{},
&hypervcommon.StepRun{
Headless: b.config.Headless,
},
&hypervcommon.StepTypeBootCommand{
BootCommand: b.config.FlatBootCommand(),
@ -501,8 +527,10 @@ func (b *Builder) checkDiskSize() error {
if b.config.DiskSize < MinDiskSize {
return fmt.Errorf("disk_size: Virtual machine requires disk space >= %v GB, but defined: %v", MinDiskSize, b.config.DiskSize/1024)
} else if b.config.DiskSize > MaxDiskSize {
} else if b.config.DiskSize > MaxDiskSize && !b.config.FixedVHD {
return fmt.Errorf("disk_size: Virtual machine requires disk space <= %v GB, but defined: %v", MaxDiskSize, b.config.DiskSize/1024)
} else if b.config.DiskSize > MaxVHDSize && b.config.FixedVHD {
return fmt.Errorf("disk_size: Virtual machine requires disk space <= %v GB, but defined: %v", MaxVHDSize/1024, b.config.DiskSize/1024)
}
return nil

View file

@ -139,6 +139,59 @@ func TestBuilderPrepare_DiskBlockSize(t *testing.T) {
}
}
func TestBuilderPrepare_FixedVHDFormat(t *testing.T) {
var b Builder
config := testConfig()
config["use_fixed_vhd_format"] = true
config["generation"] = 1
config["skip_compaction"] = true
config["differencing_disk"] = false
//use_fixed_vhd_format should work with generation = 1, skip_compaction = true, and differencing_disk = false
warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("bad err: %s", err)
}
//use_fixed_vhd_format should not work with differencing_disk = true
config["differencing_disk"] = true
b = Builder{}
warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
config["differencing_disk"] = false
//use_fixed_vhd_format should not work with skip_compaction = false
config["skip_compaction"] = false
b = Builder{}
warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
config["skip_compaction"] = true
//use_fixed_vhd_format should not work with generation = 2
config["generation"] = 2
b = Builder{}
warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_FloppyFiles(t *testing.T) {
var b Builder
config := testConfig()

View file

@ -86,10 +86,11 @@ type Config struct {
VlanId string `mapstructure:"vlan_id"`
Cpu uint `mapstructure:"cpu"`
Generation uint
EnableMacSpoofing bool `mapstructure:"enable_mac_spoofing"`
EnableDynamicMemory bool `mapstructure:"enable_dynamic_memory"`
EnableSecureBoot bool `mapstructure:"enable_secure_boot"`
EnableVirtualizationExtensions bool `mapstructure:"enable_virtualization_extensions"`
EnableMacSpoofing bool `mapstructure:"enable_mac_spoofing"`
EnableDynamicMemory bool `mapstructure:"enable_dynamic_memory"`
EnableSecureBoot bool `mapstructure:"enable_secure_boot"`
SecureBootTemplate string `mapstructure:"secure_boot_template"`
EnableVirtualizationExtensions bool `mapstructure:"enable_virtualization_extensions"`
Communicator string `mapstructure:"communicator"`
@ -97,6 +98,8 @@ type Config struct {
SkipExport bool `mapstructure:"skip_export"`
Headless bool `mapstructure:"headless"`
ctx interpolate.Context
}
@ -405,6 +408,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
EnableMacSpoofing: b.config.EnableMacSpoofing,
EnableDynamicMemory: b.config.EnableDynamicMemory,
EnableSecureBoot: b.config.EnableSecureBoot,
SecureBootTemplate: b.config.SecureBootTemplate,
EnableVirtualizationExtensions: b.config.EnableVirtualizationExtensions,
MacAddress: b.config.MacAddress,
},
@ -434,7 +438,9 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
SwitchVlanId: b.config.SwitchVlanId,
},
&hypervcommon.StepRun{},
&hypervcommon.StepRun{
Headless: b.config.Headless,
},
&hypervcommon.StepTypeBootCommand{
BootCommand: b.config.FlatBootCommand(),

View file

@ -2,14 +2,14 @@ package openstack
import (
"crypto/tls"
"fmt"
"os"
"crypto/x509"
"fmt"
"io/ioutil"
"os"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack"
"github.com/gophercloud/utils/openstack/clientconfig"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/packer/template/interpolate"
)
@ -30,6 +30,8 @@ type AccessConfig struct {
CACertFile string `mapstructure:"cacert"`
ClientCertFile string `mapstructure:"cert"`
ClientKeyFile string `mapstructure:"key"`
Token string `mapstructure:"token"`
Cloud string `mapstructure:"cloud"`
osClient *gophercloud.ProviderClient
}
@ -42,10 +44,6 @@ func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error {
return []error{fmt.Errorf("Invalid endpoint type provided")}
}
if c.Region == "" {
c.Region = os.Getenv("OS_REGION_NAME")
}
// Legacy RackSpace stuff. We're keeping this around to keep things BC.
if c.Password == "" {
c.Password = os.Getenv("SDK_PASSWORD")
@ -59,6 +57,15 @@ func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error {
if c.Username == "" {
c.Username = os.Getenv("SDK_USERNAME")
}
// End RackSpace
if c.Cloud == "" {
c.Cloud = os.Getenv("OS_CLOUD")
}
if c.Region == "" {
c.Region = os.Getenv("OS_REGION_NAME")
}
if c.CACertFile == "" {
c.CACertFile = os.Getenv("OS_CACERT")
}
@ -69,8 +76,39 @@ func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error {
c.ClientKeyFile = os.Getenv("OS_KEY")
}
// Get as much as possible from the end
ao, _ := openstack.AuthOptionsFromEnv()
clientOpts := new(clientconfig.ClientOpts)
// If a cloud entry was given, base AuthOptions on a clouds.yaml file.
if c.Cloud != "" {
clientOpts.Cloud = c.Cloud
cloud, err := clientconfig.GetCloudFromYAML(clientOpts)
if err != nil {
return []error{err}
}
if c.Region == "" && cloud.RegionName != "" {
c.Region = cloud.RegionName
}
} else {
authInfo := &clientconfig.AuthInfo{
AuthURL: c.IdentityEndpoint,
DomainID: c.DomainID,
DomainName: c.DomainName,
Password: c.Password,
ProjectID: c.TenantID,
ProjectName: c.TenantName,
Token: c.Token,
Username: c.Username,
UserID: c.UserID,
}
clientOpts.AuthInfo = authInfo
}
ao, err := clientconfig.AuthOptions(clientOpts)
if err != nil {
return []error{err}
}
// Make sure we reauth as needed
ao.AllowReauth = true
@ -87,6 +125,7 @@ func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error {
{&c.TenantName, &ao.TenantName},
{&c.DomainID, &ao.DomainID},
{&c.DomainName, &ao.DomainName},
{&c.Token, &ao.TokenID},
}
for _, s := range overrides {
if *s.From != "" {
@ -132,7 +171,7 @@ func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error {
client.HTTPClient.Transport = transport
// Auth
err = openstack.Authenticate(client, ao)
err = openstack.Authenticate(client, *ao)
if err != nil {
return []error{err}
}

View file

@ -58,6 +58,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
},
&stepCreateInstance{},
&stepInstanceInfo{},
&stepGetDefaultCredentials{
Debug: b.config.PackerDebug,
Comm: &b.config.Comm,
BuildName: b.config.PackerBuildName,
},
&communicator.StepConnect{
Config: &b.config.Comm,
Host: ocommon.CommHost,

View file

@ -112,6 +112,17 @@ func (d *driverOCI) GetInstanceIP(id string) (string, error) {
return *vnic.PublicIp, nil
}
func (d *driverOCI) GetInstanceInitialCredentials(id string) (string, string, error) {
credentials, err := d.computeClient.GetWindowsInstanceInitialCredentials(context.TODO(), core.GetWindowsInstanceInitialCredentialsRequest{
InstanceId: &id,
})
if err != nil {
return "", "", err
}
return *credentials.InstanceCredentials.Username, *credentials.InstanceCredentials.Password, err
}
// TerminateInstance terminates a compute instance.
func (d *driverOCI) TerminateInstance(id string) error {
_, err := d.computeClient.TerminateInstance(context.TODO(), core.TerminateInstanceRequest{

View file

@ -0,0 +1,62 @@
package oci
import (
"context"
"fmt"
"log"
commonhelper "github.com/hashicorp/packer/helper/common"
"github.com/hashicorp/packer/helper/communicator"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
type stepGetDefaultCredentials struct {
Debug bool
Comm *communicator.Config
BuildName string
}
func (s *stepGetDefaultCredentials) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
var (
driver = state.Get("driver").(*driverOCI)
ui = state.Get("ui").(packer.Ui)
id = state.Get("instance_id").(string)
)
// Skip if we're not using winrm
if s.Comm.Type != "winrm" {
log.Printf("[INFO] Not using winrm communicator, skipping get password...")
return multistep.ActionContinue
}
// If we already have a password, skip it
if s.Comm.WinRMPassword != "" {
ui.Say("Skipping waiting for password since WinRM password set...")
return multistep.ActionContinue
}
username, password, err := driver.GetInstanceInitialCredentials(id)
if err != nil {
err = fmt.Errorf("Error getting instance's credentials: %s", err)
ui.Error(err.Error())
state.Put("error", err)
return multistep.ActionHalt
}
s.Comm.WinRMPassword = password
s.Comm.WinRMUser = username
if s.Debug {
ui.Message(fmt.Sprintf(
"[DEBUG] (OCI default credentials): Credentials (since debug is enabled): %s", password))
}
// store so that we can access this later during provisioning
commonhelper.SetSharedState("winrm_password", s.Comm.WinRMPassword, s.BuildName)
return multistep.ActionContinue
}
func (s *stepGetDefaultCredentials) Cleanup(state multistep.StateBag) {
// no cleanup
}

View file

@ -12,7 +12,7 @@ func testConfig() map[string]interface{} {
"api_access_key": "foo",
"api_token": "bar",
"region": "ams1",
"commercial_type": "VC1S",
"commercial_type": "START1-S",
"ssh_username": "root",
"image": "image-uuid",
}
@ -98,7 +98,7 @@ func TestBuilderPrepare_CommercialType(t *testing.T) {
t.Fatalf("should error")
}
expected := "VC1S"
expected := "START1-S"
config["commercial_type"] = expected
b = Builder{}

View file

@ -4,8 +4,6 @@ import (
"context"
"fmt"
"log"
"math"
"os"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
@ -38,39 +36,10 @@ func (s StepCompactDisk) Run(_ context.Context, state multistep.StateBag) multis
ui.Say("Compacting all attached virtual disks...")
for i, diskFullPath := range diskFullPaths {
ui.Message(fmt.Sprintf("Compacting virtual disk %d", i+1))
// Get the file size of the virtual disk prior to compaction
fi, err := os.Stat(diskFullPath)
if err != nil {
state.Put("error", fmt.Errorf("Error getting virtual disk file info pre compaction: %s", err))
return multistep.ActionHalt
}
diskFileSizeStart := fi.Size()
// Defragment and compact the disk
if err := driver.CompactDisk(diskFullPath); err != nil {
state.Put("error", fmt.Errorf("Error compacting disk: %s", err))
return multistep.ActionHalt
}
// Get the file size of the virtual disk post compaction
fi, err = os.Stat(diskFullPath)
if err != nil {
state.Put("error", fmt.Errorf("Error getting virtual disk file info post compaction: %s", err))
return multistep.ActionHalt
}
diskFileSizeEnd := fi.Size()
// Report compaction results
log.Printf("Before compaction the disk file size was: %d", diskFileSizeStart)
log.Printf("After compaction the disk file size was: %d", diskFileSizeEnd)
if diskFileSizeStart > 0 {
percentChange := ((float64(diskFileSizeEnd) / float64(diskFileSizeStart)) * 100.0) - 100.0
switch {
case percentChange < 0:
ui.Message(fmt.Sprintf("Compacting reduced the disk file size by %.2f%%", math.Abs(percentChange)))
case percentChange == 0:
ui.Message(fmt.Sprintf("The compacting operation left the disk file size unchanged"))
case percentChange > 0:
ui.Message(fmt.Sprintf("WARNING: Compacting increased the disk file size by %.2f%%", percentChange))
}
}
}
return multistep.ActionContinue

View file

@ -2,8 +2,6 @@ package common
import (
"context"
"io/ioutil"
"os"
"testing"
"github.com/hashicorp/packer/helper/multistep"
@ -17,25 +15,8 @@ func TestStepCompactDisk(t *testing.T) {
state := testState(t)
step := new(StepCompactDisk)
// Create a fake vmdk file for disk file size operations
diskFile, err := ioutil.TempFile("", "disk.vmdk")
if err != nil {
t.Fatalf("Error creating fake vmdk file: %s", err)
}
diskFullPath := diskFile.Name()
defer os.Remove(diskFullPath)
content := []byte("I am the fake vmdk's contents")
if _, err := diskFile.Write(content); err != nil {
t.Fatalf("Error writing to fake vmdk file: %s", err)
}
if err := diskFile.Close(); err != nil {
t.Fatalf("Error closing fake vmdk file: %s", err)
}
// Set up required state
state.Put("disk_full_paths", []string{diskFullPath})
diskFullPaths := []string{"foo"}
state.Put("disk_full_paths", diskFullPaths)
driver := state.Get("driver").(*DriverMock)
@ -51,7 +32,7 @@ func TestStepCompactDisk(t *testing.T) {
if !driver.CompactDiskCalled {
t.Fatal("should've called")
}
if driver.CompactDiskPath != diskFullPath {
if driver.CompactDiskPath != "foo" {
t.Fatal("should call with right path")
}
}

View file

@ -212,11 +212,13 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
}
}
if b.config.Format != "" {
if !(b.config.Format == "ova" || b.config.Format == "ovf" || b.config.Format == "vmx") {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("format must be one of ova, ovf, or vmx"))
}
if b.config.Format == "" {
b.config.Format = "ovf"
}
if !(b.config.Format == "ova" || b.config.Format == "ovf" || b.config.Format == "vmx") {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("format must be one of ova, ovf, or vmx"))
}
// Warnings
@ -256,7 +258,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
exportOutputPath := b.config.OutputDir
if b.config.RemoteType != "" && b.config.Format != "" {
if b.config.RemoteType != "" {
b.config.OutputDir = b.config.VMName
}
dir.SetOutputDir(b.config.OutputDir)

View file

@ -45,8 +45,8 @@ func (s *StepExport) Run(_ context.Context, state multistep.StateBag) multistep.
return multistep.ActionContinue
}
if c.RemoteType != "esx5" || s.Format == "" {
ui.Say("Skipping export of virtual machine (export is allowed only for ESXi and the format needs to be specified)...")
if c.RemoteType != "esx5" {
ui.Say("Skipping export of virtual machine (export is allowed only for ESXi)...")
return multistep.ActionContinue
}

View file

@ -1,3 +1,5 @@
// Code generated by pigeon; DO NOT EDIT.
package bootcommand
import (

View file

@ -1,7 +1,9 @@
package hyperv
import (
"context"
"errors"
"os/exec"
"strconv"
"strings"
@ -196,7 +198,7 @@ Hyper-V\Set-VMFloppyDiskDrive -VMName $vmName -Path $null
return err
}
func CreateVirtualMachine(vmName string, path string, harddrivePath string, vhdRoot string, ram int64, diskSize int64, diskBlockSize int64, switchName string, generation uint, diffDisks bool) error {
func CreateVirtualMachine(vmName string, path string, harddrivePath string, vhdRoot string, ram int64, diskSize int64, diskBlockSize int64, switchName string, generation uint, diffDisks bool, fixedVHD bool) error {
if generation == 2 {
var script = `
@ -223,8 +225,13 @@ if ($harddrivePath){
return DisableAutomaticCheckpoints(vmName)
} else {
var script = `
param([string]$vmName, [string]$path, [string]$harddrivePath, [string]$vhdRoot, [long]$memoryStartupBytes, [long]$newVHDSizeBytes, [long]$vhdBlockSizeBytes, [string]$switchName, [string]$diffDisks)
$vhdx = $vmName + '.vhdx'
param([string]$vmName, [string]$path, [string]$harddrivePath, [string]$vhdRoot, [long]$memoryStartupBytes, [long]$newVHDSizeBytes, [long]$vhdBlockSizeBytes, [string]$switchName, [string]$diffDisks, [string]$fixedVHD)
if($fixedVHD -eq "true"){
$vhdx = $vmName + '.vhd'
}
else{
$vhdx = $vmName + '.vhdx'
}
$vhdPath = Join-Path -Path $vhdRoot -ChildPath $vhdx
if ($harddrivePath){
if($diffDisks -eq "true"){
@ -235,12 +242,17 @@ if ($harddrivePath){
}
Hyper-V\New-VM -Name $vmName -Path $path -MemoryStartupBytes $memoryStartupBytes -VHDPath $vhdPath -SwitchName $switchName
} else {
Hyper-V\New-VHD -Path $vhdPath -SizeBytes $newVHDSizeBytes -BlockSizeBytes $vhdBlockSizeBytes
if($fixedVHD -eq "true"){
Hyper-V\New-VHD -Path $vhdPath -Fixed -SizeBytes $newVHDSizeBytes
}
else {
Hyper-V\New-VHD -Path $vhdPath -SizeBytes $newVHDSizeBytes -BlockSizeBytes $vhdBlockSizeBytes
}
Hyper-V\New-VM -Name $vmName -Path $path -MemoryStartupBytes $memoryStartupBytes -VHDPath $vhdPath -SwitchName $switchName
}
`
var ps powershell.PowerShellCmd
if err := ps.Run(script, vmName, path, harddrivePath, vhdRoot, strconv.FormatInt(ram, 10), strconv.FormatInt(diskSize, 10), strconv.FormatInt(diskBlockSize, 10), switchName, strconv.FormatBool(diffDisks)); err != nil {
if err := ps.Run(script, vmName, path, harddrivePath, vhdRoot, strconv.FormatInt(ram, 10), strconv.FormatInt(diskSize, 10), strconv.FormatInt(diskBlockSize, 10), switchName, strconv.FormatBool(diffDisks), strconv.FormatBool(fixedVHD)); err != nil {
return err
}
@ -504,7 +516,7 @@ Hyper-V\Set-VMNetworkAdapter -VMName $vmName -MacAddressSpoofing $enableMacSpoof
return err
}
func SetVirtualMachineSecureBoot(vmName string, enableSecureBoot bool) error {
func SetVirtualMachineSecureBoot(vmName string, enableSecureBoot bool, templateName string) error {
var script = `
param([string]$vmName, $enableSecureBoot)
Hyper-V\Set-VMFirmware -VMName $vmName -EnableSecureBoot $enableSecureBoot
@ -517,7 +529,11 @@ Hyper-V\Set-VMFirmware -VMName $vmName -EnableSecureBoot $enableSecureBoot
enableSecureBootString = "On"
}
err := ps.Run(script, vmName, enableSecureBootString)
if templateName == "" {
templateName = "MicrosoftWindows"
}
err := ps.Run(script, vmName, enableSecureBootString, templateName)
return err
}
@ -594,12 +610,12 @@ if (Test-Path -Path ([IO.Path]::Combine($path, $vmName, 'Virtual Machines', '*.V
# SCSI controllers are stored in the scsi XML container
if ((Hyper-V\Get-VMFirmware -VM $vm).SecureBoot -eq [Microsoft.HyperV.PowerShell.OnOffState]::On)
{
$config.configuration.secure_boot_enabled.'#text' = 'True'
}
$config.configuration.secure_boot_enabled.'#text' = 'True'
}
else
{
$config.configuration.secure_boot_enabled.'#text' = 'False'
}
}
}
$vm_controllers | ForEach {
@ -886,7 +902,7 @@ Hyper-V\Get-VMNetworkAdapter -VMName $vmName | Hyper-V\Connect-VMNetworkAdapter
func AddVirtualMachineHardDiskDrive(vmName string, vhdRoot string, vhdName string, vhdSizeBytes int64, vhdBlockSize int64, controllerType string) error {
var script = `
param([string]$vmName,[string]$vhdRoot, [string]$vhdName, [string]$vhdSizeInBytes,[string]$vhdBlockSizeInByte [string]$controllerType)
param([string]$vmName,[string]$vhdRoot, [string]$vhdName, [string]$vhdSizeInBytes, [string]$vhdBlockSizeInByte, [string]$controllerType)
$vhdPath = Join-Path -Path $vhdRoot -ChildPath $vhdName
Hyper-V\New-VHD -path $vhdPath -SizeBytes $vhdSizeInBytes -BlockSizeBytes $vhdBlockSizeInByte
Hyper-V\Add-VMHardDiskDrive -VMName $vmName -path $vhdPath -controllerType $controllerType
@ -1230,3 +1246,18 @@ param([string]$vmName, [string]$scanCodes)
err := ps.Run(script, vmName, scanCodes)
return err
}
func ConnectVirtualMachine(vmName string) (context.CancelFunc, error) {
ctx, cancel := context.WithCancel(context.Background())
cmd := exec.CommandContext(ctx, "vmconnect.exe", "localhost", vmName)
err := cmd.Start()
if err != nil {
// Failed to start so cancel function not required
cancel = nil
}
return cancel, err
}
func DisconnectVirtualMachine(cancel context.CancelFunc) {
cancel()
}

View file

@ -1,36 +1,27 @@
package shell
package shell_local
import (
"fmt"
"io"
"log"
"os"
"os/exec"
"syscall"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/template/interpolate"
)
type Communicator struct {
ExecuteCommand []string
Ctx interpolate.Context
}
func (c *Communicator) Start(cmd *packer.RemoteCmd) error {
// Render the template so that we know how to execute the command
c.Ctx.Data = &ExecuteCommandTemplate{
Command: cmd.Command,
}
for i, field := range c.ExecuteCommand {
command, err := interpolate.Render(field, &c.Ctx)
if err != nil {
return fmt.Errorf("Error processing command: %s", err)
}
c.ExecuteCommand[i] = command
if len(c.ExecuteCommand) == 0 {
return fmt.Errorf("Error launching command via shell-local communicator: No ExecuteCommand provided")
}
// Build the local command to execute
log.Printf("[INFO] (shell-local communicator): Executing local shell command %s", c.ExecuteCommand)
localCmd := exec.Command(c.ExecuteCommand[0], c.ExecuteCommand[1:]...)
localCmd.Stdin = cmd.Stdin
localCmd.Stdout = cmd.Stdout
@ -79,7 +70,3 @@ func (c *Communicator) Download(string, io.Writer) error {
func (c *Communicator) DownloadDir(string, string, []string) error {
return fmt.Errorf("downloadDir not supported")
}
type ExecuteCommandTemplate struct {
Command string
}

View file

@ -19,12 +19,13 @@ func TestCommunicator(t *testing.T) {
return
}
c := &Communicator{}
c := &Communicator{
ExecuteCommand: []string{"/bin/sh", "-c", "echo foo"},
}
var buf bytes.Buffer
cmd := &packer.RemoteCmd{
Command: "/bin/echo foo",
Stdout: &buf,
Stdout: &buf,
}
if err := c.Start(cmd); err != nil {

View file

@ -0,0 +1,220 @@
package shell_local
import (
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/hashicorp/packer/common"
configHelper "github.com/hashicorp/packer/helper/config"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/template/interpolate"
)
type Config struct {
common.PackerConfig `mapstructure:",squash"`
// ** DEPRECATED: USE INLINE INSTEAD **
// ** Only Present for backwards compatibiltiy **
// Command is the command to execute
Command string
// An inline script to execute. Multiple strings are all executed
// in the context of a single shell.
Inline []string
// The shebang value used when running inline scripts.
InlineShebang string `mapstructure:"inline_shebang"`
// The file extension to use for the file generated from the inline commands
TempfileExtension string `mapstructure:"tempfile_extension"`
// The local path of the shell script to upload and execute.
Script string
// An array of multiple scripts to run.
Scripts []string
// An array of environment variables that will be injected before
// your command(s) are executed.
Vars []string `mapstructure:"environment_vars"`
EnvVarFormat string `mapstructure:"env_var_format"`
// End dedupe with postprocessor
// The command used to execute the script. The '{{ .Path }}' variable
// should be used to specify where the script goes, {{ .Vars }}
// can be used to inject the environment_vars into the environment.
ExecuteCommand []string `mapstructure:"execute_command"`
UseLinuxPathing bool `mapstructure:"use_linux_pathing"`
Ctx interpolate.Context
}
func Decode(config *Config, raws ...interface{}) error {
//Create passthrough for winrm password so we can fill it in once we know it
config.Ctx.Data = &EnvVarsTemplate{
WinRMPassword: `{{.WinRMPassword}}`,
}
err := configHelper.Decode(&config, &configHelper.DecodeOpts{
Interpolate: true,
InterpolateContext: &config.Ctx,
InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{
"execute_command",
},
},
}, raws...)
if err != nil {
return fmt.Errorf("Error decoding config: %s, config is %#v, and raws is %#v", err, config, raws)
}
return nil
}
func Validate(config *Config) error {
var errs *packer.MultiError
if runtime.GOOS == "windows" {
if len(config.ExecuteCommand) == 0 {
config.ExecuteCommand = []string{
"cmd",
"/V",
"/C",
"{{.Vars}}",
"call",
"{{.Script}}",
}
}
} else {
if config.InlineShebang == "" {
config.InlineShebang = "/bin/sh -e"
}
if len(config.ExecuteCommand) == 0 {
config.ExecuteCommand = []string{
"/bin/sh",
"-c",
"{{.Vars}} {{.Script}}",
}
}
}
// Clean up input
if config.Inline != nil && len(config.Inline) == 0 {
config.Inline = make([]string, 0)
}
if config.Scripts == nil {
config.Scripts = make([]string, 0)
}
if config.Vars == nil {
config.Vars = make([]string, 0)
}
// Verify that the user has given us a command to run
if config.Command == "" && len(config.Inline) == 0 &&
len(config.Scripts) == 0 && config.Script == "" {
errs = packer.MultiErrorAppend(errs,
errors.New("Command, Inline, Script and Scripts options cannot all be empty."))
}
// Check that user hasn't given us too many commands to run
tooManyOptionsErr := errors.New("You may only specify one of the " +
"following options: Command, Inline, Script or Scripts. Please" +
" consolidate these options in your config.")
if config.Command != "" {
if len(config.Inline) != 0 || len(config.Scripts) != 0 || config.Script != "" {
errs = packer.MultiErrorAppend(errs, tooManyOptionsErr)
} else {
config.Inline = []string{config.Command}
}
}
if config.Script != "" {
if len(config.Scripts) > 0 || len(config.Inline) > 0 {
errs = packer.MultiErrorAppend(errs, tooManyOptionsErr)
} else {
config.Scripts = []string{config.Script}
}
}
if len(config.Scripts) > 0 && config.Inline != nil {
errs = packer.MultiErrorAppend(errs, tooManyOptionsErr)
}
// Check that all scripts we need to run exist locally
for _, path := range config.Scripts {
if _, err := os.Stat(path); err != nil {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Bad script '%s': %s", path, err))
}
}
if config.UseLinuxPathing {
for index, script := range config.Scripts {
scriptAbsPath, err := filepath.Abs(script)
if err != nil {
return fmt.Errorf("Error converting %s to absolute path: %s", script, err.Error())
}
converted, err := ConvertToLinuxPath(scriptAbsPath)
if err != nil {
return err
}
config.Scripts[index] = converted
}
// Interoperability issues with WSL makes creating and running tempfiles
// via golang's os package basically impossible.
if len(config.Inline) > 0 {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Packer is unable to use the Command and Inline "+
"features with the Windows Linux Subsystem. Please use "+
"the Script or Scripts options instead"))
}
}
// This is currently undocumented and not a feature users are expected to
// interact with.
if config.EnvVarFormat == "" {
if (runtime.GOOS == "windows") && !config.UseLinuxPathing {
config.EnvVarFormat = "set %s=%s && "
} else {
config.EnvVarFormat = "%s='%s' "
}
}
// drop unnecessary "." in extension; we add this later.
if config.TempfileExtension != "" {
if strings.HasPrefix(config.TempfileExtension, ".") {
config.TempfileExtension = config.TempfileExtension[1:]
}
}
// Do a check for bad environment variables, such as '=foo', 'foobar'
for _, kv := range config.Vars {
vs := strings.SplitN(kv, "=", 2)
if len(vs) != 2 || vs[0] == "" {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Environment variable not in format 'key=value': %s", kv))
}
}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
return nil
}
// C:/path/to/your/file becomes /mnt/c/path/to/your/file
func ConvertToLinuxPath(winAbsPath string) (string, error) {
// get absolute path of script, and morph it into the bash path
winAbsPath = strings.Replace(winAbsPath, "\\", "/", -1)
splitPath := strings.SplitN(winAbsPath, ":/", 2)
winBashPath := fmt.Sprintf("/mnt/%s/%s", strings.ToLower(splitPath[0]), splitPath[1])
return winBashPath, nil
}

View file

@ -0,0 +1,16 @@
package shell_local
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestConvertToLinuxPath(t *testing.T) {
winPath := "C:/path/to/your/file"
winBashPath := "/mnt/c/path/to/your/file"
converted, _ := ConvertToLinuxPath(winPath)
assert.Equal(t, winBashPath, converted,
"Should have converted %s to %s -- not %s", winPath, winBashPath, converted)
}

201
common/shell-local/run.go Normal file
View file

@ -0,0 +1,201 @@
package shell_local
import (
"bufio"
"fmt"
"io/ioutil"
"log"
"os"
"sort"
"strings"
commonhelper "github.com/hashicorp/packer/helper/common"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/template/interpolate"
)
type ExecuteCommandTemplate struct {
Vars string
Script string
Command string
WinRMPassword string
}
type EnvVarsTemplate struct {
WinRMPassword string
}
func Run(ui packer.Ui, config *Config) (bool, error) {
scripts := make([]string, len(config.Scripts))
if len(config.Scripts) > 0 {
copy(scripts, config.Scripts)
} else if config.Inline != nil {
// If we have an inline script, then turn that into a temporary
// shell script and use that.
tempScriptFileName, err := createInlineScriptFile(config)
if err != nil {
return false, err
}
scripts = append(scripts, tempScriptFileName)
// figure out what extension the file should have, and rename it.
if config.TempfileExtension != "" {
os.Rename(tempScriptFileName, fmt.Sprintf("%s.%s", tempScriptFileName, config.TempfileExtension))
tempScriptFileName = fmt.Sprintf("%s.%s", tempScriptFileName, config.TempfileExtension)
}
defer os.Remove(tempScriptFileName)
}
// Create environment variables to set before executing the command
flattenedEnvVars, err := createFlattenedEnvVars(config)
if err != nil {
return false, err
}
for _, script := range scripts {
interpolatedCmds, err := createInterpolatedCommands(config, script, flattenedEnvVars)
if err != nil {
return false, err
}
ui.Say(fmt.Sprintf("Running local shell script: %s", script))
comm := &Communicator{
ExecuteCommand: interpolatedCmds,
}
// The remoteCmd generated here isn't actually run, but it allows us to
// use the same interafce for the shell-local communicator as we use for
// the other communicators; ultimately, this command is just used for
// buffers and for reading the final exit status.
flattenedCmd := strings.Join(interpolatedCmds, " ")
cmd := &packer.RemoteCmd{Command: flattenedCmd}
sanitized := flattenedCmd
if len(getWinRMPassword(config.PackerBuildName)) > 0 {
sanitized = strings.Replace(flattenedCmd,
getWinRMPassword(config.PackerBuildName), "*****", -1)
}
log.Printf("[INFO] (shell-local): starting local command: %s", sanitized)
if err := cmd.StartWithUi(comm, ui); err != nil {
return false, fmt.Errorf(
"Error executing script: %s\n\n"+
"Please see output above for more information.",
script)
}
if cmd.ExitStatus != 0 {
return false, fmt.Errorf(
"Erroneous exit code %d while executing script: %s\n\n"+
"Please see output above for more information.",
cmd.ExitStatus,
script)
}
}
return true, nil
}
func createInlineScriptFile(config *Config) (string, error) {
tf, err := ioutil.TempFile("", "packer-shell")
if err != nil {
return "", fmt.Errorf("Error preparing shell script: %s", err)
}
defer tf.Close()
// Write our contents to it
writer := bufio.NewWriter(tf)
if config.InlineShebang != "" {
shebang := fmt.Sprintf("#!%s\n", config.InlineShebang)
log.Printf("[INFO] (shell-local): Prepending inline script with %s", shebang)
writer.WriteString(shebang)
}
// generate context so you can interpolate the command
config.Ctx.Data = &EnvVarsTemplate{
WinRMPassword: getWinRMPassword(config.PackerBuildName),
}
for _, command := range config.Inline {
// interpolate command to check for template variables.
command, err := interpolate.Render(command, &config.Ctx)
if err != nil {
return "", err
}
if _, err := writer.WriteString(command + "\n"); err != nil {
return "", fmt.Errorf("Error preparing shell script: %s", err)
}
}
if err := writer.Flush(); err != nil {
return "", fmt.Errorf("Error preparing shell script: %s", err)
}
err = os.Chmod(tf.Name(), 0700)
if err != nil {
log.Printf("[ERROR] (shell-local): error modifying permissions of temp script file: %s", err.Error())
}
return tf.Name(), nil
}
// Generates the final command to send to the communicator, using either the
// user-provided ExecuteCommand or defaulting to something that makes sense for
// the host OS
func createInterpolatedCommands(config *Config, script string, flattenedEnvVars string) ([]string, error) {
config.Ctx.Data = &ExecuteCommandTemplate{
Vars: flattenedEnvVars,
Script: script,
Command: script,
WinRMPassword: getWinRMPassword(config.PackerBuildName),
}
interpolatedCmds := make([]string, len(config.ExecuteCommand))
for i, cmd := range config.ExecuteCommand {
interpolatedCmd, err := interpolate.Render(cmd, &config.Ctx)
if err != nil {
return nil, fmt.Errorf("Error processing command: %s", err)
}
interpolatedCmds[i] = interpolatedCmd
}
return interpolatedCmds, nil
}
func createFlattenedEnvVars(config *Config) (string, error) {
flattened := ""
envVars := make(map[string]string)
// Always available Packer provided env vars
envVars["PACKER_BUILD_NAME"] = fmt.Sprintf("%s", config.PackerBuildName)
envVars["PACKER_BUILDER_TYPE"] = fmt.Sprintf("%s", config.PackerBuilderType)
// interpolate environment variables
config.Ctx.Data = &EnvVarsTemplate{
WinRMPassword: getWinRMPassword(config.PackerBuildName),
}
// Split vars into key/value components
for _, envVar := range config.Vars {
envVar, err := interpolate.Render(envVar, &config.Ctx)
if err != nil {
return "", err
}
// Split vars into key/value components
keyValue := strings.SplitN(envVar, "=", 2)
// Store pair, replacing any single quotes in value so they parse
// correctly with required environment variable format
envVars[keyValue[0]] = strings.Replace(keyValue[1], "'", `'"'"'`, -1)
}
// Create a list of env var keys in sorted order
var keys []string
for k := range envVars {
keys = append(keys, k)
}
sort.Strings(keys)
for _, key := range keys {
flattened += fmt.Sprintf(config.EnvVarFormat, key, envVars[key])
}
return flattened, nil
}
func getWinRMPassword(buildName string) string {
winRMPass, _ := commonhelper.RetrieveSharedState("winrm_password", buildName)
return winRMPass
}

Binary file not shown.

View file

@ -2,25 +2,21 @@
"variables": {
"client_id": "{{env `ARM_CLIENT_ID`}}",
"client_secret": "{{env `ARM_CLIENT_SECRET`}}",
"resource_group": "{{env `ARM_RESOURCE_GROUP`}}",
"storage_account": "{{env `ARM_STORAGE_ACCOUNT`}}",
"subscription_id": "{{env `ARM_SUBSCRIPTION_ID`}}",
"tenant_id": "{{env `ARM_TENANT_ID`}}",
"ssh_user": "centos",
"ssh_pass": null
"ssh_pass": "{{env `ARM_SSH_PASS`}}"
},
"builders": [{
"type": "azure-arm",
"client_id": "{{user `client_id`}}",
"client_secret": "{{user `client_secret`}}",
"resource_group_name": "{{user `resource_group`}}",
"storage_account": "{{user `storage_account`}}",
"subscription_id": "{{user `subscription_id`}}",
"tenant_id": "{{user `tenant_id`}}",
"capture_container_name": "images",
"capture_name_prefix": "packer",
"managed_image_resource_group_name": "packertest",
"managed_image_name": "MyCentOSImage",
"ssh_username": "{{user `ssh_user`}}",
"ssh_password": "{{user `ssh_pass`}}",

View file

@ -2,11 +2,9 @@
"variables": {
"client_id": "{{env `ARM_CLIENT_ID`}}",
"client_secret": "{{env `ARM_CLIENT_SECRET`}}",
"resource_group": "{{env `ARM_RESOURCE_GROUP`}}",
"storage_account": "{{env `ARM_STORAGE_ACCOUNT`}}",
"subscription_id": "{{env `ARM_SUBSCRIPTION_ID`}}",
"ssh_user": "packer",
"ssh_pass": null
"ssh_pass": "{{env `ARM_SSH_PASS`}}"
},
"builders": [{
"type": "azure-arm",
@ -17,8 +15,8 @@
"storage_account": "{{user `storage_account`}}",
"subscription_id": "{{user `subscription_id`}}",
"capture_container_name": "images",
"capture_name_prefix": "packer",
"managed_image_resource_group_name": "packertest",
"managed_image_name": "MyDebianOSImage",
"ssh_username": "{{user `ssh_user`}}",
"ssh_password": "{{user `ssh_pass`}}",
@ -26,7 +24,7 @@
"os_type": "Linux",
"image_publisher": "credativ",
"image_offer": "Debian",
"image_sku": "8",
"image_sku": "9",
"ssh_pty": "true",
"location": "South Central US",

View file

@ -2,23 +2,19 @@
"variables": {
"client_id": "{{env `ARM_CLIENT_ID`}}",
"client_secret": "{{env `ARM_CLIENT_SECRET`}}",
"resource_group": "{{env `ARM_RESOURCE_GROUP`}}",
"storage_account": "{{env `ARM_STORAGE_ACCOUNT`}}",
"subscription_id": "{{env `ARM_SUBSCRIPTION_ID`}}",
"ssh_user": "packer",
"ssh_pass": null
"ssh_pass": "{{env `ARM_SSH_PASS`}}"
},
"builders": [{
"type": "azure-arm",
"client_id": "{{user `client_id`}}",
"client_secret": "{{user `client_secret`}}",
"resource_group_name": "{{user `resource_group`}}",
"storage_account": "{{user `storage_account`}}",
"subscription_id": "{{user `subscription_id`}}",
"capture_container_name": "images",
"capture_name_prefix": "packer",
"managed_image_resource_group_name": "packertest",
"managed_image_name": "MyFreeBsdOSImage",
"ssh_username": "{{user `ssh_user`}}",
"ssh_password": "{{user `ssh_pass`}}",

View file

@ -2,8 +2,6 @@
"variables": {
"client_id": "{{env `ARM_CLIENT_ID`}}",
"client_secret": "{{env `ARM_CLIENT_SECRET`}}",
"resource_group": "{{env `ARM_RESOURCE_GROUP`}}",
"storage_account": "{{env `ARM_STORAGE_ACCOUNT`}}",
"subscription_id": "{{env `ARM_SUBSCRIPTION_ID`}}"
},
"builders": [{
@ -11,12 +9,10 @@
"client_id": "{{user `client_id`}}",
"client_secret": "{{user `client_secret`}}",
"resource_group_name": "{{user `resource_group`}}",
"storage_account": "{{user `storage_account`}}",
"subscription_id": "{{user `subscription_id`}}",
"capture_container_name": "images",
"capture_name_prefix": "packer",
"managed_image_resource_group_name": "packertest",
"managed_image_name": "MyMarketplaceOSImage",
"os_type": "Linux",
"image_publisher": "bitnami",

View file

@ -2,25 +2,22 @@
"variables": {
"client_id": "{{env `ARM_CLIENT_ID`}}",
"client_secret": "{{env `ARM_CLIENT_SECRET`}}",
"resource_group": "{{env `ARM_RESOURCE_GROUP`}}",
"storage_account": "{{env `ARM_STORAGE_ACCOUNT`}}",
"subscription_id": "{{env `ARM_SUBSCRIPTION_ID`}}",
"tenant_id": "{{env `ARM_TENANT_ID`}}",
"ssh_user": "centos",
"ssh_pass": null
"ssh_pass": "{{env `ARM_SSH_PASS`}}"
},
"builders": [{
"type": "azure-arm",
"client_id": "{{user `client_id`}}",
"client_secret": "{{user `client_secret`}}",
"resource_group_name": "{{user `resource_group`}}",
"storage_account": "{{user `storage_account`}}",
"subscription_id": "{{user `subscription_id`}}",
"tenant_id": "{{user `tenant_id`}}",
"capture_container_name": "images",
"capture_name_prefix": "packer",
"managed_image_resource_group_name": "packertest",
"managed_image_name": "MyRedHatOSImage",
"ssh_username": "{{user `ssh_user`}}",
"ssh_password": "{{user `ssh_pass`}}",

View file

@ -2,23 +2,19 @@
"variables": {
"client_id": "{{env `ARM_CLIENT_ID`}}",
"client_secret": "{{env `ARM_CLIENT_SECRET`}}",
"resource_group": "{{env `ARM_RESOURCE_GROUP`}}",
"storage_account": "{{env `ARM_STORAGE_ACCOUNT`}}",
"subscription_id": "{{env `ARM_SUBSCRIPTION_ID`}}",
"ssh_user": "packer",
"ssh_pass": null
"ssh_pass": "{{env `ARM_SSH_PASS`}}"
},
"builders": [{
"type": "azure-arm",
"client_id": "{{user `client_id`}}",
"client_secret": "{{user `client_secret`}}",
"resource_group_name": "{{user `resource_group`}}",
"storage_account": "{{user `storage_account`}}",
"subscription_id": "{{user `subscription_id`}}",
"capture_container_name": "images",
"capture_name_prefix": "packer",
"managed_image_resource_group_name": "packertest",
"managed_image_name": "MySuseOSImage",
"ssh_username": "{{user `ssh_user`}}",
"ssh_password": "{{user `ssh_pass`}}",
@ -26,7 +22,7 @@
"os_type": "Linux",
"image_publisher": "SUSE",
"image_offer": "SLES",
"image_sku": "12-SP2",
"image_sku": "12-SP3",
"ssh_pty": "true",
"location": "South Central US",
@ -40,6 +36,7 @@
"/usr/sbin/waagent -force -deprovision+user && export HISTSIZE=0 && sync"
],
"inline_shebang": "/bin/sh -x",
"skip_clean": true,
"type": "shell"
}]
}

View file

@ -2,8 +2,6 @@
"variables": {
"client_id": "{{env `ARM_CLIENT_ID`}}",
"client_secret": "{{env `ARM_CLIENT_SECRET`}}",
"resource_group": "{{env `ARM_RESOURCE_GROUP`}}",
"storage_account": "{{env `ARM_STORAGE_ACCOUNT`}}",
"subscription_id": "{{env `ARM_SUBSCRIPTION_ID`}}"
},
"builders": [{
@ -11,24 +9,22 @@
"client_id": "{{user `client_id`}}",
"client_secret": "{{user `client_secret`}}",
"resource_group_name": "{{user `resource_group`}}",
"storage_account": "{{user `storage_account`}}",
"subscription_id": "{{user `subscription_id`}}",
"capture_container_name": "images",
"capture_name_prefix": "packer",
"os_type": "Linux",
"image_publisher": "Canonical",
"image_offer": "UbuntuServer",
"image_sku": "16.04-LTS",
"managed_image_resource_group_name": "packertest",
"managed_image_name": "MyUbuntuImage",
"azure_tags": {
"dept": "engineering",
"task": "image deployment"
},
"location": "West US",
"location": "South Central US",
"vm_size": "Standard_DS2_v2"
}],
"provisioners": [{

View file

@ -1,41 +0,0 @@
{
"variables": {
"client_id": "{{env `ARM_CLIENT_ID`}}",
"client_secret": "{{env `ARM_CLIENT_SECRET`}}",
"subscription_id": "{{env `ARM_SUBSCRIPTION_ID`}}"
},
"builders": [{
"type": "azure-arm",
"client_id": "{{user `client_id`}}",
"client_secret": "{{user `client_secret`}}",
"subscription_id": "{{user `subscription_id`}}",
"os_type": "Linux",
"image_publisher": "Canonical",
"image_offer": "UbuntuServer",
"image_sku": "16.04-LTS",
"managed_image_resource_group_name": "PackerImages",
"managed_image_name": "MyUbuntuImage",
"azure_tags": {
"dept": "engineering",
"task": "image deployment"
},
"location": "West US",
"vm_size": "Standard_DS2_v2"
}],
"provisioners": [{
"execute_command": "chmod +x {{ .Path }}; {{ .Vars }} sudo -E sh '{{ .Path }}'",
"inline": [
"apt-get update",
"apt-get upgrade -y",
"/usr/sbin/waagent -force -deprovision+user && export HISTSIZE=0 && sync"
],
"inline_shebang": "/bin/sh -x",
"type": "shell"
}]
}

View file

@ -2,23 +2,17 @@
"variables": {
"client_id": "{{env `ARM_CLIENT_ID`}}",
"client_secret": "{{env `ARM_CLIENT_SECRET`}}",
"resource_group": "{{env `ARM_RESOURCE_GROUP`}}",
"storage_account": "{{env `ARM_STORAGE_ACCOUNT`}}",
"subscription_id": "{{env `ARM_SUBSCRIPTION_ID`}}",
"object_id": "{{env `ARM_OBJECT_ID`}}"
"subscription_id": "{{env `ARM_SUBSCRIPTION_ID`}}"
},
"builders": [{
"type": "azure-arm",
"client_id": "{{user `client_id`}}",
"client_secret": "{{user `client_secret`}}",
"resource_group_name": "{{user `resource_group`}}",
"storage_account": "{{user `storage_account`}}",
"subscription_id": "{{user `subscription_id`}}",
"object_id": "{{user `object_id`}}",
"capture_container_name": "images",
"capture_name_prefix": "packer",
"managed_image_resource_group_name": "packertest",
"managed_image_name": "MyWindowsOSImage",
"os_type": "Windows",
"image_publisher": "MicrosoftWindowsServer",
@ -31,7 +25,7 @@
"winrm_timeout": "3m",
"winrm_username": "packer",
"location": "West US",
"location": "South Central US",
"vm_size": "Standard_DS2_v2"
}],
"provisioners": [{

View file

@ -0,0 +1,36 @@
{
"variables": {
"subscription_id": "{{env `ARM_SUBSCRIPTION_ID`}}"
},
"builders": [{
"type": "azure-arm",
"subscription_id": "{{user `subscription_id`}}",
"managed_image_resource_group_name": "packertest",
"managed_image_name": "MyWindowsOSImage",
"os_type": "Windows",
"image_publisher": "MicrosoftWindowsServer",
"image_offer": "WindowsServer",
"image_sku": "2012-R2-Datacenter",
"communicator": "winrm",
"winrm_use_ssl": "true",
"winrm_insecure": "true",
"winrm_timeout": "3m",
"winrm_username": "packer",
"location": "South Central US",
"vm_size": "Standard_DS2_v2"
}],
"provisioners": [{
"type": "powershell",
"inline": [
"if( Test-Path $Env:SystemRoot\\windows\\system32\\Sysprep\\unattend.xml ){ rm $Env:SystemRoot\\windows\\system32\\Sysprep\\unattend.xml -Force}",
"& $env:SystemRoot\\System32\\Sysprep\\Sysprep.exe /oobe /generalize /quiet /quit",
"while($true) { $imageState = Get-ItemProperty HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Setup\\State | Select ImageState; if($imageState.ImageState -ne 'IMAGE_STATE_GENERALIZE_RESEAL_TO_OOBE') { Write-Output $imageState.ImageState; Start-Sleep -s 10 } else { break } }"
]
}]
}

View file

@ -200,10 +200,18 @@ func (b *coreBuild) Run(originalUi Ui, cache Cache) ([]Artifact, error) {
if len(p.config) > 0 {
pConfig = p.config[0]
}
hookedProvisioners[i] = &HookedProvisioner{
p.provisioner,
pConfig,
p.pType,
if b.debug {
hookedProvisioners[i] = &HookedProvisioner{
&DebuggedProvisioner{Provisioner: p.provisioner},
pConfig,
p.pType,
}
} else {
hookedProvisioners[i] = &HookedProvisioner{
p.provisioner,
pConfig,
p.pType,
}
}
}

View file

@ -2,6 +2,7 @@ package packer
import (
"fmt"
"log"
"sync"
"time"
)
@ -168,3 +169,90 @@ func (p *PausedProvisioner) Cancel() {
func (p *PausedProvisioner) provision(result chan<- error, ui Ui, comm Communicator) {
result <- p.Provisioner.Provision(ui, comm)
}
// DebuggedProvisioner is a Provisioner implementation that waits until a key
// press before the provisioner is actually run.
type DebuggedProvisioner struct {
Provisioner Provisioner
cancelCh chan struct{}
doneCh chan struct{}
lock sync.Mutex
}
func (p *DebuggedProvisioner) Prepare(raws ...interface{}) error {
return p.Provisioner.Prepare(raws...)
}
func (p *DebuggedProvisioner) Provision(ui Ui, comm Communicator) error {
p.lock.Lock()
cancelCh := make(chan struct{})
p.cancelCh = cancelCh
// Setup the done channel, which is trigger when we're done
doneCh := make(chan struct{})
defer close(doneCh)
p.doneCh = doneCh
p.lock.Unlock()
defer func() {
p.lock.Lock()
defer p.lock.Unlock()
if p.cancelCh == cancelCh {
p.cancelCh = nil
}
if p.doneCh == doneCh {
p.doneCh = nil
}
}()
// Use a select to determine if we get cancelled during the wait
message := "Pausing before the next provisioner . Press enter to continue."
result := make(chan string, 1)
go func() {
line, err := ui.Ask(message)
if err != nil {
log.Printf("Error asking for input: %s", err)
}
result <- line
}()
select {
case <-result:
case <-cancelCh:
return nil
}
provDoneCh := make(chan error, 1)
go p.provision(provDoneCh, ui, comm)
select {
case err := <-provDoneCh:
return err
case <-cancelCh:
p.Provisioner.Cancel()
return <-provDoneCh
}
}
func (p *DebuggedProvisioner) Cancel() {
var doneCh chan struct{}
p.lock.Lock()
if p.cancelCh != nil {
close(p.cancelCh)
p.cancelCh = nil
}
if p.doneCh != nil {
doneCh = p.doneCh
}
p.lock.Unlock()
<-doneCh
}
func (p *DebuggedProvisioner) provision(result chan<- error, ui Ui, comm Communicator) {
result <- p.Provisioner.Provision(ui, comm)
}

View file

@ -197,3 +197,67 @@ func TestPausedProvisionerCancel(t *testing.T) {
t.Fatal("cancel should be called")
}
}
func TestDebuggedProvisioner_impl(t *testing.T) {
var _ Provisioner = new(DebuggedProvisioner)
}
func TestDebuggedProvisionerPrepare(t *testing.T) {
mock := new(MockProvisioner)
prov := &DebuggedProvisioner{
Provisioner: mock,
}
prov.Prepare(42)
if !mock.PrepCalled {
t.Fatal("prepare should be called")
}
if mock.PrepConfigs[0] != 42 {
t.Fatal("should have proper configs")
}
}
func TestDebuggedProvisionerProvision(t *testing.T) {
mock := new(MockProvisioner)
prov := &DebuggedProvisioner{
Provisioner: mock,
}
ui := testUi()
comm := new(MockCommunicator)
writeReader(ui, "\n")
prov.Provision(ui, comm)
if !mock.ProvCalled {
t.Fatal("prov should be called")
}
if mock.ProvUi != ui {
t.Fatal("should have proper ui")
}
if mock.ProvCommunicator != comm {
t.Fatal("should have proper comm")
}
}
func TestDebuggedProvisionerCancel(t *testing.T) {
mock := new(MockProvisioner)
prov := &DebuggedProvisioner{
Provisioner: mock,
}
provCh := make(chan struct{})
mock.ProvFunc = func() error {
close(provCh)
time.Sleep(10 * time.Millisecond)
return nil
}
// Start provisioning and wait for it to start
go prov.Provision(testUi(), new(MockCommunicator))
<-provCh
// Cancel it
prov.Cancel()
if !mock.CancelCalled {
t.Fatal("cancel should be called")
}
}

View file

@ -1,63 +0,0 @@
package shell_local
import (
"fmt"
"io"
"os"
"os/exec"
"syscall"
"github.com/hashicorp/packer/packer"
)
type Communicator struct{}
func (c *Communicator) Start(cmd *packer.RemoteCmd) error {
localCmd := exec.Command("sh", "-c", cmd.Command)
localCmd.Stdin = cmd.Stdin
localCmd.Stdout = cmd.Stdout
localCmd.Stderr = cmd.Stderr
// Start it. If it doesn't work, then error right away.
if err := localCmd.Start(); err != nil {
return err
}
// We've started successfully. Start a goroutine to wait for
// it to complete and track exit status.
go func() {
var exitStatus int
err := localCmd.Wait()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
exitStatus = 1
// There is no process-independent way to get the REAL
// exit status so we just try to go deeper.
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
exitStatus = status.ExitStatus()
}
}
}
cmd.SetExited(exitStatus)
}()
return nil
}
func (c *Communicator) Upload(string, io.Reader, *os.FileInfo) error {
return fmt.Errorf("upload not supported")
}
func (c *Communicator) UploadDir(string, string, []string) error {
return fmt.Errorf("uploadDir not supported")
}
func (c *Communicator) Download(string, io.Writer) error {
return fmt.Errorf("download not supported")
}
func (c *Communicator) DownloadDir(src string, dst string, exclude []string) error {
return fmt.Errorf("downloadDir not supported")
}

View file

@ -1,51 +1,12 @@
package shell_local
import (
"bufio"
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"sort"
"strings"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/helper/config"
sl "github.com/hashicorp/packer/common/shell-local"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/template/interpolate"
)
type Config struct {
common.PackerConfig `mapstructure:",squash"`
// An inline script to execute. Multiple strings are all executed
// in the context of a single shell.
Inline []string
// The shebang value used when running inline scripts.
InlineShebang string `mapstructure:"inline_shebang"`
// The local path of the shell script to upload and execute.
Script string
// An array of multiple scripts to run.
Scripts []string
// An array of environment variables that will be injected before
// your command(s) are executed.
Vars []string `mapstructure:"environment_vars"`
// The command used to execute the script. The '{{ .Path }}' variable
// should be used to specify where the script goes, {{ .Vars }}
// can be used to inject the environment_vars into the environment.
ExecuteCommand string `mapstructure:"execute_command"`
ctx interpolate.Context
}
type PostProcessor struct {
config Config
config sl.Config
}
type ExecuteCommandTemplate struct {
@ -54,179 +15,34 @@ type ExecuteCommandTemplate struct {
}
func (p *PostProcessor) Configure(raws ...interface{}) error {
err := config.Decode(&p.config, &config.DecodeOpts{
Interpolate: true,
InterpolateContext: &p.config.ctx,
InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{
"execute_command",
},
},
}, raws...)
err := sl.Decode(&p.config, raws...)
if err != nil {
return err
}
if p.config.ExecuteCommand == "" {
p.config.ExecuteCommand = `chmod +x "{{.Script}}"; {{.Vars}} "{{.Script}}"`
if len(p.config.ExecuteCommand) == 1 {
// Backwards compatibility -- before we merged the shell-local
// post-processor and provisioners, the post-processor accepted
// execute_command as a string rather than a slice of strings. It didn't
// have a configurable call to shell program, automatically prepending
// the user-supplied execute_command string with "sh -c". If users are
// still using the old way of defining ExecuteCommand (by supplying a
// single string rather than a slice of strings) then we need to
// prepend this command with the call that the post-processor defaulted
// to before.
p.config.ExecuteCommand = append([]string{"sh", "-c"}, p.config.ExecuteCommand...)
}
if p.config.Inline != nil && len(p.config.Inline) == 0 {
p.config.Inline = nil
}
if p.config.InlineShebang == "" {
p.config.InlineShebang = "/bin/sh -e"
}
if p.config.Scripts == nil {
p.config.Scripts = make([]string, 0)
}
if p.config.Vars == nil {
p.config.Vars = make([]string, 0)
}
var errs *packer.MultiError
if p.config.Script != "" && len(p.config.Scripts) > 0 {
errs = packer.MultiErrorAppend(errs,
errors.New("Only one of script or scripts can be specified."))
}
if p.config.Script != "" {
p.config.Scripts = []string{p.config.Script}
}
if len(p.config.Scripts) == 0 && p.config.Inline == nil {
errs = packer.MultiErrorAppend(errs,
errors.New("Either a script file or inline script must be specified."))
} else if len(p.config.Scripts) > 0 && p.config.Inline != nil {
errs = packer.MultiErrorAppend(errs,
errors.New("Only a script file or an inline script can be specified, not both."))
}
for _, path := range p.config.Scripts {
if _, err := os.Stat(path); err != nil {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Bad script '%s': %s", path, err))
}
}
// Do a check for bad environment variables, such as '=foo', 'foobar'
for _, kv := range p.config.Vars {
vs := strings.SplitN(kv, "=", 2)
if len(vs) != 2 || vs[0] == "" {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Environment variable not in format 'key=value': %s", kv))
}
}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
return nil
return sl.Validate(&p.config)
}
func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, error) {
// this particular post-processor doesn't do anything with the artifact
// except to return it.
scripts := make([]string, len(p.config.Scripts))
copy(scripts, p.config.Scripts)
// If we have an inline script, then turn that into a temporary
// shell script and use that.
if p.config.Inline != nil {
tf, err := ioutil.TempFile("", "packer-shell")
if err != nil {
return nil, false, fmt.Errorf("Error preparing shell script: %s", err)
}
defer os.Remove(tf.Name())
// Set the path to the temporary file
scripts = append(scripts, tf.Name())
// Write our contents to it
writer := bufio.NewWriter(tf)
writer.WriteString(fmt.Sprintf("#!%s\n", p.config.InlineShebang))
for _, command := range p.config.Inline {
if _, err := writer.WriteString(command + "\n"); err != nil {
return nil, false, fmt.Errorf("Error preparing shell script: %s", err)
}
}
if err := writer.Flush(); err != nil {
return nil, false, fmt.Errorf("Error preparing shell script: %s", err)
}
tf.Close()
retBool, retErr := sl.Run(ui, &p.config)
if !retBool {
return nil, retBool, retErr
}
// Create environment variables to set before executing the command
flattenedEnvVars := p.createFlattenedEnvVars()
for _, script := range scripts {
p.config.ctx.Data = &ExecuteCommandTemplate{
Vars: flattenedEnvVars,
Script: script,
}
command, err := interpolate.Render(p.config.ExecuteCommand, &p.config.ctx)
if err != nil {
return nil, false, fmt.Errorf("Error processing command: %s", err)
}
ui.Say(fmt.Sprintf("Post processing with local shell script: %s", script))
comm := &Communicator{}
cmd := &packer.RemoteCmd{Command: command}
log.Printf("starting local command: %s", command)
if err := cmd.StartWithUi(comm, ui); err != nil {
return nil, false, fmt.Errorf(
"Error executing script: %s\n\n"+
"Please see output above for more information.",
script)
}
if cmd.ExitStatus != 0 {
return nil, false, fmt.Errorf(
"Erroneous exit code %d while executing script: %s\n\n"+
"Please see output above for more information.",
cmd.ExitStatus,
script)
}
}
return artifact, true, nil
}
func (p *PostProcessor) createFlattenedEnvVars() (flattened string) {
flattened = ""
envVars := make(map[string]string)
// Always available Packer provided env vars
envVars["PACKER_BUILD_NAME"] = fmt.Sprintf("%s", p.config.PackerBuildName)
envVars["PACKER_BUILDER_TYPE"] = fmt.Sprintf("%s", p.config.PackerBuilderType)
// Split vars into key/value components
for _, envVar := range p.config.Vars {
keyValue := strings.SplitN(envVar, "=", 2)
// Store pair, replacing any single quotes in value so they parse
// correctly with required environment variable format
envVars[keyValue[0]] = strings.Replace(keyValue[1], "'", `'"'"'`, -1)
}
// Create a list of env var keys in sorted order
var keys []string
for k := range envVars {
keys = append(keys, k)
}
sort.Strings(keys)
// Re-assemble vars surrounding value with single quotes and flatten
for _, key := range keys {
flattened += fmt.Sprintf("%s='%s' ", key, envVars[key])
}
return
return artifact, retBool, retErr
}

View file

@ -3,9 +3,11 @@ package shell_local
import (
"io/ioutil"
"os"
"runtime"
"testing"
"github.com/hashicorp/packer/packer"
"github.com/stretchr/testify/assert"
)
func TestPostProcessor_ImplementsPostProcessor(t *testing.T) {
@ -28,32 +30,35 @@ func TestPostProcessor_Impl(t *testing.T) {
func TestPostProcessorPrepare_Defaults(t *testing.T) {
var p PostProcessor
config := testConfig()
raws := testConfig()
err := p.Configure(config)
err := p.Configure(raws)
if err != nil {
t.Fatalf("err: %s", err)
}
}
func TestPostProcessorPrepare_InlineShebang(t *testing.T) {
config := testConfig()
raws := testConfig()
delete(config, "inline_shebang")
delete(raws, "inline_shebang")
p := new(PostProcessor)
err := p.Configure(config)
err := p.Configure(raws)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if p.config.InlineShebang != "/bin/sh -e" {
expected := ""
if runtime.GOOS != "windows" {
expected = "/bin/sh -e"
}
if p.config.InlineShebang != expected {
t.Fatalf("bad value: %s", p.config.InlineShebang)
}
// Test with a good one
config["inline_shebang"] = "foo"
raws["inline_shebang"] = "foo"
p = new(PostProcessor)
err = p.Configure(config)
err = p.Configure(raws)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
@ -65,23 +70,23 @@ func TestPostProcessorPrepare_InlineShebang(t *testing.T) {
func TestPostProcessorPrepare_InvalidKey(t *testing.T) {
var p PostProcessor
config := testConfig()
raws := testConfig()
// Add a random key
config["i_should_not_be_valid"] = true
err := p.Configure(config)
raws["i_should_not_be_valid"] = true
err := p.Configure(raws)
if err == nil {
t.Fatal("should have error")
}
}
func TestPostProcessorPrepare_Script(t *testing.T) {
config := testConfig()
delete(config, "inline")
raws := testConfig()
delete(raws, "inline")
config["script"] = "/this/should/not/exist"
raws["script"] = "/this/should/not/exist"
p := new(PostProcessor)
err := p.Configure(config)
err := p.Configure(raws)
if err == nil {
t.Fatal("should have error")
}
@ -93,23 +98,65 @@ func TestPostProcessorPrepare_Script(t *testing.T) {
}
defer os.Remove(tf.Name())
config["script"] = tf.Name()
raws["script"] = tf.Name()
p = new(PostProcessor)
err = p.Configure(config)
err = p.Configure(raws)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestPostProcessorPrepare_ExecuteCommand(t *testing.T) {
// Check that passing a string will work (Backwards Compatibility)
p := new(PostProcessor)
raws := testConfig()
raws["execute_command"] = "foo bar"
err := p.Configure(raws)
expected := []string{"sh", "-c", "foo bar"}
if err != nil {
t.Fatalf("should handle backwards compatibility: %s", err)
}
assert.Equal(t, p.config.ExecuteCommand, expected,
"Did not get expected execute_command: expected: %#v; received %#v", expected, p.config.ExecuteCommand)
// Check that passing a list will work
p = new(PostProcessor)
raws = testConfig()
raws["execute_command"] = []string{"foo", "bar"}
err = p.Configure(raws)
if err != nil {
t.Fatalf("should handle backwards compatibility: %s", err)
}
expected = []string{"foo", "bar"}
assert.Equal(t, p.config.ExecuteCommand, expected,
"Did not get expected execute_command: expected: %#v; received %#v", expected, p.config.ExecuteCommand)
// Check that default is as expected
raws = testConfig()
delete(raws, "execute_command")
p = new(PostProcessor)
p.Configure(raws)
if runtime.GOOS != "windows" {
expected = []string{"/bin/sh", "-c", "{{.Vars}} {{.Script}}"}
} else {
expected = []string{"cmd", "/V", "/C", "{{.Vars}}", "call", "{{.Script}}"}
}
assert.Equal(t, p.config.ExecuteCommand, expected,
"Did not get expected default: expected: %#v; received %#v", expected, p.config.ExecuteCommand)
}
func TestPostProcessorPrepare_ScriptAndInline(t *testing.T) {
var p PostProcessor
config := testConfig()
raws := testConfig()
delete(config, "inline")
delete(config, "script")
err := p.Configure(config)
// Error if no scripts/inline commands provided
delete(raws, "inline")
delete(raws, "script")
delete(raws, "command")
delete(raws, "scripts")
err := p.Configure(raws)
if err == nil {
t.Fatal("should have error")
t.Fatalf("should error when no scripts/inline commands are provided")
}
// Test with both
@ -119,9 +166,9 @@ func TestPostProcessorPrepare_ScriptAndInline(t *testing.T) {
}
defer os.Remove(tf.Name())
config["inline"] = []interface{}{"foo"}
config["script"] = tf.Name()
err = p.Configure(config)
raws["inline"] = []interface{}{"foo"}
raws["script"] = tf.Name()
err = p.Configure(raws)
if err == nil {
t.Fatal("should have error")
}
@ -129,7 +176,7 @@ func TestPostProcessorPrepare_ScriptAndInline(t *testing.T) {
func TestPostProcessorPrepare_ScriptAndScripts(t *testing.T) {
var p PostProcessor
config := testConfig()
raws := testConfig()
// Test with both
tf, err := ioutil.TempFile("", "packer")
@ -138,21 +185,21 @@ func TestPostProcessorPrepare_ScriptAndScripts(t *testing.T) {
}
defer os.Remove(tf.Name())
config["inline"] = []interface{}{"foo"}
config["scripts"] = []string{tf.Name()}
err = p.Configure(config)
raws["inline"] = []interface{}{"foo"}
raws["scripts"] = []string{tf.Name()}
err = p.Configure(raws)
if err == nil {
t.Fatal("should have error")
}
}
func TestPostProcessorPrepare_Scripts(t *testing.T) {
config := testConfig()
delete(config, "inline")
raws := testConfig()
delete(raws, "inline")
config["scripts"] = []string{}
raws["scripts"] = []string{}
p := new(PostProcessor)
err := p.Configure(config)
err := p.Configure(raws)
if err == nil {
t.Fatal("should have error")
}
@ -164,92 +211,55 @@ func TestPostProcessorPrepare_Scripts(t *testing.T) {
}
defer os.Remove(tf.Name())
config["scripts"] = []string{tf.Name()}
raws["scripts"] = []string{tf.Name()}
p = new(PostProcessor)
err = p.Configure(config)
err = p.Configure(raws)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestPostProcessorPrepare_EnvironmentVars(t *testing.T) {
config := testConfig()
raws := testConfig()
// Test with a bad case
config["environment_vars"] = []string{"badvar", "good=var"}
raws["environment_vars"] = []string{"badvar", "good=var"}
p := new(PostProcessor)
err := p.Configure(config)
err := p.Configure(raws)
if err == nil {
t.Fatal("should have error")
}
// Test with a trickier case
config["environment_vars"] = []string{"=bad"}
raws["environment_vars"] = []string{"=bad"}
p = new(PostProcessor)
err = p.Configure(config)
err = p.Configure(raws)
if err == nil {
t.Fatal("should have error")
}
// Test with a good case
// Note: baz= is a real env variable, just empty
config["environment_vars"] = []string{"FOO=bar", "baz="}
raws["environment_vars"] = []string{"FOO=bar", "baz="}
p = new(PostProcessor)
err = p.Configure(config)
err = p.Configure(raws)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test when the env variable value contains an equals sign
config["environment_vars"] = []string{"good=withequals=true"}
raws["environment_vars"] = []string{"good=withequals=true"}
p = new(PostProcessor)
err = p.Configure(config)
err = p.Configure(raws)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test when the env variable value starts with an equals sign
config["environment_vars"] = []string{"good==true"}
raws["environment_vars"] = []string{"good==true"}
p = new(PostProcessor)
err = p.Configure(config)
err = p.Configure(raws)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestPostProcessor_createFlattenedEnvVars(t *testing.T) {
var flattenedEnvVars string
config := testConfig()
userEnvVarTests := [][]string{
{}, // No user env var
{"FOO=bar"}, // Single user env var
{"FOO=bar's"}, // User env var with single quote in value
{"FOO=bar", "BAZ=qux"}, // Multiple user env vars
{"FOO=bar=baz"}, // User env var with value containing equals
{"FOO==bar"}, // User env var with value starting with equals
}
expected := []string{
`PACKER_BUILDER_TYPE='iso' PACKER_BUILD_NAME='vmware' `,
`FOO='bar' PACKER_BUILDER_TYPE='iso' PACKER_BUILD_NAME='vmware' `,
`FOO='bar'"'"'s' PACKER_BUILDER_TYPE='iso' PACKER_BUILD_NAME='vmware' `,
`BAZ='qux' FOO='bar' PACKER_BUILDER_TYPE='iso' PACKER_BUILD_NAME='vmware' `,
`FOO='bar=baz' PACKER_BUILDER_TYPE='iso' PACKER_BUILD_NAME='vmware' `,
`FOO='=bar' PACKER_BUILDER_TYPE='iso' PACKER_BUILD_NAME='vmware' `,
}
p := new(PostProcessor)
p.Configure(config)
// Defaults provided by Packer
p.config.PackerBuildName = "vmware"
p.config.PackerBuilderType = "iso"
for i, expectedValue := range expected {
p.config.Vars = userEnvVarTests[i]
flattenedEnvVars = p.createFlattenedEnvVars()
if flattenedEnvVars != expectedValue {
t.Fatalf("expected flattened env vars to be: %s, got %s.", expectedValue, flattenedEnvVars)
}
}
}

View file

@ -133,7 +133,15 @@ func DecompressOva(dir, src string) error {
if hdr == nil || err == io.EOF {
break
}
if err != nil {
return err
}
// We use the fileinfo to get the file name because we are not
// expecting path information as from the tar header. It's important
// that we not use the path name from the tar header without checking
// for the presence of `..`. If we accidentally allow for that, we can
// open ourselves up to a path traversal vulnerability.
info := hdr.FileInfo()
// Shouldn't be any directories, skip them

View file

@ -1,9 +1,27 @@
package vagrant
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func TestVBoxProvider_impl(t *testing.T) {
var _ Provider = new(VBoxProvider)
}
func TestDecomressOVA(t *testing.T) {
td, err := ioutil.TempDir("", "pp-vagrant-virtualbox")
assert.NoError(t, err)
fixture := "../../common/test-fixtures/decompress-tar/outside_parent.tar"
err = DecompressOva(td, fixture)
assert.NoError(t, err)
_, err = os.Stat(filepath.Join(filepath.Base(td), "demo.poc"))
assert.Error(t, err)
_, err = os.Stat(filepath.Join(td, "demo.poc"))
assert.NoError(t, err)
os.RemoveAll(td)
}

View file

@ -0,0 +1,38 @@
package ansiblelocal
import (
"github.com/hashicorp/packer/packer"
"io"
"os"
)
type communicatorMock struct {
startCommand []string
uploadDestination []string
}
func (c *communicatorMock) Start(cmd *packer.RemoteCmd) error {
c.startCommand = append(c.startCommand, cmd.Command)
cmd.SetExited(0)
return nil
}
func (c *communicatorMock) Upload(dst string, _ io.Reader, _ *os.FileInfo) error {
c.uploadDestination = append(c.uploadDestination, dst)
return nil
}
func (c *communicatorMock) UploadDir(dst, src string, exclude []string) error {
return nil
}
func (c *communicatorMock) Download(src string, dst io.Writer) error {
return nil
}
func (c *communicatorMock) DownloadDir(src, dst string, exclude []string) error {
return nil
}
func (c *communicatorMock) verify() {
}

View file

@ -38,6 +38,9 @@ type Config struct {
// The main playbook file to execute.
PlaybookFile string `mapstructure:"playbook_file"`
// The playbook files to execute.
PlaybookFiles []string `mapstructure:"playbook_files"`
// An array of local paths of playbook files to upload.
PlaybookPaths []string `mapstructure:"playbook_paths"`
@ -66,6 +69,8 @@ type Config struct {
type Provisioner struct {
config Config
playbookFiles []string
}
func (p *Provisioner) Prepare(raws ...interface{}) error {
@ -80,6 +85,9 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
return err
}
// Reset the state.
p.playbookFiles = make([]string, 0, len(p.config.PlaybookFiles))
// Defaults
if p.config.Command == "" {
p.config.Command = "ANSIBLE_FORCE_COLOR=1 PYTHONUNBUFFERED=1 ansible-playbook"
@ -94,9 +102,32 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
// Validation
var errs *packer.MultiError
err = validateFileConfig(p.config.PlaybookFile, "playbook_file", true)
if err != nil {
errs = packer.MultiErrorAppend(errs, err)
// Check that either playbook_file or playbook_files is specified
if len(p.config.PlaybookFiles) != 0 && p.config.PlaybookFile != "" {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("Either playbook_file or playbook_files can be specified, not both"))
}
if len(p.config.PlaybookFiles) == 0 && p.config.PlaybookFile == "" {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("Either playbook_file or playbook_files must be specified"))
}
if p.config.PlaybookFile != "" {
err = validateFileConfig(p.config.PlaybookFile, "playbook_file", true)
if err != nil {
errs = packer.MultiErrorAppend(errs, err)
}
}
for _, playbookFile := range p.config.PlaybookFiles {
if err := validateFileConfig(playbookFile, "playbook_files", true); err != nil {
errs = packer.MultiErrorAppend(errs, err)
} else {
playbookFile, err := filepath.Abs(playbookFile)
if err != nil {
errs = packer.MultiErrorAppend(errs, err)
} else {
p.playbookFiles = append(p.playbookFiles, playbookFile)
}
}
}
// Check that the inventory file exists, if configured
@ -169,11 +200,15 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
}
}
ui.Message("Uploading main Playbook file...")
src := p.config.PlaybookFile
dst := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(src)))
if err := p.uploadFile(ui, comm, dst, src); err != nil {
return fmt.Errorf("Error uploading main playbook: %s", err)
if p.config.PlaybookFile != "" {
ui.Message("Uploading main Playbook file...")
src := p.config.PlaybookFile
dst := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(src)))
if err := p.uploadFile(ui, comm, dst, src); err != nil {
return fmt.Errorf("Error uploading main playbook: %s", err)
}
} else if err := p.provisionPlaybookFiles(ui, comm); err != nil {
return err
}
if len(p.config.InventoryFile) == 0 {
@ -204,16 +239,16 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
if len(p.config.GalaxyFile) > 0 {
ui.Message("Uploading galaxy file...")
src = p.config.GalaxyFile
dst = filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(src)))
src := p.config.GalaxyFile
dst := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(src)))
if err := p.uploadFile(ui, comm, dst, src); err != nil {
return fmt.Errorf("Error uploading galaxy file: %s", err)
}
}
ui.Message("Uploading inventory file...")
src = p.config.InventoryFile
dst = filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(src)))
src := p.config.InventoryFile
dst := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(src)))
if err := p.uploadFile(ui, comm, dst, src); err != nil {
return fmt.Errorf("Error uploading inventory file: %s", err)
}
@ -279,6 +314,44 @@ func (p *Provisioner) Cancel() {
os.Exit(0)
}
func (p *Provisioner) provisionPlaybookFiles(ui packer.Ui, comm packer.Communicator) error {
var playbookDir string
if p.config.PlaybookDir != "" {
var err error
playbookDir, err = filepath.Abs(p.config.PlaybookDir)
if err != nil {
return err
}
}
for index, playbookFile := range p.playbookFiles {
if playbookDir != "" && strings.HasPrefix(playbookFile, playbookDir) {
p.playbookFiles[index] = strings.TrimPrefix(playbookFile, playbookDir)
continue
}
if err := p.provisionPlaybookFile(ui, comm, playbookFile); err != nil {
return err
}
}
return nil
}
func (p *Provisioner) provisionPlaybookFile(ui packer.Ui, comm packer.Communicator, playbookFile string) error {
ui.Message(fmt.Sprintf("Uploading playbook file: %s", playbookFile))
remoteDir := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Dir(playbookFile)))
remotePlaybookFile := filepath.ToSlash(filepath.Join(p.config.StagingDir, playbookFile))
if err := p.createDir(ui, comm, remoteDir); err != nil {
return fmt.Errorf("Error uploading playbook file: %s [%s]", playbookFile, err)
}
if err := p.uploadFile(ui, comm, remotePlaybookFile, playbookFile); err != nil {
return fmt.Errorf("Error uploading playbook: %s [%s]", playbookFile, err)
}
return nil
}
func (p *Provisioner) executeGalaxy(ui packer.Ui, comm packer.Communicator) error {
rolesDir := filepath.ToSlash(filepath.Join(p.config.StagingDir, "roles"))
galaxyFile := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(p.config.GalaxyFile)))
@ -301,7 +374,6 @@ func (p *Provisioner) executeGalaxy(ui packer.Ui, comm packer.Communicator) erro
}
func (p *Provisioner) executeAnsible(ui packer.Ui, comm packer.Communicator) error {
playbook := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(p.config.PlaybookFile)))
inventory := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(p.config.InventoryFile)))
extraArgs := fmt.Sprintf(" --extra-vars \"packer_build_name=%s packer_builder_type=%s packer_http_addr=%s\" ",
@ -317,8 +389,28 @@ func (p *Provisioner) executeAnsible(ui packer.Ui, comm packer.Communicator) err
}
}
if p.config.PlaybookFile != "" {
playbookFile := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(p.config.PlaybookFile)))
if err := p.executeAnsiblePlaybook(ui, comm, playbookFile, extraArgs, inventory); err != nil {
return err
}
}
for _, playbookFile := range p.playbookFiles {
playbookFile = filepath.ToSlash(filepath.Join(p.config.StagingDir, playbookFile))
if err := p.executeAnsiblePlaybook(ui, comm, playbookFile, extraArgs, inventory); err != nil {
return err
}
}
return nil
}
func (p *Provisioner) executeAnsiblePlaybook(
ui packer.Ui, comm packer.Communicator, playbookFile, extraArgs, inventory string,
) error {
command := fmt.Sprintf("cd %s && %s %s%s -c local -i %s",
p.config.StagingDir, p.config.Command, playbook, extraArgs, inventory)
p.config.StagingDir, p.config.Command, playbookFile, extraArgs, inventory,
)
ui.Message(fmt.Sprintf("Executing Ansible: %s", command))
cmd := &packer.RemoteCmd{
Command: command,

View file

@ -7,14 +7,14 @@ import (
"strings"
"testing"
"fmt"
"github.com/hashicorp/packer/builder/docker"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/provisioner/file"
"github.com/hashicorp/packer/template"
"os/exec"
)
func testConfig() map[string]interface{} {
m := make(map[string]interface{})
return m
}
func TestProvisioner_Impl(t *testing.T) {
var raw interface{}
raw = &Provisioner{}
@ -73,6 +73,107 @@ func TestProvisionerPrepare_PlaybookFile(t *testing.T) {
}
}
func TestProvisionerPrepare_PlaybookFiles(t *testing.T) {
var p Provisioner
config := testConfig()
err := p.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
config["playbook_file"] = ""
config["playbook_files"] = []string{}
err = p.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
playbook_file, err := ioutil.TempFile("", "playbook")
if err != nil {
t.Fatalf("err: %s", err)
}
defer os.Remove(playbook_file.Name())
config["playbook_file"] = playbook_file.Name()
config["playbook_files"] = []string{"some_other_file"}
err = p.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
p = Provisioner{}
config["playbook_file"] = playbook_file.Name()
config["playbook_files"] = []string{}
err = p.Prepare(config)
if err != nil {
t.Fatalf("err: %s", err)
}
config["playbook_file"] = ""
config["playbook_files"] = []string{playbook_file.Name()}
err = p.Prepare(config)
if err != nil {
t.Fatalf("err: %s", err)
}
}
func TestProvisionerProvision_PlaybookFiles(t *testing.T) {
var p Provisioner
config := testConfig()
playbooks := createTempFiles("", 3)
defer removeFiles(playbooks...)
config["playbook_files"] = playbooks
err := p.Prepare(config)
if err != nil {
t.Fatalf("err: %s", err)
}
comm := &communicatorMock{}
if err := p.Provision(&uiStub{}, comm); err != nil {
t.Fatalf("err: %s", err)
}
assertPlaybooksUploaded(comm, playbooks)
assertPlaybooksExecuted(comm, playbooks)
}
func TestProvisionerProvision_PlaybookFilesWithPlaybookDir(t *testing.T) {
var p Provisioner
config := testConfig()
playbook_dir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("Failed to create playbook_dir: %s", err)
}
defer os.RemoveAll(playbook_dir)
playbooks := createTempFiles(playbook_dir, 3)
playbookNames := make([]string, 0, len(playbooks))
playbooksInPlaybookDir := make([]string, 0, len(playbooks))
for _, playbook := range playbooks {
playbooksInPlaybookDir = append(playbooksInPlaybookDir, strings.TrimPrefix(playbook, playbook_dir))
playbookNames = append(playbookNames, filepath.Base(playbook))
}
config["playbook_files"] = playbooks
config["playbook_dir"] = playbook_dir
err = p.Prepare(config)
if err != nil {
t.Fatalf("err: %s", err)
}
comm := &communicatorMock{}
if err := p.Provision(&uiStub{}, comm); err != nil {
t.Fatalf("err: %s", err)
}
assertPlaybooksNotUploaded(comm, playbookNames)
assertPlaybooksExecuted(comm, playbooksInPlaybookDir)
}
func TestProvisionerPrepare_InventoryFile(t *testing.T) {
var p Provisioner
config := testConfig()
@ -211,3 +312,216 @@ func TestProvisionerPrepare_CleanStagingDir(t *testing.T) {
t.Fatalf("expected clean_staging_directory to be set")
}
}
func TestProvisionerProvisionDocker_PlaybookFiles(t *testing.T) {
testProvisionerProvisionDockerWithPlaybookFiles(t, playbookFilesDockerTemplate)
}
func TestProvisionerProvisionDocker_PlaybookFilesWithPlaybookDir(t *testing.T) {
testProvisionerProvisionDockerWithPlaybookFiles(t, playbookFilesWithPlaybookDirDockerTemplate)
}
func testProvisionerProvisionDockerWithPlaybookFiles(t *testing.T, templateString string) {
if os.Getenv("PACKER_ACC") == "" {
t.Skip("This test is only run with PACKER_ACC=1")
}
ui := packer.TestUi(t)
cache := &packer.FileCache{CacheDir: os.TempDir()}
tpl, err := template.Parse(strings.NewReader(templateString))
if err != nil {
t.Fatalf("Unable to parse config: %s", err)
}
// Check if docker executable can be found.
_, err = exec.LookPath("docker")
if err != nil {
t.Error("docker command not found; please make sure docker is installed")
}
// Setup the builder
builder := &docker.Builder{}
warnings, err := builder.Prepare(tpl.Builders["docker"].Config)
if err != nil {
t.Fatalf("Error preparing configuration %s", err)
}
if len(warnings) > 0 {
t.Fatal("Encountered configuration warnings; aborting")
}
ansible := &Provisioner{}
err = ansible.Prepare(tpl.Provisioners[0].Config)
if err != nil {
t.Fatalf("Error preparing ansible-local provisioner: %s", err)
}
download := &file.Provisioner{}
err = download.Prepare(tpl.Provisioners[1].Config)
if err != nil {
t.Fatalf("Error preparing download: %s", err)
}
// Add hooks so the provisioners run during the build
hooks := map[string][]packer.Hook{}
hooks[packer.HookProvision] = []packer.Hook{
&packer.ProvisionHook{
Provisioners: []*packer.HookedProvisioner{
{ansible, nil, ""},
{download, nil, ""},
},
},
}
hook := &packer.DispatchHook{Mapping: hooks}
artifact, err := builder.Run(ui, hook, cache)
if err != nil {
t.Fatalf("Error running build %s", err)
}
defer os.Remove("hello_world")
defer artifact.Destroy()
actualContent, err := ioutil.ReadFile("hello_world")
if err != nil {
t.Fatalf("Expected file not found: %s", err)
}
expectedContent := "Hello world!"
if string(actualContent) != expectedContent {
t.Fatalf(`Unexpected file content: expected="%s", actual="%s"`, expectedContent, actualContent)
}
}
func assertPlaybooksExecuted(comm *communicatorMock, playbooks []string) {
cmdIndex := 0
for _, playbook := range playbooks {
playbook = filepath.ToSlash(playbook)
for ; cmdIndex < len(comm.startCommand); cmdIndex++ {
cmd := comm.startCommand[cmdIndex]
if strings.Contains(cmd, "ansible-playbook") && strings.Contains(cmd, playbook) {
break
}
}
if cmdIndex == len(comm.startCommand) {
panic(fmt.Sprintf("Playbook %s was not executed", playbook))
}
}
}
func assertPlaybooksUploaded(comm *communicatorMock, playbooks []string) {
uploadIndex := 0
for _, playbook := range playbooks {
playbook = filepath.ToSlash(playbook)
for ; uploadIndex < len(comm.uploadDestination); uploadIndex++ {
dest := comm.uploadDestination[uploadIndex]
if strings.HasSuffix(dest, playbook) {
break
}
}
if uploadIndex == len(comm.uploadDestination) {
panic(fmt.Sprintf("Playbook %s was not uploaded", playbook))
}
}
}
func assertPlaybooksNotUploaded(comm *communicatorMock, playbooks []string) {
for _, playbook := range playbooks {
playbook = filepath.ToSlash(playbook)
for _, destination := range comm.uploadDestination {
if strings.HasSuffix(destination, playbook) {
panic(fmt.Sprintf("Playbook %s was uploaded", playbook))
}
}
}
}
func testConfig() map[string]interface{} {
m := make(map[string]interface{})
return m
}
func createTempFile(dir string) string {
file, err := ioutil.TempFile(dir, "")
if err != nil {
panic(fmt.Sprintf("err: %s", err))
}
return file.Name()
}
func createTempFiles(dir string, numFiles int) []string {
files := make([]string, 0, numFiles)
defer func() {
// Cleanup the files if not all were created.
if len(files) < numFiles {
for _, file := range files {
os.Remove(file)
}
}
}()
for i := 0; i < numFiles; i++ {
files = append(files, createTempFile(dir))
}
return files
}
func removeFiles(files ...string) {
for _, file := range files {
os.Remove(file)
}
}
const playbookFilesDockerTemplate = `
{
"builders": [
{
"type": "docker",
"image": "williamyeh/ansible:centos7",
"discard": true
}
],
"provisioners": [
{
"type": "ansible-local",
"playbook_files": [
"test-fixtures/hello.yml",
"test-fixtures/world.yml"
]
},
{
"type": "file",
"source": "/tmp/hello_world",
"destination": "hello_world",
"direction": "download"
}
]
}
`
const playbookFilesWithPlaybookDirDockerTemplate = `
{
"builders": [
{
"type": "docker",
"image": "williamyeh/ansible:centos7",
"discard": true
}
],
"provisioners": [
{
"type": "ansible-local",
"playbook_files": [
"test-fixtures/hello.yml",
"test-fixtures/world.yml"
],
"playbook_dir": "test-fixtures"
},
{
"type": "file",
"source": "/tmp/hello_world",
"destination": "hello_world",
"direction": "download"
}
]
}
`

View file

@ -0,0 +1,5 @@
---
- hosts: all
tasks:
- name: write Hello
shell: echo -n "Hello" >> /tmp/hello_world

View file

@ -0,0 +1,5 @@
---
- hosts: all
tasks:
- name: write world!
shell: echo -n " world!" >> /tmp/hello_world

View file

@ -0,0 +1,15 @@
package ansiblelocal
type uiStub struct{}
func (su *uiStub) Ask(string) (string, error) {
return "", nil
}
func (su *uiStub) Error(string) {}
func (su *uiStub) Machine(string, ...string) {}
func (su *uiStub) Message(string) {}
func (su *uiStub) Say(msg string) {}

View file

@ -1,45 +0,0 @@
package shell
import (
"bytes"
"runtime"
"strings"
"testing"
"github.com/hashicorp/packer/packer"
)
func TestCommunicator_impl(t *testing.T) {
var _ packer.Communicator = new(Communicator)
}
func TestCommunicator(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("windows not supported for this test")
return
}
c := &Communicator{
ExecuteCommand: []string{"/bin/sh", "-c", "{{.Command}}"},
}
var buf bytes.Buffer
cmd := &packer.RemoteCmd{
Command: "echo foo",
Stdout: &buf,
}
if err := c.Start(cmd); err != nil {
t.Fatalf("err: %s", err)
}
cmd.Wait()
if cmd.ExitStatus != 0 {
t.Fatalf("err bad exit status: %d", cmd.ExitStatus)
}
if strings.TrimSpace(buf.String()) != "foo" {
t.Fatalf("bad: %s", buf.String())
}
}

View file

@ -1,105 +1,32 @@
package shell
import (
"errors"
"fmt"
"runtime"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/helper/config"
sl "github.com/hashicorp/packer/common/shell-local"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/template/interpolate"
)
type Config struct {
common.PackerConfig `mapstructure:",squash"`
// Command is the command to execute
Command string
// ExecuteCommand is the command used to execute the command.
ExecuteCommand []string `mapstructure:"execute_command"`
ctx interpolate.Context
}
type Provisioner struct {
config Config
config sl.Config
}
func (p *Provisioner) Prepare(raws ...interface{}) error {
err := config.Decode(&p.config, &config.DecodeOpts{
Interpolate: true,
InterpolateContext: &p.config.ctx,
InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{
"execute_command",
},
},
}, raws...)
err := sl.Decode(&p.config, raws...)
if err != nil {
return err
}
if len(p.config.ExecuteCommand) == 0 {
if runtime.GOOS == "windows" {
p.config.ExecuteCommand = []string{
"cmd",
"/C",
"{{.Command}}",
}
} else {
p.config.ExecuteCommand = []string{
"/bin/sh",
"-c",
"{{.Command}}",
}
}
}
var errs *packer.MultiError
if p.config.Command == "" {
errs = packer.MultiErrorAppend(errs,
errors.New("command must be specified"))
}
if len(p.config.ExecuteCommand) == 0 {
errs = packer.MultiErrorAppend(errs,
errors.New("execute_command must not be empty"))
}
if errs != nil && len(errs.Errors) > 0 {
return errs
err = sl.Validate(&p.config)
if err != nil {
return err
}
return nil
}
func (p *Provisioner) Provision(ui packer.Ui, _ packer.Communicator) error {
// Make another communicator for local
comm := &Communicator{
Ctx: p.config.ctx,
ExecuteCommand: p.config.ExecuteCommand,
}
// Build the remote command
cmd := &packer.RemoteCmd{Command: p.config.Command}
ui.Say(fmt.Sprintf(
"Executing local command: %s",
p.config.Command))
if err := cmd.StartWithUi(comm, ui); err != nil {
return fmt.Errorf(
"Error executing command: %s\n\n"+
"Please see output above for more information.",
p.config.Command)
}
if cmd.ExitStatus != 0 {
return fmt.Errorf(
"Erroneous exit code %d while executing command: %s\n\n"+
"Please see output above for more information.",
cmd.ExitStatus,
p.config.Command)
_, retErr := sl.Run(ui, &p.config)
if retErr != nil {
return retErr
}
return nil

View file

@ -1,14 +0,0 @@
#!/usr/bin/env bash
# Check gofmt
echo "==> Checking that code complies with gofmt requirements..."
gofmt_files=$(gofmt -s -l ${@})
if [[ -n ${gofmt_files} ]]; then
echo 'gofmt needs running on the following files:'
echo "${gofmt_files}"
echo "You can use the command: \`make fmt\` to reformat code."
exit 1
fi
echo "Check passed."
exit 0

View file

@ -56,8 +56,9 @@ This simple parsing example:
is directly mapped to:
```go
if token, err := request.ParseFromRequest(tokenString, request.OAuth2Extractor, req, keyLookupFunc); err == nil {
fmt.Printf("Token for user %v expires %v", token.Claims["user"], token.Claims["exp"])
if token, err := request.ParseFromRequest(req, request.OAuth2Extractor, keyLookupFunc); err == nil {
claims := token.Claims.(jwt.MapClaims)
fmt.Printf("Token for user %v expires %v", claims["user"], claims["exp"])
}
```

View file

@ -1,11 +1,15 @@
A [go](http://www.golang.org) (or 'golang' for search engine friendliness) implementation of [JSON Web Tokens](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html)
# jwt-go
[![Build Status](https://travis-ci.org/dgrijalva/jwt-go.svg?branch=master)](https://travis-ci.org/dgrijalva/jwt-go)
[![GoDoc](https://godoc.org/github.com/dgrijalva/jwt-go?status.svg)](https://godoc.org/github.com/dgrijalva/jwt-go)
**BREAKING CHANGES:*** Version 3.0.0 is here. It includes _a lot_ of changes including a few that break the API. We've tried to break as few things as possible, so there should just be a few type signature changes. A full list of breaking changes is available in `VERSION_HISTORY.md`. See `MIGRATION_GUIDE.md` for more information on updating your code.
A [go](http://www.golang.org) (or 'golang' for search engine friendliness) implementation of [JSON Web Tokens](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html)
**NOTICE:** A vulnerability in JWT was [recently published](https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/). As this library doesn't force users to validate the `alg` is what they expected, it's possible your usage is effected. There will be an update soon to remedy this, and it will likey require backwards-incompatible changes to the API. In the short term, please make sure your implementation verifies the `alg` is what you expect.
**NEW VERSION COMING:** There have been a lot of improvements suggested since the version 3.0.0 released in 2016. I'm working now on cutting two different releases: 3.2.0 will contain any non-breaking changes or enhancements. 4.0.0 will follow shortly which will include breaking changes. See the 4.0.0 milestone to get an idea of what's coming. If you have other ideas, or would like to participate in 4.0.0, now's the time. If you depend on this library and don't want to be interrupted, I recommend you use your dependency mangement tool to pin to version 3.
**SECURITY NOTICE:** Some older versions of Go have a security issue in the cryotp/elliptic. Recommendation is to upgrade to at least 1.8.3. See issue #216 for more detail.
**SECURITY NOTICE:** It's important that you [validate the `alg` presented is what you expect](https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/). This library attempts to make it easy to do the right thing by requiring key types match the expected alg, but you should take the extra step to verify it in your usage. See the examples provided.
## What the heck is a JWT?
@ -25,8 +29,8 @@ This library supports the parsing and verification as well as the generation and
See [the project documentation](https://godoc.org/github.com/dgrijalva/jwt-go) for examples of usage:
* [Simple example of parsing and validating a token](https://godoc.org/github.com/dgrijalva/jwt-go#example_Parse_hmac)
* [Simple example of building and signing a token](https://godoc.org/github.com/dgrijalva/jwt-go#example_New_hmac)
* [Simple example of parsing and validating a token](https://godoc.org/github.com/dgrijalva/jwt-go#example-Parse--Hmac)
* [Simple example of building and signing a token](https://godoc.org/github.com/dgrijalva/jwt-go#example-New--Hmac)
* [Directory of Examples](https://godoc.org/github.com/dgrijalva/jwt-go#pkg-examples)
## Extensions
@ -37,7 +41,7 @@ Here's an example of an extension that integrates with the Google App Engine sig
## Compliance
This library was last reviewed to comply with [RTF 7519](http://www.rfc-editor.org/info/rfc7519) dated May 2015 with a few notable differences:
This library was last reviewed to comply with [RTF 7519](http://www.rfc-editor.org/info/rfc7519) dated May 2015 with a few notable differences:
* In order to protect against accidental use of [Unsecured JWTs](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#UnsecuredJWT), tokens using `alg=none` will only be accepted if the constant `jwt.UnsafeAllowNoneSignatureType` is provided as the key.
@ -47,7 +51,10 @@ This library is considered production ready. Feedback and feature requests are
This project uses [Semantic Versioning 2.0.0](http://semver.org). Accepted pull requests will land on `master`. Periodically, versions will be tagged from `master`. You can find all the releases on [the project releases page](https://github.com/dgrijalva/jwt-go/releases).
While we try to make it obvious when we make breaking changes, there isn't a great mechanism for pushing announcements out to users. You may want to use this alternative package include: `gopkg.in/dgrijalva/jwt-go.v2`. It will do the right thing WRT semantic versioning.
While we try to make it obvious when we make breaking changes, there isn't a great mechanism for pushing announcements out to users. You may want to use this alternative package include: `gopkg.in/dgrijalva/jwt-go.v3`. It will do the right thing WRT semantic versioning.
**BREAKING CHANGES:***
* Version 3.0.0 includes _a lot_ of changes from the 2.x line, including a few that break the API. We've tried to break as few things as possible, so there should just be a few type signature changes. A full list of breaking changes is available in `VERSION_HISTORY.md`. See `MIGRATION_GUIDE.md` for more information on updating your code.
## Usage Tips
@ -68,18 +75,26 @@ Symmetric signing methods, such as HSA, use only a single secret. This is probab
Asymmetric signing methods, such as RSA, use different keys for signing and verifying tokens. This makes it possible to produce tokens with a private key, and allow any consumer to access the public key for verification.
### Signing Methods and Key Types
Each signing method expects a different object type for its signing keys. See the package documentation for details. Here are the most common ones:
* The [HMAC signing method](https://godoc.org/github.com/dgrijalva/jwt-go#SigningMethodHMAC) (`HS256`,`HS384`,`HS512`) expect `[]byte` values for signing and validation
* The [RSA signing method](https://godoc.org/github.com/dgrijalva/jwt-go#SigningMethodRSA) (`RS256`,`RS384`,`RS512`) expect `*rsa.PrivateKey` for signing and `*rsa.PublicKey` for validation
* The [ECDSA signing method](https://godoc.org/github.com/dgrijalva/jwt-go#SigningMethodECDSA) (`ES256`,`ES384`,`ES512`) expect `*ecdsa.PrivateKey` for signing and `*ecdsa.PublicKey` for validation
### JWT and OAuth
It's worth mentioning that OAuth and JWT are not the same thing. A JWT token is simply a signed JSON object. It can be used anywhere such a thing is useful. There is some confusion, though, as JWT is the most common type of bearer token used in OAuth2 authentication.
Without going too far down the rabbit hole, here's a description of the interaction of these technologies:
* OAuth is a protocol for allowing an identity provider to be separate from the service a user is logging in to. For example, whenever you use Facebook to log into a different service (Yelp, Spotify, etc), you are using OAuth.
* OAuth is a protocol for allowing an identity provider to be separate from the service a user is logging in to. For example, whenever you use Facebook to log into a different service (Yelp, Spotify, etc), you are using OAuth.
* OAuth defines several options for passing around authentication data. One popular method is called a "bearer token". A bearer token is simply a string that _should_ only be held by an authenticated user. Thus, simply presenting this token proves your identity. You can probably derive from here why a JWT might make a good bearer token.
* Because bearer tokens are used for authentication, it's important they're kept secret. This is why transactions that use bearer tokens typically happen over SSL.
## More
Documentation can be found [on godoc.org](http://godoc.org/github.com/dgrijalva/jwt-go).
The command line utility included in this project (cmd/jwt) provides a straightforward example of token creation and parsing as well as a useful tool for debugging your own integration. You'll also find several implementation examples in to documentation.
The command line utility included in this project (cmd/jwt) provides a straightforward example of token creation and parsing as well as a useful tool for debugging your own integration. You'll also find several implementation examples in the documentation.

View file

@ -1,5 +1,18 @@
## `jwt-go` Version History
#### 3.2.0
* Added method `ParseUnverified` to allow users to split up the tasks of parsing and validation
* HMAC signing method returns `ErrInvalidKeyType` instead of `ErrInvalidKey` where appropriate
* Added options to `request.ParseFromRequest`, which allows for an arbitrary list of modifiers to parsing behavior. Initial set include `WithClaims` and `WithParser`. Existing usage of this function will continue to work as before.
* Deprecated `ParseFromRequestWithClaims` to simplify API in the future.
#### 3.1.0
* Improvements to `jwt` command line tool
* Added `SkipClaimsValidation` option to `Parser`
* Documentation updates
#### 3.0.0
* **Compatibility Breaking Changes**: See MIGRATION_GUIDE.md for tips on updating your code

View file

@ -14,6 +14,7 @@ var (
)
// Implements the ECDSA family of signing methods signing methods
// Expects *ecdsa.PrivateKey for signing and *ecdsa.PublicKey for verification
type SigningMethodECDSA struct {
Name string
Hash crypto.Hash

View file

@ -51,13 +51,9 @@ func (e ValidationError) Error() string {
} else {
return "token is invalid"
}
return e.Inner.Error()
}
// No errors
func (e *ValidationError) valid() bool {
if e.Errors > 0 {
return false
}
return true
return e.Errors == 0
}

View file

@ -7,6 +7,7 @@ import (
)
// Implements the HMAC-SHA family of signing methods signing methods
// Expects key type of []byte for both signing and validation
type SigningMethodHMAC struct {
Name string
Hash crypto.Hash
@ -90,5 +91,5 @@ func (m *SigningMethodHMAC) Sign(signingString string, key interface{}) (string,
return EncodeSegment(hasher.Sum(nil)), nil
}
return "", ErrInvalidKey
return "", ErrInvalidKeyType
}

View file

@ -8,8 +8,9 @@ import (
)
type Parser struct {
ValidMethods []string // If populated, only these methods will be considered valid
UseJSONNumber bool // Use JSON Number format in JSON decoder
ValidMethods []string // If populated, only these methods will be considered valid
UseJSONNumber bool // Use JSON Number format in JSON decoder
SkipClaimsValidation bool // Skip claims validation during token parsing
}
// Parse, validate, and return a token.
@ -20,55 +21,9 @@ func (p *Parser) Parse(tokenString string, keyFunc Keyfunc) (*Token, error) {
}
func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) {
parts := strings.Split(tokenString, ".")
if len(parts) != 3 {
return nil, NewValidationError("token contains an invalid number of segments", ValidationErrorMalformed)
}
var err error
token := &Token{Raw: tokenString}
// parse Header
var headerBytes []byte
if headerBytes, err = DecodeSegment(parts[0]); err != nil {
if strings.HasPrefix(strings.ToLower(tokenString), "bearer ") {
return token, NewValidationError("tokenstring should not contain 'bearer '", ValidationErrorMalformed)
}
return token, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
}
if err = json.Unmarshal(headerBytes, &token.Header); err != nil {
return token, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
}
// parse Claims
var claimBytes []byte
token.Claims = claims
if claimBytes, err = DecodeSegment(parts[1]); err != nil {
return token, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
}
dec := json.NewDecoder(bytes.NewBuffer(claimBytes))
if p.UseJSONNumber {
dec.UseNumber()
}
// JSON Decode. Special case for map type to avoid weird pointer behavior
if c, ok := token.Claims.(MapClaims); ok {
err = dec.Decode(&c)
} else {
err = dec.Decode(&claims)
}
// Handle decode error
token, parts, err := p.ParseUnverified(tokenString, claims)
if err != nil {
return token, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
}
// Lookup signature method
if method, ok := token.Header["alg"].(string); ok {
if token.Method = GetSigningMethod(method); token.Method == nil {
return token, NewValidationError("signing method (alg) is unavailable.", ValidationErrorUnverifiable)
}
} else {
return token, NewValidationError("signing method (alg) is unspecified.", ValidationErrorUnverifiable)
return token, err
}
// Verify signing method is in the required set
@ -95,20 +50,25 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf
}
if key, err = keyFunc(token); err != nil {
// keyFunc returned an error
if ve, ok := err.(*ValidationError); ok {
return token, ve
}
return token, &ValidationError{Inner: err, Errors: ValidationErrorUnverifiable}
}
vErr := &ValidationError{}
// Validate Claims
if err := token.Claims.Valid(); err != nil {
if !p.SkipClaimsValidation {
if err := token.Claims.Valid(); err != nil {
// If the Claims Valid returned an error, check if it is a validation error,
// If it was another error type, create a ValidationError with a generic ClaimsInvalid flag set
if e, ok := err.(*ValidationError); !ok {
vErr = &ValidationError{Inner: err, Errors: ValidationErrorClaimsInvalid}
} else {
vErr = e
// If the Claims Valid returned an error, check if it is a validation error,
// If it was another error type, create a ValidationError with a generic ClaimsInvalid flag set
if e, ok := err.(*ValidationError); !ok {
vErr = &ValidationError{Inner: err, Errors: ValidationErrorClaimsInvalid}
} else {
vErr = e
}
}
}
@ -126,3 +86,63 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf
return token, vErr
}
// WARNING: Don't use this method unless you know what you're doing
//
// This method parses the token but doesn't validate the signature. It's only
// ever useful in cases where you know the signature is valid (because it has
// been checked previously in the stack) and you want to extract values from
// it.
func (p *Parser) ParseUnverified(tokenString string, claims Claims) (token *Token, parts []string, err error) {
parts = strings.Split(tokenString, ".")
if len(parts) != 3 {
return nil, parts, NewValidationError("token contains an invalid number of segments", ValidationErrorMalformed)
}
token = &Token{Raw: tokenString}
// parse Header
var headerBytes []byte
if headerBytes, err = DecodeSegment(parts[0]); err != nil {
if strings.HasPrefix(strings.ToLower(tokenString), "bearer ") {
return token, parts, NewValidationError("tokenstring should not contain 'bearer '", ValidationErrorMalformed)
}
return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
}
if err = json.Unmarshal(headerBytes, &token.Header); err != nil {
return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
}
// parse Claims
var claimBytes []byte
token.Claims = claims
if claimBytes, err = DecodeSegment(parts[1]); err != nil {
return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
}
dec := json.NewDecoder(bytes.NewBuffer(claimBytes))
if p.UseJSONNumber {
dec.UseNumber()
}
// JSON Decode. Special case for map type to avoid weird pointer behavior
if c, ok := token.Claims.(MapClaims); ok {
err = dec.Decode(&c)
} else {
err = dec.Decode(&claims)
}
// Handle decode error
if err != nil {
return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
}
// Lookup signature method
if method, ok := token.Header["alg"].(string); ok {
if token.Method = GetSigningMethod(method); token.Method == nil {
return token, parts, NewValidationError("signing method (alg) is unavailable.", ValidationErrorUnverifiable)
}
} else {
return token, parts, NewValidationError("signing method (alg) is unspecified.", ValidationErrorUnverifiable)
}
return token, parts, nil
}

View file

@ -7,6 +7,7 @@ import (
)
// Implements the RSA family of signing methods signing methods
// Expects *rsa.PrivateKey for signing and *rsa.PublicKey for validation
type SigningMethodRSA struct {
Name string
Hash crypto.Hash
@ -44,7 +45,7 @@ func (m *SigningMethodRSA) Alg() string {
}
// Implements the Verify method from SigningMethod
// For this signing method, must be an rsa.PublicKey structure.
// For this signing method, must be an *rsa.PublicKey structure.
func (m *SigningMethodRSA) Verify(signingString, signature string, key interface{}) error {
var err error
@ -73,7 +74,7 @@ func (m *SigningMethodRSA) Verify(signingString, signature string, key interface
}
// Implements the Sign method from SigningMethod
// For this signing method, must be an rsa.PrivateKey structure.
// For this signing method, must be an *rsa.PrivateKey structure.
func (m *SigningMethodRSA) Sign(signingString string, key interface{}) (string, error) {
var rsaKey *rsa.PrivateKey
var ok bool

View file

@ -39,6 +39,38 @@ func ParseRSAPrivateKeyFromPEM(key []byte) (*rsa.PrivateKey, error) {
return pkey, nil
}
// Parse PEM encoded PKCS1 or PKCS8 private key protected with password
func ParseRSAPrivateKeyFromPEMWithPassword(key []byte, password string) (*rsa.PrivateKey, error) {
var err error
// Parse PEM block
var block *pem.Block
if block, _ = pem.Decode(key); block == nil {
return nil, ErrKeyMustBePEMEncoded
}
var parsedKey interface{}
var blockDecrypted []byte
if blockDecrypted, err = x509.DecryptPEMBlock(block, []byte(password)); err != nil {
return nil, err
}
if parsedKey, err = x509.ParsePKCS1PrivateKey(blockDecrypted); err != nil {
if parsedKey, err = x509.ParsePKCS8PrivateKey(blockDecrypted); err != nil {
return nil, err
}
}
var pkey *rsa.PrivateKey
var ok bool
if pkey, ok = parsedKey.(*rsa.PrivateKey); !ok {
return nil, ErrNotRSAPrivateKey
}
return pkey, nil
}
// Parse PEM encoded PKCS1 or PKCS8 public key
func ParseRSAPublicKeyFromPEM(key []byte) (*rsa.PublicKey, error) {
var err error

View file

@ -1,148 +0,0 @@
# Tips
## Implementing default logging and re-authentication attempts
You can implement custom logging and/or limit re-auth attempts by creating a custom HTTP client
like the following and setting it as the provider client's HTTP Client (via the
`gophercloud.ProviderClient.HTTPClient` field):
```go
//...
// LogRoundTripper satisfies the http.RoundTripper interface and is used to
// customize the default Gophercloud RoundTripper to allow for logging.
type LogRoundTripper struct {
rt http.RoundTripper
numReauthAttempts int
}
// newHTTPClient return a custom HTTP client that allows for logging relevant
// information before and after the HTTP request.
func newHTTPClient() http.Client {
return http.Client{
Transport: &LogRoundTripper{
rt: http.DefaultTransport,
},
}
}
// RoundTrip performs a round-trip HTTP request and logs relevant information about it.
func (lrt *LogRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
glog.Infof("Request URL: %s\n", request.URL)
response, err := lrt.rt.RoundTrip(request)
if response == nil {
return nil, err
}
if response.StatusCode == http.StatusUnauthorized {
if lrt.numReauthAttempts == 3 {
return response, fmt.Errorf("Tried to re-authenticate 3 times with no success.")
}
lrt.numReauthAttempts++
}
glog.Debugf("Response Status: %s\n", response.Status)
return response, nil
}
endpoint := "https://127.0.0.1/auth"
pc := openstack.NewClient(endpoint)
pc.HTTPClient = newHTTPClient()
//...
```
## Implementing custom objects
OpenStack request/response objects may differ among variable names or types.
### Custom request objects
To pass custom options to a request, implement the desired `<ACTION>OptsBuilder` interface. For
example, to pass in
```go
type MyCreateServerOpts struct {
Name string
Size int
}
```
to `servers.Create`, simply implement the `servers.CreateOptsBuilder` interface:
```go
func (o MyCreateServeropts) ToServerCreateMap() (map[string]interface{}, error) {
return map[string]interface{}{
"name": o.Name,
"size": o.Size,
}, nil
}
```
create an instance of your custom options object, and pass it to `servers.Create`:
```go
// ...
myOpts := MyCreateServerOpts{
Name: "s1",
Size: "100",
}
server, err := servers.Create(computeClient, myOpts).Extract()
// ...
```
### Custom response objects
Some OpenStack services have extensions. Extensions that are supported in Gophercloud can be
combined to create a custom object:
```go
// ...
type MyVolume struct {
volumes.Volume
tenantattr.VolumeExt
}
var v struct {
MyVolume `json:"volume"`
}
err := volumes.Get(client, volID).ExtractInto(&v)
// ...
```
## Overriding default `UnmarshalJSON` method
For some response objects, a field may be a custom type or may be allowed to take on
different types. In these cases, overriding the default `UnmarshalJSON` method may be
necessary. To do this, declare the JSON `struct` field tag as "-" and create an `UnmarshalJSON`
method on the type:
```go
// ...
type MyVolume struct {
ID string `json: "id"`
TimeCreated time.Time `json: "-"`
}
func (r *MyVolume) UnmarshalJSON(b []byte) error {
type tmp MyVolume
var s struct {
tmp
TimeCreated gophercloud.JSONRFC3339MilliNoZ `json:"created_at"`
}
err := json.Unmarshal(b, &s)
if err != nil {
return err
}
*r = Volume(s.tmp)
r.TimeCreated = time.Time(s.CreatedAt)
return err
}
// ...
```

View file

@ -1,32 +0,0 @@
# Compute
## Floating IPs
* `github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingip` is now `github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips`
* `floatingips.Associate` and `floatingips.Disassociate` have been removed.
* `floatingips.DisassociateOpts` is now required to disassociate a Floating IP.
## Security Groups
* `secgroups.AddServerToGroup` is now `secgroups.AddServer`.
* `secgroups.RemoveServerFromGroup` is now `secgroups.RemoveServer`.
## Servers
* `servers.Reboot` now requires a `servers.RebootOpts` struct:
```golang
rebootOpts := &servers.RebootOpts{
Type: servers.SoftReboot,
}
res := servers.Reboot(client, server.ID, rebootOpts)
```
# Identity
## V3
### Tokens
* `Token.ExpiresAt` is now of type `gophercloud.JSONRFC3339Milli` instead of
`time.Time`

View file

@ -127,7 +127,7 @@ new resource in the `server` variable (a
## Advanced Usage
Have a look at the [FAQ](./FAQ.md) for some tips on customizing the way Gophercloud works.
Have a look at the [FAQ](./docs/FAQ.md) for some tips on customizing the way Gophercloud works.
## Backwards-Compatibility Guarantees
@ -141,3 +141,19 @@ See the [contributing guide](./.github/CONTRIBUTING.md).
If you're struggling with something or have spotted a potential bug, feel free
to submit an issue to our [bug tracker](/issues).
## Thank You
We'd like to extend special thanks and appreciation to the following:
### OpenLab
<a href="http://openlabtesting.org/"><img src="./docs/assets/openlab.png" width="600px"></a>
OpenLab is providing a full CI environment to test each PR and merge for a variety of OpenStack releases.
### VEXXHOST
<a href="https://vexxhost.com/"><img src="./docs/assets/vexxhost.png" width="600px"></a>
VEXXHOST is providing their services to assist with the development and testing of Gophercloud.

View file

@ -1,74 +0,0 @@
## On Pull Requests
- Before you start a PR there needs to be a Github issue and a discussion about it
on that issue with a core contributor, even if it's just a 'SGTM'.
- A PR's description must reference the issue it closes with a `For <ISSUE NUMBER>` (e.g. For #293).
- A PR's description must contain link(s) to the line(s) in the OpenStack
source code (on Github) that prove(s) the PR code to be valid. Links to documentation
are not good enough. The link(s) should be to a non-`master` branch. For example,
a pull request implementing the creation of a Neutron v2 subnet might put the
following link in the description:
https://github.com/openstack/neutron/blob/stable/mitaka/neutron/api/v2/attributes.py#L749
From that link, a reviewer (or user) can verify the fields in the request/response
objects in the PR.
- A PR that is in-progress should have `[wip]` in front of the PR's title. When
ready for review, remove the `[wip]` and ping a core contributor with an `@`.
- Forcing PRs to be small can have the effect of users submitting PRs in a hierarchical chain, with
one depending on the next. If a PR depends on another one, it should have a [Pending #PRNUM]
prefix in the PR title. In addition, it will be the PR submitter's responsibility to remove the
[Pending #PRNUM] tag once the PR has been updated with the merged, dependent PR. That will
let reviewers know it is ready to review.
- A PR should be small. Even if you intend on implementing an entire
service, a PR should only be one route of that service
(e.g. create server or get server, but not both).
- Unless explicitly asked, do not squash commits in the middle of a review; only
append. It makes it difficult for the reviewer to see what's changed from one
review to the next.
## On Code
- In re design: follow as closely as is reasonable the code already in the library.
Most operations (e.g. create, delete) admit the same design.
- Unit tests and acceptance (integration) tests must be written to cover each PR.
Tests for operations with several options (e.g. list, create) should include all
the options in the tests. This will allow users to verify an operation on their
own infrastructure and see an example of usage.
- If in doubt, ask in-line on the PR.
### File Structure
- The following should be used in most cases:
- `requests.go`: contains all the functions that make HTTP requests and the
types associated with the HTTP request (parameters for URL, body, etc)
- `results.go`: contains all the response objects and their methods
- `urls.go`: contains the endpoints to which the requests are made
### Naming
- For methods on a type in `results.go`, the receiver should be named `r` and the
variable into which it will be unmarshalled `s`.
- Functions in `requests.go`, with the exception of functions that return a
`pagination.Pager`, should be named returns of the name `r`.
- Functions in `requests.go` that accept request bodies should accept as their
last parameter an `interface` named `<Action>OptsBuilder` (eg `CreateOptsBuilder`).
This `interface` should have at the least a method named `To<Resource><Action>Map`
(eg `ToPortCreateMap`).
- Functions in `requests.go` that accept query strings should accept as their
last parameter an `interface` named `<Action>OptsBuilder` (eg `ListOptsBuilder`).
This `interface` should have at the least a method named `To<Resource><Action>Query`
(eg `ToServerListQuery`).

View file

@ -9,12 +9,32 @@ ProviderClient representing an active session on that provider.
Its fields are the union of those recognized by each identity implementation and
provider.
An example of manually providing authentication information:
opts := gophercloud.AuthOptions{
IdentityEndpoint: "https://openstack.example.com:5000/v2.0",
Username: "{username}",
Password: "{password}",
TenantID: "{tenant_id}",
}
provider, err := openstack.AuthenticatedClient(opts)
An example of using AuthOptionsFromEnv(), where the environment variables can
be read from a file, such as a standard openrc file:
opts, err := openstack.AuthOptionsFromEnv()
provider, err := openstack.AuthenticatedClient(opts)
*/
type AuthOptions struct {
// IdentityEndpoint specifies the HTTP endpoint that is required to work with
// the Identity API of the appropriate version. While it's ultimately needed by
// all of the identity services, it will often be populated by a provider-level
// function.
//
// The IdentityEndpoint is typically referred to as the "auth_url" or
// "OS_AUTH_URL" in the information provided by the cloud operator.
IdentityEndpoint string `json:"-"`
// Username is required if using Identity V2 API. Consult with your provider's
@ -39,7 +59,7 @@ type AuthOptions struct {
// If DomainID or DomainName are provided, they will also apply to TenantName.
// It is not currently possible to authenticate with Username and a Domain
// and scope to a Project in a different Domain by using TenantName. To
// accomplish that, the ProjectID will need to be provided to the TenantID
// accomplish that, the ProjectID will need to be provided as the TenantID
// option.
TenantID string `json:"tenantId,omitempty"`
TenantName string `json:"tenantName,omitempty"`
@ -50,15 +70,28 @@ type AuthOptions struct {
// false, it will not cache these settings, but re-authentication will not be
// possible. This setting defaults to false.
//
// NOTE: The reauth function will try to re-authenticate endlessly if left unchecked.
// The way to limit the number of attempts is to provide a custom HTTP client to the provider client
// and provide a transport that implements the RoundTripper interface and stores the number of failed retries.
// For an example of this, see here: https://github.com/rackspace/rack/blob/1.0.0/auth/clients.go#L311
// NOTE: The reauth function will try to re-authenticate endlessly if left
// unchecked. The way to limit the number of attempts is to provide a custom
// HTTP client to the provider client and provide a transport that implements
// the RoundTripper interface and stores the number of failed retries. For an
// example of this, see here:
// https://github.com/rackspace/rack/blob/1.0.0/auth/clients.go#L311
AllowReauth bool `json:"-"`
// TokenID allows users to authenticate (possibly as another user) with an
// authentication token ID.
TokenID string `json:"-"`
// Scope determines the scoping of the authentication request.
Scope *AuthScope `json:"-"`
}
// AuthScope allows a created token to be limited to a specific domain or project.
type AuthScope struct {
ProjectID string
ProjectName string
DomainID string
DomainName string
}
// ToTokenV2CreateMap allows AuthOptions to satisfy the AuthOptionsBuilder
@ -241,82 +274,85 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
}
func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) {
var scope struct {
ProjectID string
ProjectName string
DomainID string
DomainName string
}
if opts.TenantID != "" {
scope.ProjectID = opts.TenantID
} else {
if opts.TenantName != "" {
scope.ProjectName = opts.TenantName
scope.DomainID = opts.DomainID
scope.DomainName = opts.DomainName
// For backwards compatibility.
// If AuthOptions.Scope was not set, try to determine it.
// This works well for common scenarios.
if opts.Scope == nil {
opts.Scope = new(AuthScope)
if opts.TenantID != "" {
opts.Scope.ProjectID = opts.TenantID
} else {
if opts.TenantName != "" {
opts.Scope.ProjectName = opts.TenantName
opts.Scope.DomainID = opts.DomainID
opts.Scope.DomainName = opts.DomainName
}
}
}
if scope.ProjectName != "" {
if opts.Scope.ProjectName != "" {
// ProjectName provided: either DomainID or DomainName must also be supplied.
// ProjectID may not be supplied.
if scope.DomainID == "" && scope.DomainName == "" {
if opts.Scope.DomainID == "" && opts.Scope.DomainName == "" {
return nil, ErrScopeDomainIDOrDomainName{}
}
if scope.ProjectID != "" {
if opts.Scope.ProjectID != "" {
return nil, ErrScopeProjectIDOrProjectName{}
}
if scope.DomainID != "" {
if opts.Scope.DomainID != "" {
// ProjectName + DomainID
return map[string]interface{}{
"project": map[string]interface{}{
"name": &scope.ProjectName,
"domain": map[string]interface{}{"id": &scope.DomainID},
"name": &opts.Scope.ProjectName,
"domain": map[string]interface{}{"id": &opts.Scope.DomainID},
},
}, nil
}
if scope.DomainName != "" {
if opts.Scope.DomainName != "" {
// ProjectName + DomainName
return map[string]interface{}{
"project": map[string]interface{}{
"name": &scope.ProjectName,
"domain": map[string]interface{}{"name": &scope.DomainName},
"name": &opts.Scope.ProjectName,
"domain": map[string]interface{}{"name": &opts.Scope.DomainName},
},
}, nil
}
} else if scope.ProjectID != "" {
} else if opts.Scope.ProjectID != "" {
// ProjectID provided. ProjectName, DomainID, and DomainName may not be provided.
if scope.DomainID != "" {
if opts.Scope.DomainID != "" {
return nil, ErrScopeProjectIDAlone{}
}
if scope.DomainName != "" {
if opts.Scope.DomainName != "" {
return nil, ErrScopeProjectIDAlone{}
}
// ProjectID
return map[string]interface{}{
"project": map[string]interface{}{
"id": &scope.ProjectID,
"id": &opts.Scope.ProjectID,
},
}, nil
} else if scope.DomainID != "" {
} else if opts.Scope.DomainID != "" {
// DomainID provided. ProjectID, ProjectName, and DomainName may not be provided.
if scope.DomainName != "" {
if opts.Scope.DomainName != "" {
return nil, ErrScopeDomainIDOrDomainName{}
}
// DomainID
return map[string]interface{}{
"domain": map[string]interface{}{
"id": &scope.DomainID,
"id": &opts.Scope.DomainID,
},
}, nil
} else if opts.Scope.DomainName != "" {
// DomainName
return map[string]interface{}{
"domain": map[string]interface{}{
"name": &opts.Scope.DomainName,
},
}, nil
} else if scope.DomainName != "" {
return nil, ErrScopeDomainName{}
}
return nil, nil

View file

@ -3,11 +3,17 @@ Package gophercloud provides a multi-vendor interface to OpenStack-compatible
clouds. The library has a three-level hierarchy: providers, services, and
resources.
Provider structs represent the service providers that offer and manage a
collection of services. The IdentityEndpoint is typically refered to as
"auth_url" in information provided by the cloud operator. Additionally,
the cloud may refer to TenantID or TenantName as project_id and project_name.
These are defined like so:
Authenticating with Providers
Provider structs represent the cloud providers that offer and manage a
collection of services. You will generally want to create one Provider
client per OpenStack cloud.
Use your OpenStack credentials to create a Provider client. The
IdentityEndpoint is typically refered to as "auth_url" or "OS_AUTH_URL" in
information provided by the cloud operator. Additionally, the cloud may refer to
TenantID or TenantName as project_id and project_name. Credentials are
specified like so:
opts := gophercloud.AuthOptions{
IdentityEndpoint: "https://openstack.example.com:5000/v2.0",
@ -18,6 +24,16 @@ These are defined like so:
provider, err := openstack.AuthenticatedClient(opts)
You may also use the openstack.AuthOptionsFromEnv() helper function. This
function reads in standard environment variables frequently found in an
OpenStack `openrc` file. Again note that Gophercloud currently uses "tenant"
instead of "project".
opts, err := openstack.AuthOptionsFromEnv()
provider, err := openstack.AuthenticatedClient(opts)
Service Clients
Service structs are specific to a provider and handle all of the logic and
operations for a particular OpenStack service. Examples of services include:
Compute, Object Storage, Block Storage. In order to define one, you need to
@ -27,6 +43,8 @@ pass in the parent provider, like so:
client := openstack.NewComputeV2(provider, opts)
Resources
Resource structs are the domain models that services make use of in order
to work with and represent the state of API resources:
@ -62,6 +80,12 @@ of results:
return true, nil
})
If you want to obtain the entire collection of pages without doing any
intermediary processing on each page, you can use the AllPages method:
allPages, err := servers.List(client, nil).AllPages()
allServers, err := servers.ExtractServers(allPages)
This top-level package contains utility functions and data types that are used
throughout the provider and service packages. Of particular note for end users
are the AuthOptions and EndpointOpts structs.

View file

@ -27,7 +27,7 @@ const (
// unambiguously identify one, and only one, endpoint within the catalog.
//
// Usually, these are passed to service client factory functions in a provider
// package, like "rackspace.NewComputeV2()".
// package, like "openstack.NewComputeV2()".
type EndpointOpts struct {
// Type [required] is the service type for the client (e.g., "compute",
// "object-store"). Generally, this will be supplied by the service client

View file

@ -1,6 +1,9 @@
package gophercloud
import "fmt"
import (
"fmt"
"strings"
)
// BaseError is an error type that all other error types embed.
type BaseError struct {
@ -43,6 +46,33 @@ func (e ErrInvalidInput) Error() string {
return e.choseErrString()
}
// ErrMissingEnvironmentVariable is the error when environment variable is required
// in a particular situation but not provided by the user
type ErrMissingEnvironmentVariable struct {
BaseError
EnvironmentVariable string
}
func (e ErrMissingEnvironmentVariable) Error() string {
e.DefaultErrString = fmt.Sprintf("Missing environment variable [%s]", e.EnvironmentVariable)
return e.choseErrString()
}
// ErrMissingAnyoneOfEnvironmentVariables is the error when anyone of the environment variables
// is required in a particular situation but not provided by the user
type ErrMissingAnyoneOfEnvironmentVariables struct {
BaseError
EnvironmentVariables []string
}
func (e ErrMissingAnyoneOfEnvironmentVariables) Error() string {
e.DefaultErrString = fmt.Sprintf(
"Missing one of the following environment variables [%s]",
strings.Join(e.EnvironmentVariables, ", "),
)
return e.choseErrString()
}
// ErrUnexpectedResponseCode is returned by the Request method when a response code other than
// those listed in OkCodes is encountered.
type ErrUnexpectedResponseCode struct {
@ -72,6 +102,11 @@ type ErrDefault401 struct {
ErrUnexpectedResponseCode
}
// ErrDefault403 is the default error type returned on a 403 HTTP response code.
type ErrDefault403 struct {
ErrUnexpectedResponseCode
}
// ErrDefault404 is the default error type returned on a 404 HTTP response code.
type ErrDefault404 struct {
ErrUnexpectedResponseCode
@ -103,11 +138,22 @@ type ErrDefault503 struct {
}
func (e ErrDefault400) Error() string {
return "Invalid request due to incorrect syntax or missing required parameters."
e.DefaultErrString = fmt.Sprintf(
"Bad request with: [%s %s], error message: %s",
e.Method, e.URL, e.Body,
)
return e.choseErrString()
}
func (e ErrDefault401) Error() string {
return "Authentication failed"
}
func (e ErrDefault403) Error() string {
e.DefaultErrString = fmt.Sprintf(
"Request forbidden: [%s %s], error message: %s",
e.Method, e.URL, e.Body,
)
return e.choseErrString()
}
func (e ErrDefault404) Error() string {
return "Resource not found"
}
@ -141,6 +187,12 @@ type Err401er interface {
Error401(ErrUnexpectedResponseCode) error
}
// Err403er is the interface resource error types implement to override the error message
// from a 403 error.
type Err403er interface {
Error403(ErrUnexpectedResponseCode) error
}
// Err404er is the interface resource error types implement to override the error message
// from a 404 error.
type Err404er interface {
@ -393,13 +445,6 @@ func (e ErrScopeProjectIDAlone) Error() string {
return "ProjectID must be supplied alone in a Scope"
}
// ErrScopeDomainName indicates that a DomainName was provided alone in a Scope.
type ErrScopeDomainName struct{ BaseError }
func (e ErrScopeDomainName) Error() string {
return "DomainName must be supplied with a ProjectName or ProjectID in a Scope"
}
// ErrScopeEmpty indicates that no credentials were provided in a Scope.
type ErrScopeEmpty struct{ BaseError }

View file

@ -8,10 +8,27 @@ import (
var nilOptions = gophercloud.AuthOptions{}
// AuthOptionsFromEnv fills out an identity.AuthOptions structure with the settings found on the various OpenStack
// OS_* environment variables. The following variables provide sources of truth: OS_AUTH_URL, OS_USERNAME,
// OS_PASSWORD, OS_TENANT_ID, and OS_TENANT_NAME. Of these, OS_USERNAME, OS_PASSWORD, and OS_AUTH_URL must
// have settings, or an error will result. OS_TENANT_ID and OS_TENANT_NAME are optional.
/*
AuthOptionsFromEnv fills out an identity.AuthOptions structure with the
settings found on the various OpenStack OS_* environment variables.
The following variables provide sources of truth: OS_AUTH_URL, OS_USERNAME,
OS_PASSWORD, OS_TENANT_ID, and OS_TENANT_NAME.
Of these, OS_USERNAME, OS_PASSWORD, and OS_AUTH_URL must have settings,
or an error will result. OS_TENANT_ID, OS_TENANT_NAME, OS_PROJECT_ID, and
OS_PROJECT_NAME are optional.
OS_TENANT_ID and OS_TENANT_NAME are mutually exclusive to OS_PROJECT_ID and
OS_PROJECT_NAME. If OS_PROJECT_ID and OS_PROJECT_NAME are set, they will
still be referred as "tenant" in Gophercloud.
To use this function, first set the OS_* environment variables (for example,
by sourcing an `openrc` file), then:
opts, err := openstack.AuthOptionsFromEnv()
provider, err := openstack.AuthenticatedClient(opts)
*/
func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) {
authURL := os.Getenv("OS_AUTH_URL")
username := os.Getenv("OS_USERNAME")
@ -22,18 +39,34 @@ func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) {
domainID := os.Getenv("OS_DOMAIN_ID")
domainName := os.Getenv("OS_DOMAIN_NAME")
// If OS_PROJECT_ID is set, overwrite tenantID with the value.
if v := os.Getenv("OS_PROJECT_ID"); v != "" {
tenantID = v
}
// If OS_PROJECT_NAME is set, overwrite tenantName with the value.
if v := os.Getenv("OS_PROJECT_NAME"); v != "" {
tenantName = v
}
if authURL == "" {
err := gophercloud.ErrMissingInput{Argument: "authURL"}
err := gophercloud.ErrMissingEnvironmentVariable{
EnvironmentVariable: "OS_AUTH_URL",
}
return nilOptions, err
}
if username == "" && userID == "" {
err := gophercloud.ErrMissingInput{Argument: "username"}
err := gophercloud.ErrMissingAnyoneOfEnvironmentVariables{
EnvironmentVariables: []string{"OS_USERNAME", "OS_USERID"},
}
return nilOptions, err
}
if password == "" {
err := gophercloud.ErrMissingInput{Argument: "password"}
err := gophercloud.ErrMissingEnvironmentVariable{
EnvironmentVariable: "OS_PASSWORD",
}
return nilOptions, err
}

View file

@ -2,7 +2,6 @@ package openstack
import (
"fmt"
"net/url"
"reflect"
"github.com/gophercloud/gophercloud"
@ -12,43 +11,66 @@ import (
)
const (
v20 = "v2.0"
v30 = "v3.0"
// v2 represents Keystone v2.
// It should never increase beyond 2.0.
v2 = "v2.0"
// v3 represents Keystone v3.
// The version can be anything from v3 to v3.x.
v3 = "v3"
)
// NewClient prepares an unauthenticated ProviderClient instance.
// Most users will probably prefer using the AuthenticatedClient function instead.
// This is useful if you wish to explicitly control the version of the identity service that's used for authentication explicitly,
// for example.
/*
NewClient prepares an unauthenticated ProviderClient instance.
Most users will probably prefer using the AuthenticatedClient function
instead.
This is useful if you wish to explicitly control the version of the identity
service that's used for authentication explicitly, for example.
A basic example of using this would be:
ao, err := openstack.AuthOptionsFromEnv()
provider, err := openstack.NewClient(ao.IdentityEndpoint)
client, err := openstack.NewIdentityV3(provider, gophercloud.EndpointOpts{})
*/
func NewClient(endpoint string) (*gophercloud.ProviderClient, error) {
u, err := url.Parse(endpoint)
base, err := utils.BaseEndpoint(endpoint)
if err != nil {
return nil, err
}
hadPath := u.Path != ""
u.Path, u.RawQuery, u.Fragment = "", "", ""
base := u.String()
endpoint = gophercloud.NormalizeURL(endpoint)
base = gophercloud.NormalizeURL(base)
if hadPath {
return &gophercloud.ProviderClient{
IdentityBase: base,
IdentityEndpoint: endpoint,
}, nil
}
p := new(gophercloud.ProviderClient)
p.IdentityBase = base
p.IdentityEndpoint = endpoint
p.UseTokenLock()
return &gophercloud.ProviderClient{
IdentityBase: base,
IdentityEndpoint: "",
}, nil
return p, nil
}
// AuthenticatedClient logs in to an OpenStack cloud found at the identity endpoint specified by options, acquires a token, and
// returns a Client instance that's ready to operate.
// It first queries the root identity endpoint to determine which versions of the identity service are supported, then chooses
// the most recent identity service available to proceed.
/*
AuthenticatedClient logs in to an OpenStack cloud found at the identity endpoint
specified by the options, acquires a token, and returns a Provider Client
instance that's ready to operate.
If the full path to a versioned identity endpoint was specified (example:
http://example.com:5000/v3), that path will be used as the endpoint to query.
If a versionless endpoint was specified (example: http://example.com:5000/),
the endpoint will be queried to determine which versions of the identity service
are available, then chooses the most recent or most supported version.
Example:
ao, err := openstack.AuthOptionsFromEnv()
provider, err := openstack.AuthenticatedClient(ao)
client, err := openstack.NewNetworkV2(client, gophercloud.EndpointOpts{
Region: os.Getenv("OS_REGION_NAME"),
})
*/
func AuthenticatedClient(options gophercloud.AuthOptions) (*gophercloud.ProviderClient, error) {
client, err := NewClient(options.IdentityEndpoint)
if err != nil {
@ -62,11 +84,12 @@ func AuthenticatedClient(options gophercloud.AuthOptions) (*gophercloud.Provider
return client, nil
}
// Authenticate or re-authenticate against the most recent identity service supported at the provided endpoint.
// Authenticate or re-authenticate against the most recent identity service
// supported at the provided endpoint.
func Authenticate(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
versions := []*utils.Version{
{ID: v20, Priority: 20, Suffix: "/v2.0/"},
{ID: v30, Priority: 30, Suffix: "/v3/"},
{ID: v2, Priority: 20, Suffix: "/v2.0/"},
{ID: v3, Priority: 30, Suffix: "/v3/"},
}
chosen, endpoint, err := utils.ChooseVersion(client, versions)
@ -75,9 +98,9 @@ func Authenticate(client *gophercloud.ProviderClient, options gophercloud.AuthOp
}
switch chosen.ID {
case v20:
case v2:
return v2auth(client, endpoint, options, gophercloud.EndpointOpts{})
case v30:
case v3:
return v3auth(client, endpoint, &options, gophercloud.EndpointOpts{})
default:
// The switch statement must be out of date from the versions list.
@ -123,9 +146,21 @@ func v2auth(client *gophercloud.ProviderClient, endpoint string, options gopherc
}
if options.AllowReauth {
// here we're creating a throw-away client (tac). it's a copy of the user's provider client, but
// with the token and reauth func zeroed out. combined with setting `AllowReauth` to `false`,
// this should retry authentication only once
tac := *client
tac.ReauthFunc = nil
tac.TokenID = ""
tao := options
tao.AllowReauth = false
client.ReauthFunc = func() error {
client.TokenID = ""
return v2auth(client, endpoint, options, eo)
err := v2auth(&tac, endpoint, tao, eo)
if err != nil {
return err
}
client.TokenID = tac.TokenID
return nil
}
}
client.TokenID = token.ID
@ -167,9 +202,32 @@ func v3auth(client *gophercloud.ProviderClient, endpoint string, opts tokens3.Au
client.TokenID = token.ID
if opts.CanReauth() {
// here we're creating a throw-away client (tac). it's a copy of the user's provider client, but
// with the token and reauth func zeroed out. combined with setting `AllowReauth` to `false`,
// this should retry authentication only once
tac := *client
tac.ReauthFunc = nil
tac.TokenID = ""
var tao tokens3.AuthOptionsBuilder
switch ot := opts.(type) {
case *gophercloud.AuthOptions:
o := *ot
o.AllowReauth = false
tao = &o
case *tokens3.AuthOptions:
o := *ot
o.AllowReauth = false
tao = &o
default:
tao = opts
}
client.ReauthFunc = func() error {
client.TokenID = ""
return v3auth(client, endpoint, opts, eo)
err := v3auth(&tac, endpoint, tao, eo)
if err != nil {
return err
}
client.TokenID = tac.TokenID
return nil
}
}
client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
@ -179,7 +237,8 @@ func v3auth(client *gophercloud.ProviderClient, endpoint string, opts tokens3.Au
return nil
}
// NewIdentityV2 creates a ServiceClient that may be used to interact with the v2 identity service.
// NewIdentityV2 creates a ServiceClient that may be used to interact with the
// v2 identity service.
func NewIdentityV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
endpoint := client.IdentityBase + "v2.0/"
clientType := "identity"
@ -199,7 +258,8 @@ func NewIdentityV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOp
}, nil
}
// NewIdentityV3 creates a ServiceClient that may be used to access the v3 identity service.
// NewIdentityV3 creates a ServiceClient that may be used to access the v3
// identity service.
func NewIdentityV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
endpoint := client.IdentityBase + "v3/"
clientType := "identity"
@ -212,6 +272,19 @@ func NewIdentityV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOp
}
}
// Ensure endpoint still has a suffix of v3.
// This is because EndpointLocator might have found a versionless
// endpoint or the published endpoint is still /v2.0. In both
// cases, we need to fix the endpoint to point to /v3.
base, err := utils.BaseEndpoint(endpoint)
if err != nil {
return nil, err
}
base = gophercloud.NormalizeURL(base)
endpoint = base + "v3/"
return &gophercloud.ServiceClient{
ProviderClient: client,
Endpoint: endpoint,
@ -232,33 +305,43 @@ func initClientOpts(client *gophercloud.ProviderClient, eo gophercloud.EndpointO
return sc, nil
}
// NewObjectStorageV1 creates a ServiceClient that may be used with the v1 object storage package.
// NewObjectStorageV1 creates a ServiceClient that may be used with the v1
// object storage package.
func NewObjectStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
return initClientOpts(client, eo, "object-store")
}
// NewComputeV2 creates a ServiceClient that may be used with the v2 compute package.
// NewComputeV2 creates a ServiceClient that may be used with the v2 compute
// package.
func NewComputeV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
return initClientOpts(client, eo, "compute")
}
// NewNetworkV2 creates a ServiceClient that may be used with the v2 network package.
// NewNetworkV2 creates a ServiceClient that may be used with the v2 network
// package.
func NewNetworkV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
sc, err := initClientOpts(client, eo, "network")
sc.ResourceBase = sc.Endpoint + "v2.0/"
return sc, err
}
// NewBlockStorageV1 creates a ServiceClient that may be used to access the v1 block storage service.
// NewBlockStorageV1 creates a ServiceClient that may be used to access the v1
// block storage service.
func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
return initClientOpts(client, eo, "volume")
}
// NewBlockStorageV2 creates a ServiceClient that may be used to access the v2 block storage service.
// NewBlockStorageV2 creates a ServiceClient that may be used to access the v2
// block storage service.
func NewBlockStorageV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
return initClientOpts(client, eo, "volumev2")
}
// NewBlockStorageV3 creates a ServiceClient that may be used to access the v3 block storage service.
func NewBlockStorageV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
return initClientOpts(client, eo, "volumev3")
}
// NewSharedFileSystemV2 creates a ServiceClient that may be used to access the v2 shared file system service.
func NewSharedFileSystemV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
return initClientOpts(client, eo, "sharev2")
@ -270,7 +353,8 @@ func NewCDNV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (
return initClientOpts(client, eo, "cdn")
}
// NewOrchestrationV1 creates a ServiceClient that may be used to access the v1 orchestration service.
// NewOrchestrationV1 creates a ServiceClient that may be used to access the v1
// orchestration service.
func NewOrchestrationV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
return initClientOpts(client, eo, "orchestration")
}
@ -280,16 +364,45 @@ func NewDBV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*
return initClientOpts(client, eo, "database")
}
// NewDNSV2 creates a ServiceClient that may be used to access the v2 DNS service.
// NewDNSV2 creates a ServiceClient that may be used to access the v2 DNS
// service.
func NewDNSV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
sc, err := initClientOpts(client, eo, "dns")
sc.ResourceBase = sc.Endpoint + "v2/"
return sc, err
}
// NewImageServiceV2 creates a ServiceClient that may be used to access the v2 image service.
// NewImageServiceV2 creates a ServiceClient that may be used to access the v2
// image service.
func NewImageServiceV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
sc, err := initClientOpts(client, eo, "image")
sc.ResourceBase = sc.Endpoint + "v2/"
return sc, err
}
// NewLoadBalancerV2 creates a ServiceClient that may be used to access the v2
// load balancer service.
func NewLoadBalancerV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
sc, err := initClientOpts(client, eo, "load-balancer")
sc.ResourceBase = sc.Endpoint + "v2.0/"
return sc, err
}
// NewClusteringV1 creates a ServiceClient that may be used with the v1 clustering
// package.
func NewClusteringV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
return initClientOpts(client, eo, "clustering")
}
// NewMessagingV2 creates a ServiceClient that may be used with the v2 messaging
// service.
func NewMessagingV2(client *gophercloud.ProviderClient, clientID string, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
sc, err := initClientOpts(client, eo, "messaging")
sc.MoreHeaders = map[string]string{"Client-ID": clientID}
return sc, err
}
// NewContainerV1 creates a ServiceClient that may be used with v1 container package
func NewContainerV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
return initClientOpts(client, eo, "container")
}

View file

@ -1,15 +0,0 @@
// Package extensions provides information and interaction with the different extensions available
// for an OpenStack service.
//
// The purpose of OpenStack API extensions is to:
//
// - Introduce new features in the API without requiring a version change.
// - Introduce vendor-specific niche functionality.
// - Act as a proving ground for experimental functionalities that might be included in a future
// version of the API.
//
// Extensions usually have tags that prevent conflicts with other extensions that define attributes
// or resources with the same names, and with core resources and attributes.
// Because an extension might not be supported by all plug-ins, its availability varies with deployments
// and the specific plug-in.
package extensions

View file

@ -1 +0,0 @@
package extensions

View file

@ -1,20 +0,0 @@
package extensions
import (
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
// Get retrieves information for a specific extension using its alias.
func Get(c *gophercloud.ServiceClient, alias string) (r GetResult) {
_, r.Err = c.Get(ExtensionURL(c, alias), &r.Body, nil)
return
}
// List returns a Pager which allows you to iterate over the full collection of extensions.
// It does not accept query parameters.
func List(c *gophercloud.ServiceClient) pagination.Pager {
return pagination.NewPager(c, ListExtensionURL(c), func(r pagination.PageResult) pagination.Page {
return ExtensionPage{pagination.SinglePageBase(r)}
})
}

View file

@ -1,53 +0,0 @@
package extensions
import (
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
// GetResult temporarily stores the result of a Get call.
// Use its Extract() method to interpret it as an Extension.
type GetResult struct {
gophercloud.Result
}
// Extract interprets a GetResult as an Extension.
func (r GetResult) Extract() (*Extension, error) {
var s struct {
Extension *Extension `json:"extension"`
}
err := r.ExtractInto(&s)
return s.Extension, err
}
// Extension is a struct that represents an OpenStack extension.
type Extension struct {
Updated string `json:"updated"`
Name string `json:"name"`
Links []interface{} `json:"links"`
Namespace string `json:"namespace"`
Alias string `json:"alias"`
Description string `json:"description"`
}
// ExtensionPage is the page returned by a pager when traversing over a collection of extensions.
type ExtensionPage struct {
pagination.SinglePageBase
}
// IsEmpty checks whether an ExtensionPage struct is empty.
func (r ExtensionPage) IsEmpty() (bool, error) {
is, err := ExtractExtensions(r)
return len(is) == 0, err
}
// ExtractExtensions accepts a Page struct, specifically an ExtensionPage struct, and extracts the
// elements into a slice of Extension structs.
// In other words, a generic collection is mapped into a relevant slice.
func ExtractExtensions(r pagination.Page) ([]Extension, error) {
var s struct {
Extensions []Extension `json:"extensions"`
}
err := (r.(ExtensionPage)).ExtractInto(&s)
return s.Extensions, err
}

Some files were not shown because too many files have changed in this diff Show more