diff --git a/builder/azure/chroot/builder.go b/builder/azure/chroot/builder.go index f2f14f407..c476b2fd1 100644 --- a/builder/azure/chroot/builder.go +++ b/builder/azure/chroot/builder.go @@ -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, }) diff --git a/builder/azure/chroot/step_verify_source_disk.go b/builder/azure/chroot/step_verify_source_disk.go new file mode 100644 index 000000000..b949447cb --- /dev/null +++ b/builder/azure/chroot/step_verify_source_disk.go @@ -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) {} diff --git a/builder/azure/chroot/step_verify_source_disk_test.go b/builder/azure/chroot/step_verify_source_disk_test.go new file mode 100644 index 000000000..113fd3f73 --- /dev/null +++ b/builder/azure/chroot/step_verify_source_disk_test.go @@ -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 +}