//go:generate struct-markdown package arm import ( "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/base64" "encoding/json" "fmt" "io/ioutil" "math/big" "net" "regexp" "strings" "time" "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2018-04-01/compute" "github.com/Azure/go-autorest/autorest/to" "github.com/masterzen/winrm" azcommon "github.com/hashicorp/packer/builder/azure/common" "github.com/hashicorp/packer/builder/azure/common/client" "github.com/hashicorp/packer/builder/azure/common/constants" "github.com/hashicorp/packer/builder/azure/pkcs12" "github.com/hashicorp/packer/common" commonhelper "github.com/hashicorp/packer/helper/common" "github.com/hashicorp/packer/helper/communicator" "github.com/hashicorp/packer/helper/config" "github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/template/interpolate" "golang.org/x/crypto/ssh" ) const ( DefaultImageVersion = "latest" DefaultUserName = "packer" DefaultPrivateVirtualNetworkWithPublicIp = false DefaultVMSize = "Standard_A1" ) const ( // https://docs.microsoft.com/en-us/azure/architecture/best-practices/naming-conventions#naming-rules-and-restrictions // Regular expressions in Go are not expressive enough, such that the regular expression returned by Azure // can be used (no backtracking). // // -> ^[^_\W][\w-._]{0,79}(? // // "shared_image_gallery_destination": { // "resource_group": "ResourceGroup", // "gallery_name": "GalleryName", // "image_name": "ImageName", // "image_version": "1.0.0", // "replication_regions": ["regionA", "regionB", "regionC"] // } // "managed_image_name": "TargetImageName", // "managed_image_resource_group_name": "TargetResourceGroup" SharedGalleryDestination SharedImageGalleryDestination `mapstructure:"shared_image_gallery_destination"` // How long to wait for an image to be published to the shared image // gallery before timing out. If your Packer build is failing on the // Publishing to Shared Image Gallery step with the error `Original Error: // context deadline exceeded`, but the image is present when you check your // Azure dashboard, then you probably need to increase this timeout from // its default of "60m" (valid time units include `s` for seconds, `m` for // minutes, and `h` for hours.) SharedGalleryTimeout time.Duration `mapstructure:"shared_image_gallery_timeout"` // PublisherName for your base image. See // [documentation](https://azure.microsoft.com/en-us/documentation/articles/resource-groups-vm-searching/) // for details. // // CLI example `az vm image list-publishers --location westus` ImagePublisher string `mapstructure:"image_publisher" required:"true"` // Offer for your base image. See // [documentation](https://azure.microsoft.com/en-us/documentation/articles/resource-groups-vm-searching/) // for details. // // CLI example // `az vm image list-offers --location westus --publisher Canonical` ImageOffer string `mapstructure:"image_offer" required:"true"` // SKU for your base image. See // [documentation](https://azure.microsoft.com/en-us/documentation/articles/resource-groups-vm-searching/) // for details. // // CLI example // `az vm image list-skus --location westus --publisher Canonical --offer UbuntuServer` ImageSku string `mapstructure:"image_sku" required:"true"` // Specify a specific version of an OS to boot from. // Defaults to `latest`. There may be a difference in versions available // across regions due to image synchronization latency. To ensure a consistent // version across regions set this value to one that is available in all // regions where you are deploying. // // CLI example // `az vm image list --location westus --publisher Canonical --offer UbuntuServer --sku 16.04.0-LTS --all` ImageVersion string `mapstructure:"image_version" required:"false"` // Specify a custom VHD to use. If this value is set, do // not set image_publisher, image_offer, image_sku, or image_version. ImageUrl string `mapstructure:"image_url" required:"false"` // Specify the source managed image's resource group used to use. If this // value is set, do not set image\_publisher, image\_offer, image\_sku, or // image\_version. If this value is set, the value // `custom_managed_image_name` must also be set. See // [documentation](https://docs.microsoft.com/en-us/azure/storage/storage-managed-disks-overview#images) // to learn more about managed images. CustomManagedImageResourceGroupName string `mapstructure:"custom_managed_image_resource_group_name" required:"false"` // Specify the source managed image's name to use. If this value is set, do // not set image\_publisher, image\_offer, image\_sku, or image\_version. // If this value is set, the value // `custom_managed_image_resource_group_name` must also be set. See // [documentation](https://docs.microsoft.com/en-us/azure/storage/storage-managed-disks-overview#images) // to learn more about managed images. CustomManagedImageName string `mapstructure:"custom_managed_image_name" required:"false"` customManagedImageID string Location string `mapstructure:"location"` // Size of the VM used for building. This can be changed when you deploy a // VM from your VHD. See // [pricing](https://azure.microsoft.com/en-us/pricing/details/virtual-machines/) // information. Defaults to `Standard_A1`. // // CLI example `az vm list-sizes --location westus` VMSize string `mapstructure:"vm_size" required:"false"` // Specify the managed image resource group name where the result of the // Packer build will be saved. The resource group must already exist. If // this value is set, the value managed_image_name must also be set. See // documentation to learn more about managed images. ManagedImageResourceGroupName string `mapstructure:"managed_image_resource_group_name"` // Specify the managed image name where the result of the Packer build will // be saved. The image name must not exist ahead of time, and will not be // overwritten. If this value is set, the value // managed_image_resource_group_name must also be set. See documentation to // learn more about managed images. ManagedImageName string `mapstructure:"managed_image_name"` // Specify the storage account // type for a managed image. Valid values are Standard_LRS and Premium_LRS. // The default is Standard_LRS. ManagedImageStorageAccountType string `mapstructure:"managed_image_storage_account_type" required:"false"` managedImageStorageAccountType compute.StorageAccountTypes // If // managed_image_os_disk_snapshot_name is set, a snapshot of the OS disk // is created with the same name as this value before the VM is captured. ManagedImageOSDiskSnapshotName string `mapstructure:"managed_image_os_disk_snapshot_name" required:"false"` // If // managed_image_data_disk_snapshot_prefix is set, snapshot of the data // disk(s) is created with the same prefix as this value before the VM is // captured. ManagedImageDataDiskSnapshotPrefix string `mapstructure:"managed_image_data_disk_snapshot_prefix" required:"false"` manageImageLocation string // Store the image in zone-resilient storage. You need to create it in a // region that supports [availability // zones](https://docs.microsoft.com/en-us/azure/availability-zones/az-overview). ManagedImageZoneResilient bool `mapstructure:"managed_image_zone_resilient" required:"false"` // the user can define up to 15 // tags. Tag names cannot exceed 512 characters, and tag values cannot exceed // 256 characters. Tags are applied to every resource deployed by a Packer // build, i.e. Resource Group, VM, NIC, VNET, Public IP, KeyVault, etc. AzureTags map[string]*string `mapstructure:"azure_tags" required:"false"` // Resource group under which the final artifact will be stored. ResourceGroupName string `mapstructure:"resource_group_name"` // Storage account under which the final artifact will be stored. StorageAccount string `mapstructure:"storage_account"` // temporary name assigned to the VM. If this // value is not set, a random value will be assigned. Knowing the resource // group and VM name allows one to execute commands to update the VM during a // Packer build, e.g. attach a resource disk to the VM. TempComputeName string `mapstructure:"temp_compute_name" required:"false"` // name assigned to the temporary resource group created during the build. // If this value is not set, a random value will be assigned. This resource // group is deleted at the end of the build. TempResourceGroupName string `mapstructure:"temp_resource_group_name"` // Specify an existing resource group to run the build in. BuildResourceGroupName string `mapstructure:"build_resource_group_name"` storageAccountBlobEndpoint string // This value allows you to // set a virtual_network_name and obtain a public IP. If this value is not // set and virtual_network_name is defined Packer is only allowed to be // executed from a host on the same subnet / virtual network. PrivateVirtualNetworkWithPublicIp bool `mapstructure:"private_virtual_network_with_public_ip" required:"false"` // Use a pre-existing virtual network for the // VM. This option enables private communication with the VM, no public IP // address is used or provisioned (unless you set // private_virtual_network_with_public_ip). VirtualNetworkName string `mapstructure:"virtual_network_name" required:"false"` // If virtual_network_name is set, // this value may also be set. If virtual_network_name is set, and this // value is not set the builder attempts to determine the subnet to use with // the virtual network. If the subnet cannot be found, or it cannot be // disambiguated, this value should be set. VirtualNetworkSubnetName string `mapstructure:"virtual_network_subnet_name" required:"false"` // If virtual_network_name is // set, this value may also be set. If virtual_network_name is set, and // this value is not set the builder attempts to determine the resource group // containing the virtual network. If the resource group cannot be found, or // it cannot be disambiguated, this value should be set. VirtualNetworkResourceGroupName string `mapstructure:"virtual_network_resource_group_name" required:"false"` // Specify a file containing custom data to inject into the cloud-init // process. The contents of the file are read and injected into the ARM // template. The custom data will be passed to cloud-init for processing at // the time of provisioning. See // [documentation](http://cloudinit.readthedocs.io/en/latest/topics/examples.html) // to learn more about custom data, and how it can be used to influence the // provisioning process. CustomDataFile string `mapstructure:"custom_data_file" required:"false"` customData string // Used for creating images from Marketplace images. Please refer to // [Deploy an image with Marketplace // terms](https://aka.ms/azuremarketplaceapideployment) for more details. // Not all Marketplace images support programmatic deployment, and support // is controlled by the image publisher. // // An example plan\_info object is defined below. // // ``` json // { // "plan_info": { // "plan_name": "rabbitmq", // "plan_product": "rabbitmq", // "plan_publisher": "bitnami" // } // } // ``` // // `plan_name` (string) - The plan name, required. `plan_product` (string) - // The plan product, required. `plan_publisher` (string) - The plan publisher, // required. `plan_promotion_code` (string) - Some images accept a promotion // code, optional. // // Images created from the Marketplace with `plan_info` **must** specify // `plan_info` whenever the image is deployed. The builder automatically adds // tags to the image to ensure this information is not lost. The following // tags are added. // // 1. PlanName // 2. PlanProduct // 3. PlanPublisher // 4. PlanPromotionCode // PlanInfo PlanInformation `mapstructure:"plan_info" required:"false"` // If either Linux or Windows is specified Packer will // automatically configure authentication credentials for the provisioned // machine. For Linux this configures an SSH authorized key. For Windows // this configures a WinRM certificate. OSType string `mapstructure:"os_type" required:"false"` // Specify the size of the OS disk in GB // (gigabytes). Values of zero or less than zero are ignored. OSDiskSizeGB int32 `mapstructure:"os_disk_size_gb" required:"false"` // The size(s) of any additional hard disks for the VM in gigabytes. If // this is not specified then the VM will only contain an OS disk. The // number of additional disks and maximum size of a disk depends on the // configuration of your VM. See // [Windows](https://docs.microsoft.com/en-us/azure/virtual-machines/windows/about-disks-and-vhds) // or // [Linux](https://docs.microsoft.com/en-us/azure/virtual-machines/linux/about-disks-and-vhds) // for more information. // // For VHD builds the final artifacts will be named // `PREFIX-dataDisk-.UUID.vhd` and stored in the specified capture // container along side the OS disk. The additional disks are included in // the deployment template `PREFIX-vmTemplate.UUID`. // // For Managed build the final artifacts are included in the managed image. // The additional disk will have the same storage account type as the OS // disk, as specified with the `managed_image_storage_account_type` // setting. AdditionalDiskSize []int32 `mapstructure:"disk_additional_size" required:"false"` // Specify the disk caching type. Valid values // are None, ReadOnly, and ReadWrite. The default value is ReadWrite. DiskCachingType string `mapstructure:"disk_caching_type" required:"false"` diskCachingType compute.CachingTypes // Specify the list of IP addresses and CIDR blocks that should be // allowed access to the VM. If provided, an Azure Network Security // Group will be created with corresponding rules and be bound to // the NIC attached to the VM. AllowedInboundIpAddresses []string `mapstructure:"allowed_inbound_ip_addresses"` // Runtime Values UserName string Password string tmpAdminPassword string tmpCertificatePassword string tmpResourceGroupName string tmpComputeName string tmpNicName string tmpPublicIPAddressName string tmpDeploymentName string tmpKeyVaultName string tmpOSDiskName string tmpSubnetName string tmpVirtualNetworkName string tmpWinRMCertificateUrl string // Authentication with the VM via SSH sshAuthorizedKey string // Authentication with the VM via WinRM winrmCertificate string Comm communicator.Config `mapstructure:",squash"` ctx interpolate.Context // If you want packer to delete the // temporary resource group asynchronously set this value. It's a boolean // value and defaults to false. Important Setting this true means that // your builds are faster, however any failed deletes are not reported. AsyncResourceGroupDelete bool `mapstructure:"async_resourcegroup_delete" required:"false"` } type keyVaultCertificate struct { Data string `json:"data"` DataType string `json:"dataType"` Password string `json:"password,omitempty"` } func (c *Config) toVMID() string { var resourceGroupName string if c.tmpResourceGroupName != "" { resourceGroupName = c.tmpResourceGroupName } else { resourceGroupName = c.BuildResourceGroupName } return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/virtualMachines/%s", c.ClientConfig.SubscriptionID, resourceGroupName, c.tmpComputeName) } func (c *Config) isManagedImage() bool { return c.ManagedImageName != "" } func (c *Config) toVirtualMachineCaptureParameters() *compute.VirtualMachineCaptureParameters { return &compute.VirtualMachineCaptureParameters{ DestinationContainerName: &c.CaptureContainerName, VhdPrefix: &c.CaptureNamePrefix, OverwriteVhds: to.BoolPtr(false), } } func (c *Config) toImageParameters() *compute.Image { return &compute.Image{ ImageProperties: &compute.ImageProperties{ SourceVirtualMachine: &compute.SubResource{ ID: to.StringPtr(c.toVMID()), }, StorageProfile: &compute.ImageStorageProfile{ ZoneResilient: to.BoolPtr(c.ManagedImageZoneResilient), }, }, Location: to.StringPtr(c.Location), Tags: c.AzureTags, } } func (c *Config) createCertificate() (string, error) { privateKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { err = fmt.Errorf("Failed to Generate Private Key: %s", err) return "", err } host := fmt.Sprintf("%s.cloudapp.net", c.tmpComputeName) notBefore := time.Now() notAfter := notBefore.Add(24 * time.Hour) serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) if err != nil { err = fmt.Errorf("Failed to Generate Serial Number: %v", err) return "", err } template := x509.Certificate{ SerialNumber: serialNumber, Issuer: pkix.Name{ CommonName: host, }, Subject: pkix.Name{ CommonName: host, }, NotBefore: notBefore, NotAfter: notAfter, KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, } derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) if err != nil { err = fmt.Errorf("Failed to Create Certificate: %s", err) return "", err } pfxBytes, err := pkcs12.Encode(derBytes, privateKey, c.tmpCertificatePassword) if err != nil { err = fmt.Errorf("Failed to encode certificate as PFX: %s", err) return "", err } keyVaultDescription := keyVaultCertificate{ Data: base64.StdEncoding.EncodeToString(pfxBytes), DataType: "pfx", Password: c.tmpCertificatePassword, } bytes, err := json.Marshal(keyVaultDescription) if err != nil { err = fmt.Errorf("Failed to marshal key vault description: %s", err) return "", err } return base64.StdEncoding.EncodeToString(bytes), nil } func newConfig(raws ...interface{}) (*Config, []string, error) { var c Config c.ctx.Funcs = azcommon.TemplateFuncs err := config.Decode(&c, &config.DecodeOpts{ Interpolate: true, InterpolateContext: &c.ctx, }, raws...) if err != nil { return nil, nil, err } provideDefaultValues(&c) setRuntimeValues(&c) setUserNamePassword(&c) err = c.ClientConfig.SetDefaultValues() if err != nil { return nil, nil, err } err = setCustomData(&c) if err != nil { return nil, nil, err } // NOTE: if the user did not specify a communicator, then default to both // SSH and WinRM. This is for backwards compatibility because the code did // not specifically force the user to set a communicator. if c.Comm.Type == "" || strings.EqualFold(c.Comm.Type, "ssh") { err = setSshValues(&c) if err != nil { return nil, nil, err } } if c.Comm.Type == "" || strings.EqualFold(c.Comm.Type, "winrm") { err = setWinRMCertificate(&c) if err != nil { return nil, nil, err } } var errs *packer.MultiError errs = packer.MultiErrorAppend(errs, c.Comm.Prepare(&c.ctx)...) assertRequiredParametersSet(&c, errs) assertTagProperties(&c, errs) if errs != nil && len(errs.Errors) > 0 { return nil, nil, errs } return &c, nil, nil } func setSshValues(c *Config) error { if c.Comm.SSHTimeout == 0 { c.Comm.SSHTimeout = 20 * time.Minute } if c.Comm.SSHPrivateKeyFile != "" { privateKeyBytes, err := c.Comm.ReadSSHPrivateKeyFile() if err != nil { return err } signer, err := ssh.ParsePrivateKey(privateKeyBytes) if err != nil { return err } publicKey := signer.PublicKey() c.sshAuthorizedKey = fmt.Sprintf("%s %s packer Azure Deployment%s", publicKey.Type(), base64.StdEncoding.EncodeToString(publicKey.Marshal()), time.Now().Format(time.RFC3339)) c.Comm.SSHPrivateKey = privateKeyBytes } else { sshKeyPair, err := NewOpenSshKeyPair() if err != nil { return err } c.sshAuthorizedKey = sshKeyPair.AuthorizedKey() c.Comm.SSHPrivateKey = sshKeyPair.PrivateKey() } return nil } func setWinRMCertificate(c *Config) error { c.Comm.WinRMTransportDecorator = func() winrm.Transporter { return &winrm.ClientNTLM{} } cert, err := c.createCertificate() c.winrmCertificate = cert return err } func setRuntimeValues(c *Config) { var tempName = NewTempName() c.tmpAdminPassword = tempName.AdminPassword // store so that we can access this later during provisioning commonhelper.SetSharedState("winrm_password", c.tmpAdminPassword, c.PackerConfig.PackerBuildName) packer.LogSecretFilter.Set(c.tmpAdminPassword) c.tmpCertificatePassword = tempName.CertificatePassword if c.TempComputeName == "" { c.tmpComputeName = tempName.ComputeName } else { c.tmpComputeName = c.TempComputeName } c.tmpDeploymentName = tempName.DeploymentName // Only set tmpResourceGroupName if no name has been specified if c.TempResourceGroupName == "" && c.BuildResourceGroupName == "" { c.tmpResourceGroupName = tempName.ResourceGroupName } else if c.TempResourceGroupName != "" && c.BuildResourceGroupName == "" { c.tmpResourceGroupName = c.TempResourceGroupName } c.tmpNicName = tempName.NicName c.tmpPublicIPAddressName = tempName.PublicIPAddressName c.tmpOSDiskName = tempName.OSDiskName c.tmpSubnetName = tempName.SubnetName c.tmpVirtualNetworkName = tempName.VirtualNetworkName c.tmpKeyVaultName = tempName.KeyVaultName } func setUserNamePassword(c *Config) { if c.Comm.SSHUsername == "" { c.Comm.SSHUsername = DefaultUserName } c.UserName = c.Comm.SSHUsername if c.Comm.SSHPassword != "" { c.Password = c.Comm.SSHPassword } else { c.Password = c.tmpAdminPassword } } func setCustomData(c *Config) error { if c.CustomDataFile == "" { return nil } b, err := ioutil.ReadFile(c.CustomDataFile) if err != nil { return err } c.customData = base64.StdEncoding.EncodeToString(b) return nil } func provideDefaultValues(c *Config) { if c.VMSize == "" { c.VMSize = DefaultVMSize } if c.ManagedImageStorageAccountType == "" { c.managedImageStorageAccountType = compute.StorageAccountTypesStandardLRS } if c.DiskCachingType == "" { c.diskCachingType = compute.CachingTypesReadWrite } if c.ImagePublisher != "" && c.ImageVersion == "" { c.ImageVersion = DefaultImageVersion } c.ClientConfig.SetDefaultValues() } func assertTagProperties(c *Config, errs *packer.MultiError) { if len(c.AzureTags) > 15 { errs = packer.MultiErrorAppend(errs, fmt.Errorf("a max of 15 tags are supported, but %d were provided", len(c.AzureTags))) } for k, v := range c.AzureTags { if len(k) > 512 { errs = packer.MultiErrorAppend(errs, fmt.Errorf("the tag name %q exceeds (%d) the 512 character limit", k, len(k))) } if len(*v) > 256 { errs = packer.MultiErrorAppend(errs, fmt.Errorf("the tag name %q exceeds (%d) the 256 character limit", *v, len(*v))) } } } func assertRequiredParametersSet(c *Config, errs *packer.MultiError) { c.ClientConfig.Validate(errs) ///////////////////////////////////////////// // Capture if c.CaptureContainerName == "" && c.ManagedImageName == "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_container_name or managed_image_name must be specified")) } if c.CaptureNamePrefix == "" && c.ManagedImageResourceGroupName == "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_name_prefix or managed_image_resource_group_name must be specified")) } if (c.CaptureNamePrefix != "" || c.CaptureContainerName != "") && (c.ManagedImageResourceGroupName != "" || c.ManagedImageName != "") { errs = packer.MultiErrorAppend(errs, fmt.Errorf("Either a VHD or a managed image can be built, but not both. Please specify either capture_container_name and capture_name_prefix or managed_image_resource_group_name and managed_image_name.")) } if c.CaptureContainerName != "" { if !reCaptureContainerName.MatchString(c.CaptureContainerName) { errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_container_name must satisfy the regular expression %q.", reCaptureContainerName.String())) } if strings.HasSuffix(c.CaptureContainerName, "-") { errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_container_name must not end with a hyphen, e.g. '-'.")) } if strings.Contains(c.CaptureContainerName, "--") { errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_container_name must not contain consecutive hyphens, e.g. '--'.")) } if c.CaptureNamePrefix == "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_name_prefix must be specified")) } if !reCaptureNamePrefix.MatchString(c.CaptureNamePrefix) { errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_name_prefix must satisfy the regular expression %q.", reCaptureNamePrefix.String())) } if strings.HasSuffix(c.CaptureNamePrefix, "-") || strings.HasSuffix(c.CaptureNamePrefix, ".") { errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_name_prefix must not end with a hyphen or period.")) } } if c.TempResourceGroupName != "" && c.BuildResourceGroupName != "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("The settings temp_resource_group_name and build_resource_group_name cannot both be defined. Please define one or neither.")) } ///////////////////////////////////////////// // Compute toInt := func(b bool) int { if b { return 1 } else { return 0 } } isImageUrl := c.ImageUrl != "" isCustomManagedImage := c.CustomManagedImageName != "" || c.CustomManagedImageResourceGroupName != "" isSharedGallery := c.SharedGallery.GalleryName != "" isPlatformImage := c.ImagePublisher != "" || c.ImageOffer != "" || c.ImageSku != "" countSourceInputs := toInt(isImageUrl) + toInt(isCustomManagedImage) + toInt(isPlatformImage) + toInt(isSharedGallery) if countSourceInputs > 1 { errs = packer.MultiErrorAppend(errs, fmt.Errorf("Specify either a VHD (image_url), Image Reference (image_publisher, image_offer, image_sku), a Managed Disk (custom_managed_disk_image_name, custom_managed_disk_resource_group_name), or a Shared Gallery Image (shared_image_gallery)")) } if isImageUrl && c.ManagedImageResourceGroupName != "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("A managed image must be created from a managed image, it cannot be created from a VHD.")) } if c.SharedGallery.GalleryName != "" { if c.SharedGallery.Subscription == "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("A shared_image_gallery.subscription must be specified")) } if c.SharedGallery.ResourceGroup == "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("A shared_image_gallery.resource_group must be specified")) } if c.SharedGallery.ImageName == "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("A shared_image_gallery.image_name must be specified")) } if c.CaptureContainerName != "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("VHD Target [capture_container_name] is not supported when using Shared Image Gallery as source. Use managed_image_resource_group_name instead.")) } if c.CaptureNamePrefix != "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("VHD Target [capture_name_prefix] is not supported when using Shared Image Gallery as source. Use managed_image_name instead.")) } } else if c.ImageUrl == "" && c.CustomManagedImageName == "" { if c.ImagePublisher == "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("An image_publisher must be specified")) } if c.ImageOffer == "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("An image_offer must be specified")) } if c.ImageSku == "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("An image_sku must be specified")) } } else if c.ImageUrl == "" && c.ImagePublisher == "" { if c.CustomManagedImageResourceGroupName == "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("A custom_managed_image_resource_group_name must be specified")) } if c.CustomManagedImageName == "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("A custom_managed_image_name must be specified")) } if c.ManagedImageResourceGroupName == "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("A managed_image_resource_group_name must be specified")) } if c.ManagedImageName == "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("A managed_image_name must be specified")) } } else { if c.ImagePublisher != "" || c.ImageOffer != "" || c.ImageSku != "" || c.ImageVersion != "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("An image_url must not be specified if image_publisher, image_offer, image_sku, or image_version is specified")) } } ///////////////////////////////////////////// // Deployment xor := func(a, b bool) bool { return (a || b) && !(a && b) } if !xor((c.StorageAccount != "" || c.ResourceGroupName != ""), (c.ManagedImageName != "" || c.ManagedImageResourceGroupName != "")) { errs = packer.MultiErrorAppend(errs, fmt.Errorf("Specify either a VHD (storage_account and resource_group_name) or Managed Image (managed_image_resource_group_name and managed_image_name) output")) } if !xor(c.Location != "", c.BuildResourceGroupName != "") { errs = packer.MultiErrorAppend(errs, fmt.Errorf("Specify either a location to create the resource group in or an existing build_resource_group_name, but not both.")) } if c.ManagedImageName == "" && c.ManagedImageResourceGroupName == "" { if c.StorageAccount == "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("A storage_account must be specified")) } if c.ResourceGroupName == "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("A resource_group_name must be specified")) } } if c.TempResourceGroupName != "" { if ok, err := assertResourceGroupName(c.TempResourceGroupName, "temp_resource_group_name"); !ok { errs = packer.MultiErrorAppend(errs, err) } } if c.BuildResourceGroupName != "" { if ok, err := assertResourceGroupName(c.BuildResourceGroupName, "build_resource_group_name"); !ok { errs = packer.MultiErrorAppend(errs, err) } } if c.ManagedImageResourceGroupName != "" { if ok, err := assertResourceGroupName(c.ManagedImageResourceGroupName, "managed_image_resource_group_name"); !ok { errs = packer.MultiErrorAppend(errs, err) } } if c.ManagedImageName != "" { if ok, err := assertManagedImageName(c.ManagedImageName, "managed_image_name"); !ok { errs = packer.MultiErrorAppend(errs, err) } } if c.ManagedImageName != "" && c.ManagedImageResourceGroupName != "" && c.SharedGalleryDestination.SigDestinationGalleryName != "" { if c.SharedGalleryDestination.SigDestinationResourceGroup == "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("A resource_group must be specified for shared_image_gallery_destination")) } if c.SharedGalleryDestination.SigDestinationImageName == "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("An image_name must be specified for shared_image_gallery_destination")) } if c.SharedGalleryDestination.SigDestinationImageVersion == "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("An image_version must be specified for shared_image_gallery_destination")) } if len(c.SharedGalleryDestination.SigDestinationReplicationRegions) == 0 { errs = packer.MultiErrorAppend(errs, fmt.Errorf("A list of replication_regions must be specified for shared_image_gallery_destination")) } } if c.SharedGalleryTimeout == 0 { // default to a one-hour timeout. In the sdk, the default is 15 m. c.SharedGalleryTimeout = 60 * time.Minute } if c.ManagedImageOSDiskSnapshotName != "" { if ok, err := assertManagedImageOSDiskSnapshotName(c.ManagedImageOSDiskSnapshotName, "managed_image_os_disk_snapshot_name"); !ok { errs = packer.MultiErrorAppend(errs, err) } } if c.ManagedImageDataDiskSnapshotPrefix != "" { if ok, err := assertManagedImageDataDiskSnapshotName(c.ManagedImageDataDiskSnapshotPrefix, "managed_image_data_disk_snapshot_prefix"); !ok { errs = packer.MultiErrorAppend(errs, err) } } if c.VirtualNetworkName == "" && c.VirtualNetworkResourceGroupName != "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("If virtual_network_resource_group_name is specified, so must virtual_network_name")) } if c.VirtualNetworkName == "" && c.VirtualNetworkSubnetName != "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("If virtual_network_subnet_name is specified, so must virtual_network_name")) } if c.AllowedInboundIpAddresses != nil && len(c.AllowedInboundIpAddresses) >= 1 { if c.VirtualNetworkName != "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("If virtual_network_name is specified, allowed_inbound_ip_addresses cannot be specified")) } else { if ok, err := assertAllowedInboundIpAddresses(c.AllowedInboundIpAddresses, "allowed_inbound_ip_addresses"); !ok { errs = packer.MultiErrorAppend(errs, err) } } } ///////////////////////////////////////////// // Plan Info if c.PlanInfo.PlanName != "" || c.PlanInfo.PlanProduct != "" || c.PlanInfo.PlanPublisher != "" || c.PlanInfo.PlanPromotionCode != "" { if c.PlanInfo.PlanName == "" || c.PlanInfo.PlanProduct == "" || c.PlanInfo.PlanPublisher == "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("if either plan_name, plan_product, plan_publisher, or plan_promotion_code are defined then plan_name, plan_product, and plan_publisher must be defined")) } else { if c.AzureTags == nil { c.AzureTags = make(map[string]*string) } c.AzureTags["PlanInfo"] = &c.PlanInfo.PlanName c.AzureTags["PlanProduct"] = &c.PlanInfo.PlanProduct c.AzureTags["PlanPublisher"] = &c.PlanInfo.PlanPublisher c.AzureTags["PlanPromotionCode"] = &c.PlanInfo.PlanPromotionCode } } ///////////////////////////////////////////// // OS if strings.EqualFold(c.OSType, constants.Target_Linux) { c.OSType = constants.Target_Linux } else if strings.EqualFold(c.OSType, constants.Target_Windows) { c.OSType = constants.Target_Windows } else if c.OSType == "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("An os_type must be specified")) } else { errs = packer.MultiErrorAppend(errs, fmt.Errorf("The os_type %q is invalid", c.OSType)) } switch c.ManagedImageStorageAccountType { case "", string(compute.StorageAccountTypesStandardLRS): c.managedImageStorageAccountType = compute.StorageAccountTypesStandardLRS case string(compute.StorageAccountTypesPremiumLRS): c.managedImageStorageAccountType = compute.StorageAccountTypesPremiumLRS default: errs = packer.MultiErrorAppend(errs, fmt.Errorf("The managed_image_storage_account_type %q is invalid", c.ManagedImageStorageAccountType)) } switch c.DiskCachingType { case string(compute.CachingTypesNone): c.diskCachingType = compute.CachingTypesNone case string(compute.CachingTypesReadOnly): c.diskCachingType = compute.CachingTypesReadOnly case "", string(compute.CachingTypesReadWrite): c.diskCachingType = compute.CachingTypesReadWrite default: errs = packer.MultiErrorAppend(errs, fmt.Errorf("The disk_caching_type %q is invalid", c.DiskCachingType)) } } func assertManagedImageName(name, setting string) (bool, error) { if !isValidAzureName(reManagedDiskName, name) { return false, fmt.Errorf("The setting %s must match the regular expression %q, and not end with a '-' or '.'.", setting, validManagedDiskName) } return true, nil } func assertManagedImageOSDiskSnapshotName(name, setting string) (bool, error) { if !isValidAzureName(reSnapshotName, name) { return false, fmt.Errorf("The setting %s must only contain characters from a-z, A-Z, 0-9 and _ and the maximum length is 80 characters", setting) } return true, nil } func assertManagedImageDataDiskSnapshotName(name, setting string) (bool, error) { if !isValidAzureName(reSnapshotPrefix, name) { return false, fmt.Errorf("The setting %s must only contain characters from a-z, A-Z, 0-9 and _ and the maximum length (excluding the prefix) is 60 characters", setting) } return true, nil } func assertAllowedInboundIpAddresses(ipAddresses []string, setting string) (bool, error) { for _, ipAddress := range ipAddresses { if net.ParseIP(ipAddress) == nil { if _, _, err := net.ParseCIDR(ipAddress); err != nil { return false, fmt.Errorf("The setting %s must only contain valid IP addresses or CIDR blocks", setting) } } } return true, nil } func assertResourceGroupName(rgn, setting string) (bool, error) { if !isValidAzureName(reResourceGroupName, rgn) { return false, fmt.Errorf("The setting %s must match the regular expression %q, and not end with a '-' or '.'.", setting, validResourceGroupNameRe) } return true, nil } func isValidAzureName(re *regexp.Regexp, rgn string) bool { return re.Match([]byte(rgn)) && !strings.HasSuffix(rgn, ".") && !strings.HasSuffix(rgn, "-") } func (c *Config) validateLocationZoneResiliency(say func(s string)) { // Docs on regions that support Availibility Zones: // https://docs.microsoft.com/en-us/azure/availability-zones/az-overview#regions-that-support-availability-zones // Query technical names for locations: // az account list-locations --query '[].name' -o tsv var zones = make(map[string]struct{}) zones["westeurope"] = struct{}{} zones["centralus"] = struct{}{} zones["eastus2"] = struct{}{} zones["francecentral"] = struct{}{} zones["northeurope"] = struct{}{} zones["southeastasia"] = struct{}{} zones["westus2"] = struct{}{} if _, ok := zones[c.Location]; !ok { say(fmt.Sprintf("WARNING: Zone resiliency may not be supported in %s, checkout the docs at https://docs.microsoft.com/en-us/azure/availability-zones/", c.Location)) } }