diff --git a/builder/azure/chroot/builder.go b/builder/azure/chroot/builder.go index 398b2230e..4b07916a6 100644 --- a/builder/azure/chroot/builder.go +++ b/builder/azure/chroot/builder.go @@ -4,8 +4,6 @@ import ( "context" "errors" "fmt" - "github.com/Azure/go-autorest/autorest/azure" - "github.com/Azure/go-autorest/autorest/to" "log" "runtime" "strings" @@ -20,6 +18,8 @@ import ( "github.com/hashicorp/packer/template/interpolate" "github.com/Azure/azure-sdk-for-go/profiles/latest/compute/mgmt/compute" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/to" ) type Config struct { @@ -29,6 +29,7 @@ type Config struct { FromScratch bool `mapstructure:"from_scratch"` Source string `mapstructure:"source"` + sourceType sourceType CommandWrapper string `mapstructure:"command_wrapper"` PreMountCommands []string `mapstructure:"pre_mount_commands"` @@ -52,6 +53,13 @@ type Config struct { ctx interpolate.Context } +type sourceType string + +const ( + sourcePlatformImage sourceType = "PlatformImage" + sourceDisk sourceType = "Disk" +) + func (c *Config) GetContext() interpolate.Context { return c.ctx } @@ -163,9 +171,14 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { } else { if _, err := client.ParsePlatformImageURN(b.config.Source); err == nil { log.Println("Source is platform image:", b.config.Source) + b.config.sourceType = sourcePlatformImage + } else if id, err := azure.ParseResourceID(b.config.Source); err == nil && + strings.EqualFold(id.Provider, "Microsoft.Compute") && strings.EqualFold(id.ResourceType, "disks") { + log.Println("Source is a disk resource ID:", b.config.Source) + b.config.sourceType = sourceDisk } else { errs = packer.MultiErrorAppend( - errs, fmt.Errorf("source: %q is not a valid platform image specifier", b.config.Source)) + errs, fmt.Errorf("source: %q is not a valid platform image specifier, nor is it a disk resource ID", b.config.Source)) } } @@ -301,16 +314,36 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack Location: info.Location, }) } else { - if pi, err := client.ParsePlatformImageURN(b.config.Source); err == nil { - if strings.EqualFold(pi.Version, "latest") { - - vmi, err := azcli.VirtualMachineImagesClient().GetLatest(ctx, pi.Publisher, pi.Offer, pi.Sku, info.Location) - if err != nil { - return nil, fmt.Errorf("error retieving latest version of %q: %v", b.config.Source, err) + switch b.config.sourceType { + case sourcePlatformImage: + + if pi, err := client.ParsePlatformImageURN(b.config.Source); err == nil { + if strings.EqualFold(pi.Version, "latest") { + + vmi, err := azcli.VirtualMachineImagesClient().GetLatest(ctx, pi.Publisher, pi.Offer, pi.Sku, info.Location) + if err != nil { + return nil, fmt.Errorf("error retieving latest version of %q: %v", b.config.Source, err) + } + pi.Version = to.String(vmi.Name) + log.Println("Resolved latest version of source image:", pi.Version) } - pi.Version = to.String(vmi.Name) - log.Println("Resolved latest version of source image:", pi.Version) + steps = append(steps, + &StepCreateNewDisk{ + SubscriptionID: info.SubscriptionID, + ResourceGroup: info.ResourceGroupName, + DiskName: b.config.TemporaryOSDiskName, + DiskSizeGB: b.config.OSDiskSizeGB, + DiskStorageAccountType: b.config.OSDiskStorageAccountType, + HyperVGeneration: b.config.ImageHyperVGeneration, + Location: info.Location, + PlatformImage: pi, + + SkipCleanup: b.config.OSDiskSkipCleanup, + }) + } else { + panic("Unknown image source: " + b.config.Source) } + case sourceDisk: steps = append(steps, &StepCreateNewDisk{ SubscriptionID: info.SubscriptionID, @@ -319,13 +352,13 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack DiskSizeGB: b.config.OSDiskSizeGB, DiskStorageAccountType: b.config.OSDiskStorageAccountType, HyperVGeneration: b.config.ImageHyperVGeneration, - Location: info.Location, - PlatformImage: pi, + SourceDiskResourceID: b.config.Source, + //todo(paulmey) validate that source disk is in same location as VM SkipCleanup: b.config.OSDiskSkipCleanup, }) - } else { - panic("Unknown image source: " + b.config.Source) + default: + panic(fmt.Errorf("Unknown source type: %+q", b.config.sourceType)) } } diff --git a/builder/azure/chroot/step_create_new_disk.go b/builder/azure/chroot/step_create_new_disk.go index d8df8b6b2..ed5844c4e 100644 --- a/builder/azure/chroot/step_create_new_disk.go +++ b/builder/azure/chroot/step_create_new_disk.go @@ -19,8 +19,11 @@ type StepCreateNewDisk struct { DiskSizeGB int32 // optional, ignored if 0 DiskStorageAccountType string // from compute.DiskStorageAccountTypes HyperVGeneration string - Location string - PlatformImage *client.PlatformImage + + Location string + PlatformImage *client.PlatformImage + + SourceDiskResourceID string SkipCleanup bool } @@ -55,7 +58,10 @@ func (s StepCreateNewDisk) Run(ctx context.Context, state multistep.StateBag) mu disk.DiskProperties.DiskSizeGB = to.Int32Ptr(s.DiskSizeGB) } - if s.PlatformImage == nil { + if s.SourceDiskResourceID != "" { + disk.CreationData.CreateOption = compute.Copy + disk.CreationData.SourceResourceID = to.StringPtr(s.SourceDiskResourceID) + } else if s.PlatformImage == nil { disk.CreationData.CreateOption = compute.Empty } else { disk.CreationData.CreateOption = compute.FromImage diff --git a/builder/azure/chroot/step_create_new_disk_test.go b/builder/azure/chroot/step_create_new_disk_test.go new file mode 100644 index 000000000..f0488ade7 --- /dev/null +++ b/builder/azure/chroot/step_create_new_disk_test.go @@ -0,0 +1,69 @@ +package chroot + +import ( + "context" + "io/ioutil" + "net/http" + "regexp" + "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_StepCreateNewDisk_FromDisk(t *testing.T) { + sut := StepCreateNewDisk{ + SubscriptionID: "SubscriptionID", + ResourceGroup: "ResourceGroupName", + DiskName: "TemporaryOSDiskName", + DiskSizeGB: 42, + DiskStorageAccountType: string(compute.PremiumLRS), + HyperVGeneration: string(compute.V1), + Location: "westus", + SourceDiskResourceID: "SourceDisk", + } + + expected := regexp.MustCompile(`[\s\n]`).ReplaceAllString(` +{ + "location": "westus", + "properties": { + "osType": "Linux", + "hyperVGeneration": "V1", + "creationData": { + "createOption": "Copy", + "sourceResourceId": "SourceDisk" + }, + "diskSizeGB": 42 + }, + "sku": { + "name": "Premium_LRS" + } +}`, "") + + m := compute.NewDisksClient("subscriptionId") + m.Sender = autorest.SenderFunc(func(r *http.Request) (*http.Response, error) { + b, _ := ioutil.ReadAll(r.Body) + if string(b) != expected { + t.Fatalf("expected body to be %q, but got %q", expected, string(b)) + } + return &http.Response{ + Request: r, + StatusCode: 200, + }, nil + }) + + state := new(multistep.BasicStateBag) + state.Put("azureclient", &client.AzureClientSetMock{ + DisksClientMock: m, + }) + state.Put("ui", packer.TestUi(t)) + + r := sut.Run(context.TODO(), state) + + if r != multistep.ActionContinue { + t.Fatal("Run failed") + } +} diff --git a/builder/azure/common/client/azure_client_set_mock.go b/builder/azure/common/client/azure_client_set_mock.go new file mode 100644 index 000000000..9f4a34b1c --- /dev/null +++ b/builder/azure/common/client/azure_client_set_mock.go @@ -0,0 +1,46 @@ +package client + +import ( + "github.com/Azure/azure-sdk-for-go/profiles/latest/compute/mgmt/compute/computeapi" + "github.com/Azure/go-autorest/autorest" +) + +// AzureClientSetMock provides a generic mock for AzureClientSet +type AzureClientSetMock struct { + DisksClientMock computeapi.DisksClientAPI + ImagesClientMock computeapi.ImagesClientAPI + VirtualMachineImagesClientMock VirtualMachineImagesClientAPI + VirtualMachinesClientMock computeapi.VirtualMachinesClientAPI + PollClientMock autorest.Client + MetadataClientMock MetadataClientAPI +} + +// DisksClient returns a DisksClientAPI +func (m *AzureClientSetMock) DisksClient() computeapi.DisksClientAPI { + return m.DisksClientMock +} + +// ImagesClient returns a ImagesClientAPI +func (m *AzureClientSetMock) ImagesClient() computeapi.ImagesClientAPI { + return m.ImagesClientMock +} + +// VirtualMachineImagesClient returns a VirtualMachineImagesClientAPI +func (m *AzureClientSetMock) VirtualMachineImagesClient() VirtualMachineImagesClientAPI { + return m.VirtualMachineImagesClientMock +} + +// VirtualMachinesClient returns a VirtualMachinesClientAPI +func (m *AzureClientSetMock) VirtualMachinesClient() computeapi.VirtualMachinesClientAPI { + return m.VirtualMachinesClientMock +} + +// PollClient returns an autorest Client that can be used for polling async requests +func (m *AzureClientSetMock) PollClient() autorest.Client { + return m.PollClientMock +} + +// MetadataClient returns a MetadataClientAPI +func (m *AzureClientSetMock) MetadataClient() MetadataClientAPI { + return m.MetadataClientMock +}