Add StepVerifySourceDisk

This commit is contained in:
Paul Meyer 2019-09-26 22:17:07 +00:00
parent cb729e5a38
commit 11ef06b94d
3 changed files with 241 additions and 24 deletions

View File

@ -36,58 +36,58 @@ type Config struct {
ClientConfig client.Config `mapstructure:",squash"`
// When set to `true`, starts with an empty, unpartitioned disk. Defaults to `false`.
FromScratch bool `mapstructure:"from_scratch"`
FromScratch bool `mapstructure:"from_scratch"`
// Either a managed disk resourced ID or a publisher:offer:sku:version specifier for plaform image sources.
Source string `mapstructure:"source" required:"true"`
sourceType sourceType
Source string `mapstructure:"source" required:"true"`
sourceType sourceType
// How to run shell commands. This may be useful to set environment variables or perhaps run
// a command with sudo or so on. This is a configuration template where the `.Command` variable
// is replaced with the command to be run. Defaults to `{{.Command}}`.
CommandWrapper string `mapstructure:"command_wrapper"`
// A series of commands to execute after attaching the root volume and before mounting the chroot.
// This is not required unless using `from_scratch`. If so, this should include any partitioning
CommandWrapper string `mapstructure:"command_wrapper"`
// A series of commands to execute after attaching the root volume and before mounting the chroot.
// This is not required unless using `from_scratch`. If so, this should include any partitioning
// and filesystem creation commands. The path to the device is provided by `{{.Device}}`.
PreMountCommands []string `mapstructure:"pre_mount_commands"`
// Options to supply the `mount` command when mounting devices. Each option will be prefixed with
PreMountCommands []string `mapstructure:"pre_mount_commands"`
// Options to supply the `mount` command when mounting devices. Each option will be prefixed with
// `-o` and supplied to the `mount` command ran by Packer. Because this command is ran in a shell,
// user discretion is advised. See this manual page for the `mount` command for valid file system specific options.
MountOptions []string `mapstructure:"mount_options"`
MountOptions []string `mapstructure:"mount_options"`
// The partition number containing the / partition. By default this is the first partition of the volume.
MountPartition string `mapstructure:"mount_partition"`
MountPartition string `mapstructure:"mount_partition"`
// The path where the volume will be mounted. This is where the chroot environment will be. This defaults
// to `/mnt/packer-amazon-chroot-volumes/{{.Device}}`. This is a configuration template where the `.Device`
// to `/mnt/packer-amazon-chroot-volumes/{{.Device}}`. This is a configuration template where the `.Device`
// variable is replaced with the name of the device where the volume is attached.
MountPath string `mapstructure:"mount_path"`
MountPath string `mapstructure:"mount_path"`
// As `pre_mount_commands`, but the commands are executed after mounting the root device and before the
// extra mount and copy steps. The device and mount path are provided by `{{.Device}}` and `{{.MountPath}}`.
PostMountCommands []string `mapstructure:"post_mount_commands"`
// This is a list of devices to mount into the chroot environment. This configuration parameter requires
PostMountCommands []string `mapstructure:"post_mount_commands"`
// This is a list of devices to mount into the chroot environment. This configuration parameter requires
// some additional documentation which is in the "Chroot Mounts" section below. Please read that section
// for more information on how to use this.
ChrootMounts [][]string `mapstructure:"chroot_mounts"`
// Paths to files on the running Azure instance that will be copied into the chroot environment prior to
ChrootMounts [][]string `mapstructure:"chroot_mounts"`
// Paths to files on the running Azure instance that will be copied into the chroot environment prior to
// provisioning. Defaults to `/etc/resolv.conf` so that DNS lookups work. Pass an empty list to skip copying
// `/etc/resolv.conf`. You may need to do this if you're building an image that uses systemd.
CopyFiles []string `mapstructure:"copy_files"`
CopyFiles []string `mapstructure:"copy_files"`
// The name of the temporary disk that will be created in the resource group of the VM that Packer is
// running on. Will be generated if not set.
TemporaryOSDiskName string `mapstructure:"temporary_os_disk_name"`
TemporaryOSDiskName string `mapstructure:"temporary_os_disk_name"`
// Try to resize the OS disk to this size on the first copy. Disks can only be englarged. If not specified,
// the disk will keep its original size. Required when using `from_scratch`
OSDiskSizeGB int32 `mapstructure:"os_disk_size_gb"`
OSDiskSizeGB int32 `mapstructure:"os_disk_size_gb"`
// The [storage SKU](https://docs.microsoft.com/en-us/rest/api/compute/disks/createorupdate#diskstorageaccounttypes)
// to use for the OS Disk. Defaults to `Standard_LRS`.
OSDiskStorageAccountType string `mapstructure:"os_disk_storage_account_type"`
// The [cache type](https://docs.microsoft.com/en-us/rest/api/compute/images/createorupdate#cachingtypes)
// specified in the resulting image and for attaching it to the Packer VM. Defaults to `ReadOnly`
OSDiskCacheType string `mapstructure:"os_disk_cache_type"`
OSDiskCacheType string `mapstructure:"os_disk_cache_type"`
// If set to `true`, leaves the temporary disk behind in the Packer VM resource group. Defaults to `false`
OSDiskSkipCleanup bool `mapstructure:"os_disk_skip_cleanup"`
OSDiskSkipCleanup bool `mapstructure:"os_disk_skip_cleanup"`
// The image to create using this build.
ImageResourceID string `mapstructure:"image_resource_id" required:"true"`
ImageResourceID string `mapstructure:"image_resource_id" required:"true"`
// The [Hyper-V generation type](https://docs.microsoft.com/en-us/rest/api/compute/images/createorupdate#hypervgenerationtypes).
// Defaults to `V2`.
ImageHyperVGeneration string `mapstructure:"image_hyperv_generation"`
@ -102,7 +102,7 @@ const (
sourceDisk sourceType = "Disk"
)
// GetContext implements ContextProvider to allow steps to use the config context
// GetContext implements ContextProvider to allow steps to use the config context
// for template interpolation
func (c *Config) GetContext() interpolate.Context {
return c.ctx
@ -354,6 +354,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
log.Println("Resolved latest version of source image:", pi.Version)
}
steps = append(steps,
&StepCreateNewDisk{
SubscriptionID: info.SubscriptionID,
ResourceGroup: info.ResourceGroupName,
@ -371,6 +372,11 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
}
case sourceDisk:
steps = append(steps,
&StepVerifySourceDisk{
SourceDiskResourceID: b.config.Source,
SubscriptionID: info.SubscriptionID,
Location: info.Location,
},
&StepCreateNewDisk{
SubscriptionID: info.SubscriptionID,
ResourceGroup: info.ResourceGroupName,
@ -379,7 +385,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
DiskStorageAccountType: b.config.OSDiskStorageAccountType,
HyperVGeneration: b.config.ImageHyperVGeneration,
SourceDiskResourceID: b.config.Source,
//todo(paulmey) validate that source disk is in same location as VM
Location: info.Location,
SkipCleanup: b.config.OSDiskSkipCleanup,
})

View File

@ -0,0 +1,65 @@
package chroot
import (
"context"
"fmt"
"strings"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/go-autorest/autorest/to"
"github.com/hashicorp/packer/builder/azure/common/client"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
type StepVerifySourceDisk struct {
SubscriptionID string
SourceDiskResourceID string
Location string
}
func (s StepVerifySourceDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
azcli := state.Get("azureclient").(client.AzureClientSet)
ui := state.Get("ui").(packer.Ui)
ui.Say("Checking source disk location")
resource, err := azure.ParseResourceID(s.SourceDiskResourceID)
if err != nil {
ui.Error(fmt.Sprintf("Could not parse resource id %q: %s", s.SourceDiskResourceID, err))
return multistep.ActionHalt
}
if !strings.EqualFold(resource.SubscriptionID, s.SubscriptionID) {
ui.Error(fmt.Sprintf("Source disk resource %q is in a different subscription than this VM (%q). "+
"Packer does not know how to handle that.",
s.SourceDiskResourceID, s.SubscriptionID))
return multistep.ActionHalt
}
if !(strings.EqualFold(resource.Provider, "Microsoft.Compute") && strings.EqualFold(resource.ResourceType, "disks")) {
ui.Error(fmt.Sprintf("Resource ID %q is not a managed disk resource", s.SourceDiskResourceID))
return multistep.ActionHalt
}
disk, err := azcli.DisksClient().Get(ctx,
resource.ResourceGroup, resource.ResourceName)
if err != nil {
ui.Error(fmt.Sprintf("Unable to retrieve disk (%q): %s", s.SourceDiskResourceID, err))
return multistep.ActionHalt
}
location := to.String(disk.Location)
if !strings.EqualFold(location, s.Location) {
ui.Error(fmt.Sprintf("Source disk resource %q is in a different location (%q) than this VM (%q). "+
"Packer does not know how to handle that.",
s.SourceDiskResourceID,
location,
s.Location))
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s StepVerifySourceDisk) Cleanup(state multistep.StateBag) {}

View File

@ -0,0 +1,146 @@
package chroot
import (
"context"
"io/ioutil"
"net/http"
"reflect"
"regexp"
"strings"
"testing"
"github.com/Azure/azure-sdk-for-go/profiles/latest/compute/mgmt/compute"
"github.com/Azure/go-autorest/autorest"
"github.com/hashicorp/packer/builder/azure/common/client"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
func Test_StepVerifySourceDisk_Run(t *testing.T) {
type fields struct {
SubscriptionID string
SourceDiskResourceID string
Location string
GetDiskResponseCode int
GetDiskResponseBody string
}
type args struct {
state multistep.StateBag
}
tests := []struct {
name string
fields fields
args args
want multistep.StepAction
errormatch string
}{
{
name: "HappyPath",
fields: fields{
SubscriptionID: "subid1",
SourceDiskResourceID: "/subscriptions/subid1/resourcegroups/rg1/providers/Microsoft.Compute/disks/disk1",
Location: "westus2",
GetDiskResponseCode: 200,
GetDiskResponseBody: `{"location":"westus2"}`,
},
want: multistep.ActionContinue,
},
{
name: "DiskNotFound",
fields: fields{
SubscriptionID: "subid1",
SourceDiskResourceID: "/subscriptions/subid1/resourcegroups/rg1/providers/Microsoft.Compute/disks/disk1",
Location: "westus2",
GetDiskResponseCode: 404,
GetDiskResponseBody: `{}`,
},
want: multistep.ActionHalt,
errormatch: "Unable to retrieve",
},
{
name: "NotADisk",
fields: fields{
SubscriptionID: "subid1",
SourceDiskResourceID: "/subscriptions/subid1/resourcegroups/rg1/providers/Microsoft.Compute/images/image1",
Location: "westus2",
GetDiskResponseCode: 404,
},
want: multistep.ActionHalt,
errormatch: "not a managed disk",
},
{
name: "OtherSubscription",
fields: fields{
SubscriptionID: "subid1",
SourceDiskResourceID: "/subscriptions/subid2/resourcegroups/rg1/providers/Microsoft.Compute/disks/disk1",
Location: "westus2",
GetDiskResponseCode: 200,
GetDiskResponseBody: `{"location":"westus2"}`,
},
want: multistep.ActionHalt,
errormatch: "different subscription",
},
{
name: "OtherLocation",
fields: fields{
SubscriptionID: "subid1",
SourceDiskResourceID: "/subscriptions/subid1/resourcegroups/rg1/providers/Microsoft.Compute/disks/disk1",
Location: "eastus",
GetDiskResponseCode: 200,
GetDiskResponseBody: `{"location":"westus2"}`,
},
want: multistep.ActionHalt,
errormatch: "different location",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := StepVerifySourceDisk{
SubscriptionID: tt.fields.SubscriptionID,
SourceDiskResourceID: tt.fields.SourceDiskResourceID,
Location: tt.fields.Location,
}
m := compute.NewDisksClient("subscriptionId")
m.Sender = autorest.SenderFunc(func(r *http.Request) (*http.Response, error) {
return &http.Response{
Request: r,
Body: ioutil.NopCloser(strings.NewReader(tt.fields.GetDiskResponseBody)),
StatusCode: tt.fields.GetDiskResponseCode,
}, nil
})
errorBuffer := &strings.Builder{}
ui := &packer.BasicUi{
Reader: strings.NewReader(""),
Writer: ioutil.Discard,
ErrorWriter: errorBuffer,
}
state := new(multistep.BasicStateBag)
state.Put("azureclient", &client.AzureClientSetMock{
DisksClientMock: m,
})
state.Put("ui", ui)
if got := s.Run(context.TODO(), state); !reflect.DeepEqual(got, tt.want) {
t.Errorf("StepVerifySourceDisk.Run() = %v, want %v", got, tt.want)
}
if tt.errormatch != "" {
if !regexp.MustCompile(tt.errormatch).MatchString(errorBuffer.String()) {
t.Errorf("Expected the error output (%q) to match %q", errorBuffer.String(), tt.errormatch)
}
}
})
}
}
type uiThatRemebersErrors struct {
packer.Ui
LastError string
}