mirror of
https://github.com/hashicorp/packer.git
synced 2026-02-22 01:10:18 -05:00
Merge remote-tracking branch 'upstream/master' into prestine
This commit is contained in:
commit
5298142da6
197 changed files with 16709 additions and 2283 deletions
50
CHANGELOG.md
50
CHANGELOG.md
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
16
Makefile
16
Makefile
|
|
@ -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 {} \;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ func TestStateBagShouldBePopulatedExpectedValues(t *testing.T) {
|
|||
constants.ArmStorageAccountName,
|
||||
constants.ArmVirtualMachineCaptureParameters,
|
||||
constants.ArmPublicIPAddressName,
|
||||
constants.ArmAsyncResourceGroupDelete,
|
||||
}
|
||||
|
||||
for _, v := range expectedStateBagKeys {
|
||||
|
|
|
|||
|
|
@ -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 == "" &&
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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'",
|
||||
|
|
|
|||
|
|
@ -35,4 +35,5 @@ const (
|
|||
ArmManagedImageResourceGroupName string = "arm.ManagedImageResourceGroupName"
|
||||
ArmManagedImageLocation string = "arm.ManagedImageLocation"
|
||||
ArmManagedImageName string = "arm.ManagedImageName"
|
||||
ArmAsyncResourceGroupDelete string = "arm.AsyncResourceGroupDelete"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 user’s 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 SDK’s 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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
62
builder/oracle/oci/step_get_default_credentials.go
Normal file
62
builder/oracle/oci/step_get_default_credentials.go
Normal 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
|
||||
}
|
||||
|
|
@ -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{}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// Code generated by pigeon; DO NOT EDIT.
|
||||
|
||||
package bootcommand
|
||||
|
||||
import (
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 {
|
||||
220
common/shell-local/config.go
Normal file
220
common/shell-local/config.go
Normal 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
|
||||
}
|
||||
16
common/shell-local/config_test.go
Normal file
16
common/shell-local/config_test.go
Normal 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
201
common/shell-local/run.go
Normal 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
|
||||
}
|
||||
BIN
common/test-fixtures/decompress-tar/outside_parent.tar
Normal file
BIN
common/test-fixtures/decompress-tar/outside_parent.tar
Normal file
Binary file not shown.
|
|
@ -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`}}",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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`}}",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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`}}",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": [{
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}]
|
||||
}
|
||||
|
|
@ -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": [{
|
||||
|
|
|
|||
36
examples/azure/windows_quickstart.json
Normal file
36
examples/azure/windows_quickstart.json
Normal 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 } }"
|
||||
]
|
||||
}]
|
||||
}
|
||||
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
38
provisioner/ansible-local/communicator_mock.go
Normal file
38
provisioner/ansible-local/communicator_mock.go
Normal 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() {
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
|
|
|||
5
provisioner/ansible-local/test-fixtures/hello.yml
Normal file
5
provisioner/ansible-local/test-fixtures/hello.yml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
- hosts: all
|
||||
tasks:
|
||||
- name: write Hello
|
||||
shell: echo -n "Hello" >> /tmp/hello_world
|
||||
5
provisioner/ansible-local/test-fixtures/world.yml
Normal file
5
provisioner/ansible-local/test-fixtures/world.yml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
- hosts: all
|
||||
tasks:
|
||||
- name: write world!
|
||||
shell: echo -n " world!" >> /tmp/hello_world
|
||||
15
provisioner/ansible-local/ui_stub.go
Normal file
15
provisioner/ansible-local/ui_stub.go
Normal 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) {}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
5
vendor/github.com/dgrijalva/jwt-go/MIGRATION_GUIDE.md
generated
vendored
5
vendor/github.com/dgrijalva/jwt-go/MIGRATION_GUIDE.md
generated
vendored
|
|
@ -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"])
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
35
vendor/github.com/dgrijalva/jwt-go/README.md
generated
vendored
35
vendor/github.com/dgrijalva/jwt-go/README.md
generated
vendored
|
|
@ -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
|
||||
|
||||
[](https://travis-ci.org/dgrijalva/jwt-go)
|
||||
[](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.
|
||||
|
|
|
|||
13
vendor/github.com/dgrijalva/jwt-go/VERSION_HISTORY.md
generated
vendored
13
vendor/github.com/dgrijalva/jwt-go/VERSION_HISTORY.md
generated
vendored
|
|
@ -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
|
||||
|
|
|
|||
1
vendor/github.com/dgrijalva/jwt-go/ecdsa.go
generated
vendored
1
vendor/github.com/dgrijalva/jwt-go/ecdsa.go
generated
vendored
|
|
@ -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
|
||||
|
|
|
|||
6
vendor/github.com/dgrijalva/jwt-go/errors.go
generated
vendored
6
vendor/github.com/dgrijalva/jwt-go/errors.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
3
vendor/github.com/dgrijalva/jwt-go/hmac.go
generated
vendored
3
vendor/github.com/dgrijalva/jwt-go/hmac.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
134
vendor/github.com/dgrijalva/jwt-go/parser.go
generated
vendored
134
vendor/github.com/dgrijalva/jwt-go/parser.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
5
vendor/github.com/dgrijalva/jwt-go/rsa.go
generated
vendored
5
vendor/github.com/dgrijalva/jwt-go/rsa.go
generated
vendored
|
|
@ -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
|
||||
|
|
|
|||
32
vendor/github.com/dgrijalva/jwt-go/rsa_utils.go
generated
vendored
32
vendor/github.com/dgrijalva/jwt-go/rsa_utils.go
generated
vendored
|
|
@ -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
|
||||
|
|
|
|||
148
vendor/github.com/gophercloud/gophercloud/FAQ.md
generated
vendored
148
vendor/github.com/gophercloud/gophercloud/FAQ.md
generated
vendored
|
|
@ -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
|
||||
}
|
||||
// ...
|
||||
```
|
||||
32
vendor/github.com/gophercloud/gophercloud/MIGRATING.md
generated
vendored
32
vendor/github.com/gophercloud/gophercloud/MIGRATING.md
generated
vendored
|
|
@ -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`
|
||||
18
vendor/github.com/gophercloud/gophercloud/README.md
generated
vendored
18
vendor/github.com/gophercloud/gophercloud/README.md
generated
vendored
|
|
@ -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.
|
||||
|
|
|
|||
74
vendor/github.com/gophercloud/gophercloud/STYLEGUIDE.md
generated
vendored
74
vendor/github.com/gophercloud/gophercloud/STYLEGUIDE.md
generated
vendored
|
|
@ -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`).
|
||||
112
vendor/github.com/gophercloud/gophercloud/auth_options.go
generated
vendored
112
vendor/github.com/gophercloud/gophercloud/auth_options.go
generated
vendored
|
|
@ -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
|
||||
|
|
|
|||
34
vendor/github.com/gophercloud/gophercloud/doc.go
generated
vendored
34
vendor/github.com/gophercloud/gophercloud/doc.go
generated
vendored
|
|
@ -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.
|
||||
|
|
|
|||
2
vendor/github.com/gophercloud/gophercloud/endpoint_search.go
generated
vendored
2
vendor/github.com/gophercloud/gophercloud/endpoint_search.go
generated
vendored
|
|
@ -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
|
||||
|
|
|
|||
63
vendor/github.com/gophercloud/gophercloud/errors.go
generated
vendored
63
vendor/github.com/gophercloud/gophercloud/errors.go
generated
vendored
|
|
@ -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 }
|
||||
|
||||
|
|
|
|||
47
vendor/github.com/gophercloud/gophercloud/openstack/auth_env.go
generated
vendored
47
vendor/github.com/gophercloud/gophercloud/openstack/auth_env.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
201
vendor/github.com/gophercloud/gophercloud/openstack/client.go
generated
vendored
201
vendor/github.com/gophercloud/gophercloud/openstack/client.go
generated
vendored
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
15
vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/doc.go
generated
vendored
15
vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/doc.go
generated
vendored
|
|
@ -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
|
||||
1
vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/errors.go
generated
vendored
1
vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/errors.go
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
package extensions
|
||||
20
vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/requests.go
generated
vendored
20
vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/requests.go
generated
vendored
|
|
@ -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)}
|
||||
})
|
||||
}
|
||||
53
vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/results.go
generated
vendored
53
vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/results.go
generated
vendored
|
|
@ -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
Loading…
Reference in a new issue