diff --git a/.gitignore b/.gitignore index 8dd382f2a..3999641cd 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ .idea test/.env *~ +*.received.* website/.bundle website/vendor diff --git a/builder/azure/arm/TestVirtualMachineDeployment05.approved.txt b/builder/azure/arm/TestVirtualMachineDeployment05.approved.txt new file mode 100644 index 000000000..906820f27 --- /dev/null +++ b/builder/azure/arm/TestVirtualMachineDeployment05.approved.txt @@ -0,0 +1,120 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json", + "contentVersion": "1.0.0.0", + "parameters": { + "adminPassword": { + "type": "string" + }, + "adminUsername": { + "type": "string" + }, + "dnsNameForPublicIP": { + "type": "string" + }, + "osDiskName": { + "type": "string" + }, + "storageAccountBlobEndpoint": { + "type": "string" + }, + "vmName": { + "type": "string" + }, + "vmSize": { + "type": "string" + } + }, + "resources": [ + { + "apiVersion": "[variables('apiVersion')]", + "dependsOn": [], + "location": "[variables('location')]", + "name": "[variables('nicName')]", + "properties": { + "ipConfigurations": [ + { + "name": "ipconfig", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "subnet": { + "id": "[variables('subnetRef')]" + } + } + } + ] + }, + "type": "Microsoft.Network/networkInterfaces" + }, + { + "apiVersion": "[variables('apiVersion')]", + "dependsOn": [ + "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]" + ], + "location": "[variables('location')]", + "name": "[parameters('vmName')]", + "properties": { + "diagnosticsProfile": { + "bootDiagnostics": { + "enabled": false + } + }, + "hardwareProfile": { + "vmSize": "[parameters('vmSize')]" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]" + } + ] + }, + "osProfile": { + "adminPassword": "[parameters('adminPassword')]", + "adminUsername": "[parameters('adminUsername')]", + "computerName": "[parameters('vmName')]", + "linuxConfiguration": { + "ssh": { + "publicKeys": [ + { + "keyData": "", + "path": "[variables('sshKeyPath')]" + } + ] + } + } + }, + "storageProfile": { + "osDisk": { + "caching": "ReadWrite", + "createOption": "FromImage", + "image": { + "uri": "https://localhost/custom.vhd" + }, + "name": "osdisk", + "osType": "Linux", + "vhd": { + "uri": "[concat(parameters('storageAccountBlobEndpoint'),variables('vmStorageAccountContainerName'),'/', parameters('osDiskName'),'.vhd')]" + } + } + } + }, + "type": "Microsoft.Compute/virtualMachines" + } + ], + "variables": { + "addressPrefix": "10.0.0.0/16", + "apiVersion": "2015-06-15", + "location": "[resourceGroup().location]", + "nicName": "packerNic", + "publicIPAddressName": "packerPublicIP", + "publicIPAddressType": "Dynamic", + "sshKeyPath": "[concat('/home/',parameters('adminUsername'),'/.ssh/authorized_keys')]", + "subnetAddressPrefix": "10.0.0.0/24", + "subnetName": "ignore", + "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", + "virtualNetworkName": "ignore", + "virtualNetworkResourceGroup": "ignore", + "vmStorageAccountContainerName": "images", + "vnetID": "[resourceId(variables('virtualNetworkResourceGroup'), 'Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" + } +} \ No newline at end of file diff --git a/builder/azure/arm/azure_client.go b/builder/azure/arm/azure_client.go index 2f6e1e39a..096979b04 100644 --- a/builder/azure/arm/azure_client.go +++ b/builder/azure/arm/azure_client.go @@ -36,6 +36,9 @@ type AzureClient struct { resources.DeploymentsClient resources.GroupsClient network.PublicIPAddressesClient + network.InterfacesClient + network.SubnetsClient + network.VirtualNetworksClient compute.VirtualMachinesClient common.VaultClient armStorage.AccountsClient @@ -122,6 +125,24 @@ func NewAzureClient(subscriptionID, resourceGroupName, storageAccountName string azureClient.GroupsClient.ResponseInspector = byInspecting(maxlen) azureClient.GroupsClient.UserAgent += packerUserAgent + azureClient.InterfacesClient = network.NewInterfacesClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID) + azureClient.InterfacesClient.Authorizer = servicePrincipalToken + azureClient.InterfacesClient.RequestInspector = withInspection(maxlen) + azureClient.InterfacesClient.ResponseInspector = byInspecting(maxlen) + azureClient.InterfacesClient.UserAgent += packerUserAgent + + azureClient.SubnetsClient = network.NewSubnetsClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID) + azureClient.SubnetsClient.Authorizer = servicePrincipalToken + azureClient.SubnetsClient.RequestInspector = withInspection(maxlen) + azureClient.SubnetsClient.ResponseInspector = byInspecting(maxlen) + azureClient.SubnetsClient.UserAgent += packerUserAgent + + azureClient.VirtualNetworksClient = network.NewVirtualNetworksClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID) + azureClient.VirtualNetworksClient.Authorizer = servicePrincipalToken + azureClient.VirtualNetworksClient.RequestInspector = withInspection(maxlen) + azureClient.VirtualNetworksClient.ResponseInspector = byInspecting(maxlen) + azureClient.VirtualNetworksClient.UserAgent += packerUserAgent + azureClient.PublicIPAddressesClient = network.NewPublicIPAddressesClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID) azureClient.PublicIPAddressesClient.Authorizer = servicePrincipalToken azureClient.PublicIPAddressesClient.RequestInspector = withInspection(maxlen) diff --git a/builder/azure/arm/builder.go b/builder/azure/arm/builder.go index 6785e3a63..0d85d3fc8 100644 --- a/builder/azure/arm/builder.go +++ b/builder/azure/arm/builder.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "log" + "strings" "time" packerAzureCommon "github.com/mitchellh/packer/builder/azure/common" @@ -20,7 +21,6 @@ import ( packerCommon "github.com/mitchellh/packer/common" "github.com/mitchellh/packer/helper/communicator" "github.com/mitchellh/packer/packer" - "strings" ) type Builder struct { @@ -30,6 +30,7 @@ type Builder struct { } const ( + DefaultNicName = "packerNic" DefaultPublicIPAddressName = "packerPublicIP" DefaultSasBlobContainer = "system/Microsoft.Compute" DefaultSasBlobPermission = "r" @@ -82,11 +83,21 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe return nil, err } + resolver := newResourceResolver(azureClient) + if err := resolver.Resolve(b.config); err != nil { + return nil, err + } + b.config.storageAccountBlobEndpoint, err = b.getBlobEndpoint(azureClient, b.config.ResourceGroupName, b.config.StorageAccount) if err != nil { return nil, err } + endpointConnectType := PublicEndpoint + if b.isPrivateNetworkCommunication() { + endpointConnectType = PrivateEndpoint + } + b.setTemplateParameters(b.stateBag) var steps []multistep.Step @@ -95,7 +106,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe NewStepCreateResourceGroup(azureClient, ui), NewStepValidateTemplate(azureClient, ui, b.config, GetVirtualMachineDeployment), NewStepDeployTemplate(azureClient, ui, b.config, GetVirtualMachineDeployment), - NewStepGetIPAddress(azureClient, ui), + NewStepGetIPAddress(azureClient, ui, endpointConnectType), &communicator.StepConnectSSH{ Config: &b.config.Comm, Host: lin.SSHHost, @@ -117,7 +128,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe NewStepSetCertificate(b.config, ui), NewStepValidateTemplate(azureClient, ui, b.config, GetVirtualMachineDeployment), NewStepDeployTemplate(azureClient, ui, b.config, GetVirtualMachineDeployment), - NewStepGetIPAddress(azureClient, ui), + NewStepGetIPAddress(azureClient, ui, endpointConnectType), &communicator.StepConnectWinRM{ Config: &b.config.Comm, Host: func(stateBag multistep.StateBag) (string, error) { @@ -176,6 +187,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe return &Artifact{}, nil } +func (b *Builder) isPrivateNetworkCommunication() bool { + return b.config.VirtualNetworkName != "" +} + func (b *Builder) Cancel() { if b.runner != nil { log.Println("Cancelling the step runner...") @@ -213,6 +228,7 @@ func (b *Builder) configureStateBag(stateBag multistep.StateBag) { stateBag.Put(constants.ArmDeploymentName, b.config.tmpDeploymentName) stateBag.Put(constants.ArmKeyVaultName, b.config.tmpKeyVaultName) stateBag.Put(constants.ArmLocation, b.config.Location) + stateBag.Put(constants.ArmNicName, DefaultNicName) stateBag.Put(constants.ArmPublicIPAddressName, DefaultPublicIPAddressName) stateBag.Put(constants.ArmResourceGroupName, b.config.tmpResourceGroupName) stateBag.Put(constants.ArmStorageAccountName, b.config.StorageAccount) diff --git a/builder/azure/arm/builder_test.go b/builder/azure/arm/builder_test.go index ecb2ea75f..9f0d26670 100644 --- a/builder/azure/arm/builder_test.go +++ b/builder/azure/arm/builder_test.go @@ -22,6 +22,7 @@ func TestStateBagShouldBePopulatedExpectedValues(t *testing.T) { constants.ArmComputeName, constants.ArmDeploymentName, constants.ArmLocation, + constants.ArmNicName, constants.ArmResourceGroupName, constants.ArmStorageAccountName, constants.ArmVirtualMachineCaptureParameters, diff --git a/builder/azure/arm/config.go b/builder/azure/arm/config.go index 355ee8e47..93b7f7b59 100644 --- a/builder/azure/arm/config.go +++ b/builder/azure/arm/config.go @@ -70,11 +70,14 @@ type Config struct { VMSize string `mapstructure:"vm_size"` // Deployment - ResourceGroupName string `mapstructure:"resource_group_name"` - StorageAccount string `mapstructure:"storage_account"` - storageAccountBlobEndpoint string - CloudEnvironmentName string `mapstructure:"cloud_environment_name"` - cloudEnvironment *azure.Environment + ResourceGroupName string `mapstructure:"resource_group_name"` + StorageAccount string `mapstructure:"storage_account"` + storageAccountBlobEndpoint string + CloudEnvironmentName string `mapstructure:"cloud_environment_name"` + cloudEnvironment *azure.Environment + VirtualNetworkName string `mapstructure:"virtual_network_name"` + VirtualNetworkSubnetName string `mapstructure:"virtual_network_subnet_name"` + VirtualNetworkResourceGroupName string `mapstructure:"virtual_network_resource_group_name"` // OS OSType string `mapstructure:"os_type"` @@ -447,6 +450,12 @@ func assertRequiredParametersSet(c *Config, errs *packer.MultiError) { if c.ResourceGroupName == "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("A resource_group_name must be specified")) } + 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")) + } ///////////////////////////////////////////// // OS diff --git a/builder/azure/arm/config_retriever.go b/builder/azure/arm/config_retriever.go index 428b159ea..b31340f46 100644 --- a/builder/azure/arm/config_retriever.go +++ b/builder/azure/arm/config_retriever.go @@ -1,5 +1,12 @@ package arm +// Method to resolve information about the user so that a client can be +// constructed to communicated with Azure. +// +// The following data are resolved. +// +// 1. TenantID + import ( "github.com/Azure/go-autorest/autorest/azure" "github.com/mitchellh/packer/builder/azure/common" @@ -11,7 +18,9 @@ type configRetriever struct { } func newConfigRetriever() configRetriever { - return configRetriever{common.FindTenantID} + return configRetriever{ + common.FindTenantID, + } } func (cr configRetriever) FillParameters(c *Config) error { @@ -22,5 +31,6 @@ func (cr configRetriever) FillParameters(c *Config) error { } c.TenantID = tenantID } + return nil } diff --git a/builder/azure/arm/config_test.go b/builder/azure/arm/config_test.go index ae66cc09b..9e5a89245 100644 --- a/builder/azure/arm/config_test.go +++ b/builder/azure/arm/config_test.go @@ -142,6 +142,78 @@ func TestConfigShouldRejectCustomImageAndMarketPlace(t *testing.T) { } } +func TestConfigVirtualNetworkNameIsOptional(t *testing.T) { + config := map[string]string{ + "capture_name_prefix": "ignore", + "capture_container_name": "ignore", + "location": "ignore", + "image_url": "ignore", + "storage_account": "ignore", + "resource_group_name": "ignore", + "subscription_id": "ignore", + "os_type": constants.Target_Linux, + "communicator": "none", + "virtual_network_name": "MyVirtualNetwork", + } + + c, _, _ := newConfig(config, getPackerConfiguration()) + if c.VirtualNetworkName != "MyVirtualNetwork" { + t.Errorf("Expected Config to set virtual_network_name to MyVirtualNetwork, but got %q", c.VirtualNetworkName) + } + if c.VirtualNetworkResourceGroupName != "" { + t.Errorf("Expected Config to leave virtual_network_resource_group_name to '', but got %q", c.VirtualNetworkResourceGroupName) + } + if c.VirtualNetworkSubnetName != "" { + t.Errorf("Expected Config to leave virtual_network_subnet_name to '', but got %q", c.VirtualNetworkSubnetName) + } +} + +// The user can pass the value virtual_network_resource_group_name to avoid the lookup of +// a virtual network's resource group, or to help with disambiguation. The value should +// only be set if virtual_network_name was set. +func TestConfigVirtualNetworkResourceGroupNameMustBeSetWithVirtualNetworkName(t *testing.T) { + config := map[string]string{ + "capture_name_prefix": "ignore", + "capture_container_name": "ignore", + "location": "ignore", + "image_url": "ignore", + "storage_account": "ignore", + "resource_group_name": "ignore", + "subscription_id": "ignore", + "os_type": constants.Target_Linux, + "communicator": "none", + "virtual_network_resource_group_name": "MyVirtualNetworkRG", + } + + _, _, err := newConfig(config, getPackerConfiguration()) + if err == nil { + t.Error("Expected Config to reject virtual_network_resource_group_name, if virtual_network_name is not set.") + } +} + +// The user can pass the value virtual_network_subnet_name to avoid the lookup of +// a virtual network subnet's name, or to help with disambiguation. The value should +// only be set if virtual_network_name was set. +func TestConfigVirtualNetworkSubnetNameMustBeSetWithVirtualNetworkName(t *testing.T) { + config := map[string]string{ + "capture_name_prefix": "ignore", + "capture_container_name": "ignore", + "location": "ignore", + "image_url": "ignore", + "storage_account": "ignore", + "resource_group_name": "ignore", + "subscription_id": "ignore", + "os_type": constants.Target_Linux, + "communicator": "none", + "virtual_network_subnet_name": "MyVirtualNetworkRG", + } + + _, _, err := newConfig(config, getPackerConfiguration()) + if err == nil { + t.Error("Expected Config to reject virtual_network_subnet_name, if virtual_network_name is not set.") + } +} + func TestConfigShouldDefaultToPublicCloud(t *testing.T) { c, _, _ := newConfig(getArmBuilderConfiguration(), getPackerConfiguration()) diff --git a/builder/azure/arm/resource_resolver.go b/builder/azure/arm/resource_resolver.go new file mode 100644 index 000000000..0dc278b25 --- /dev/null +++ b/builder/azure/arm/resource_resolver.go @@ -0,0 +1,101 @@ +package arm + +// Code to resolve resources that are required by the API. These resources +// can most likely be resolved without asking the user, thereby reducing the +// amount of configuration they need to provide. +// +// Resource resolver differs from config retriever because resource resolver +// requires a client to communicate with the Azure API. A config retriever is +// used to determine values without use of a client. + +import ( + "fmt" + "strings" +) + +type resourceResolver struct { + client *AzureClient + findVirtualNetworkResourceGroup func(*AzureClient, string) (string, error) + findVirtualNetworkSubnet func(*AzureClient, string, string) (string, error) +} + +func newResourceResolver(client *AzureClient) *resourceResolver { + return &resourceResolver{ + client: client, + findVirtualNetworkResourceGroup: findVirtualNetworkResourceGroup, + findVirtualNetworkSubnet: findVirtualNetworkSubnet, + } +} + +func (s *resourceResolver) Resolve(c *Config) error { + if s.shouldResolveResourceGroup(c) { + resourceGroupName, err := s.findVirtualNetworkResourceGroup(s.client, c.VirtualNetworkName) + if err != nil { + return err + } + + subnetName, err := s.findVirtualNetworkSubnet(s.client, resourceGroupName, c.VirtualNetworkName) + if err != nil { + return err + } + + c.VirtualNetworkResourceGroupName = resourceGroupName + c.VirtualNetworkSubnetName = subnetName + } + + return nil +} + +func (s *resourceResolver) shouldResolveResourceGroup(c *Config) bool { + return c.VirtualNetworkName != "" && c.VirtualNetworkResourceGroupName == "" +} + +func getResourceGroupNameFromId(id string) string { + // "/subscriptions/3f499422-dd76-4114-8859-86d526c9deb6/resourceGroups/packer-Resource-Group-yylnwsl30j/providers/... + xs := strings.Split(id, "/") + return xs[4] +} + +func findVirtualNetworkResourceGroup(client *AzureClient, name string) (string, error) { + virtualNetworks, err := client.VirtualNetworksClient.ListAll() + if err != nil { + return "", err + } + + resourceGroupNames := make([]string, 0) + + for _, virtualNetwork := range *virtualNetworks.Value { + if strings.EqualFold(name, *virtualNetwork.Name) { + rgn := getResourceGroupNameFromId(*virtualNetwork.ID) + resourceGroupNames = append(resourceGroupNames, rgn) + } + } + + if len(resourceGroupNames) == 0 { + return "", fmt.Errorf("Cannot find a resource group with a virtual network called %q", name) + } + + if len(resourceGroupNames) > 1 { + return "", fmt.Errorf("Found multiple resource groups with a virtual network called %q, please use virtual_network_resource_group_name to disambiguate", name) + } + + return resourceGroupNames[0], nil +} + +func findVirtualNetworkSubnet(client *AzureClient, resourceGroupName string, name string) (string, error) { + subnets, err := client.SubnetsClient.List(resourceGroupName, name) + if err != nil { + return "", err + } + + if len(*subnets.Value) == 0 { + return "", fmt.Errorf("Cannot find a subnet in the resource group %q associated with the virtual network called %q", resourceGroupName, name) + } + + if len(*subnets.Value) > 1 { + return "", fmt.Errorf("Found multiple subnets in the resource group %q associated with the virtual network called %q, please use virtual_network_subnet_name to disambiguate", resourceGroupName, name) + } + + subnet := (*subnets.Value)[0] + return *subnet.Name, nil +} diff --git a/builder/azure/arm/resource_resolver_test.go b/builder/azure/arm/resource_resolver_test.go new file mode 100644 index 000000000..f8e339687 --- /dev/null +++ b/builder/azure/arm/resource_resolver_test.go @@ -0,0 +1,76 @@ +package arm + +import ( + "testing" +) + +func TestResourceResolverIgnoresEmptyVirtualNetworkName(t *testing.T) { + c, _, _ := newConfig(getArmBuilderConfiguration(), getPackerConfiguration()) + if c.VirtualNetworkName != "" { + t.Fatalf("Expected VirtualNetworkName to be empty by default") + } + + sut := newTestResourceResolver() + sut.findVirtualNetworkResourceGroup = nil // assert that this is not even called + sut.Resolve(c) + + if c.VirtualNetworkName != "" { + t.Fatalf("Expected VirtualNetworkName to be empty") + } + if c.VirtualNetworkResourceGroupName != "" { + t.Fatalf("Expected VirtualNetworkResourceGroupName to be empty") + } +} + +// If the user fully specified the virtual network name and resource group then +// there is no need to do a lookup. +func TestResourceResolverIgnoresSetVirtualNetwork(t *testing.T) { + c, _, _ := newConfig(getArmBuilderConfiguration(), getPackerConfiguration()) + c.VirtualNetworkName = "--virtual-network-name--" + c.VirtualNetworkResourceGroupName = "--virtual-network-resource-group-name--" + c.VirtualNetworkSubnetName = "--virtual-network-subnet-name--" + + sut := newTestResourceResolver() + sut.findVirtualNetworkResourceGroup = nil // assert that this is not even called + sut.findVirtualNetworkSubnet = nil // assert that this is not even called + sut.Resolve(c) + + if c.VirtualNetworkName != "--virtual-network-name--" { + t.Fatalf("Expected VirtualNetworkName to be --virtual-network-name--") + } + if c.VirtualNetworkResourceGroupName != "--virtual-network-resource-group-name--" { + t.Fatalf("Expected VirtualNetworkResourceGroupName to be --virtual-network-resource-group-name--") + } + if c.VirtualNetworkSubnetName != "--virtual-network-subnet-name--" { + t.Fatalf("Expected VirtualNetworkSubnetName to be --virtual-network-subnet-name--") + } +} + +// If the user set virtual network name then the code should resolve virtual network +// resource group name. +func TestResourceResolverSetVirtualNetworkResourceGroupName(t *testing.T) { + c, _, _ := newConfig(getArmBuilderConfiguration(), getPackerConfiguration()) + c.VirtualNetworkName = "--virtual-network-name--" + + sut := newTestResourceResolver() + sut.Resolve(c) + + if c.VirtualNetworkResourceGroupName != "findVirtualNetworkResourceGroup is mocked" { + t.Fatalf("Expected VirtualNetworkResourceGroupName to be 'findVirtualNetworkResourceGroup is mocked'") + } + if c.VirtualNetworkSubnetName != "findVirtualNetworkSubnet is mocked" { + t.Fatalf("Expected findVirtualNetworkSubnet to be 'findVirtualNetworkSubnet is mocked'") + } +} + +func newTestResourceResolver() resourceResolver { + return resourceResolver{ + client: nil, + findVirtualNetworkResourceGroup: func(*AzureClient, string) (string, error) { + return "findVirtualNetworkResourceGroup is mocked", nil + }, + findVirtualNetworkSubnet: func(*AzureClient, string, string) (string, error) { + return "findVirtualNetworkSubnet is mocked", nil + }, + } +} diff --git a/builder/azure/arm/step_get_ip_address.go b/builder/azure/arm/step_get_ip_address.go index 4b664c4ed..d0b24e82c 100644 --- a/builder/azure/arm/step_get_ip_address.go +++ b/builder/azure/arm/step_get_ip_address.go @@ -11,43 +11,77 @@ import ( "github.com/mitchellh/packer/packer" ) +type EndpointType int + +const ( + PublicEndpoint EndpointType = iota + PrivateEndpoint +) + +var ( + EndpointCommunicationText = map[EndpointType]string{ + PublicEndpoint: "PublicEndpoint", + PrivateEndpoint: "PrivateEndpoint", + } +) + type StepGetIPAddress struct { - client *AzureClient - get func(resourceGroupName string, ipAddressName string) (string, error) - say func(message string) - error func(e error) + client *AzureClient + endpoint EndpointType + get func(resourceGroupName string, ipAddressName string, interfaceName string) (string, error) + say func(message string) + error func(e error) } -func NewStepGetIPAddress(client *AzureClient, ui packer.Ui) *StepGetIPAddress { +func NewStepGetIPAddress(client *AzureClient, ui packer.Ui, endpoint EndpointType) *StepGetIPAddress { var step = &StepGetIPAddress{ - client: client, - say: func(message string) { ui.Say(message) }, - error: func(e error) { ui.Error(e.Error()) }, + client: client, + endpoint: endpoint, + say: func(message string) { ui.Say(message) }, + error: func(e error) { ui.Error(e.Error()) }, + } + + switch endpoint { + case PrivateEndpoint: + step.get = step.getPrivateIP + case PublicEndpoint: + step.get = step.getPublicIP } - step.get = step.getIPAddress return step } -func (s *StepGetIPAddress) getIPAddress(resourceGroupName string, ipAddressName string) (string, error) { - res, err := s.client.PublicIPAddressesClient.Get(resourceGroupName, ipAddressName, "") +func (s *StepGetIPAddress) getPrivateIP(resourceGroupName string, ipAddressName string, interfaceName string) (string, error) { + resp, err := s.client.InterfacesClient.Get(resourceGroupName, interfaceName, "") + if err != nil { + return "", err + } + + return *(*resp.Properties.IPConfigurations)[0].Properties.PrivateIPAddress, nil +} + +func (s *StepGetIPAddress) getPublicIP(resourceGroupName string, ipAddressName string, interfaceName string) (string, error) { + resp, err := s.client.PublicIPAddressesClient.Get(resourceGroupName, ipAddressName, "") if err != nil { - return "", nil + return "", err } - return *res.Properties.IPAddress, nil + return *resp.Properties.IPAddress, nil } func (s *StepGetIPAddress) Run(state multistep.StateBag) multistep.StepAction { - s.say("Getting the public IP address ...") + s.say("Getting the VM's IP address ...") var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string) var ipAddressName = state.Get(constants.ArmPublicIPAddressName).(string) + var nicName = state.Get(constants.ArmNicName).(string) s.say(fmt.Sprintf(" -> ResourceGroupName : '%s'", resourceGroupName)) s.say(fmt.Sprintf(" -> PublicIPAddressName : '%s'", ipAddressName)) + s.say(fmt.Sprintf(" -> NicName : '%s'", nicName)) + s.say(fmt.Sprintf(" -> Network Connection : '%s'", EndpointCommunicationText[s.endpoint])) - address, err := s.get(resourceGroupName, ipAddressName) + address, err := s.get(resourceGroupName, ipAddressName, nicName) if err != nil { state.Put(constants.Error, err) s.error(err) @@ -55,8 +89,8 @@ func (s *StepGetIPAddress) Run(state multistep.StateBag) multistep.StepAction { return multistep.ActionHalt } - s.say(fmt.Sprintf(" -> Public IP : '%s'", address)) state.Put(constants.SSHHost, address) + s.say(fmt.Sprintf(" -> IP Address : '%s'", address)) return multistep.ActionContinue } diff --git a/builder/azure/arm/step_get_ip_address_test.go b/builder/azure/arm/step_get_ip_address_test.go index 8f35ad8da..d6e29a2d7 100644 --- a/builder/azure/arm/step_get_ip_address_test.go +++ b/builder/azure/arm/step_get_ip_address_test.go @@ -13,9 +13,10 @@ import ( func TestStepGetIPAddressShouldFailIfGetFails(t *testing.T) { var testSubject = &StepGetIPAddress{ - get: func(string, string) (string, error) { return "", fmt.Errorf("!! Unit Test FAIL !!") }, - say: func(message string) {}, - error: func(e error) {}, + get: func(string, string, string) (string, error) { return "", fmt.Errorf("!! Unit Test FAIL !!") }, + endpoint: PublicEndpoint, + say: func(message string) {}, + error: func(e error) {}, } stateBag := createTestStateBagStepGetIPAddress() @@ -32,9 +33,10 @@ func TestStepGetIPAddressShouldFailIfGetFails(t *testing.T) { func TestStepGetIPAddressShouldPassIfGetPasses(t *testing.T) { var testSubject = &StepGetIPAddress{ - get: func(string, string) (string, error) { return "", nil }, - say: func(message string) {}, - error: func(e error) {}, + get: func(string, string, string) (string, error) { return "", nil }, + endpoint: PublicEndpoint, + say: func(message string) {}, + error: func(e error) {}, } stateBag := createTestStateBagStepGetIPAddress() @@ -52,16 +54,19 @@ func TestStepGetIPAddressShouldPassIfGetPasses(t *testing.T) { func TestStepGetIPAddressShouldTakeStepArgumentsFromStateBag(t *testing.T) { var actualResourceGroupName string var actualIPAddressName string + var actualNicName string var testSubject = &StepGetIPAddress{ - get: func(resourceGroupName string, ipAddressName string) (string, error) { + get: func(resourceGroupName string, ipAddressName string, nicName string) (string, error) { actualResourceGroupName = resourceGroupName actualIPAddressName = ipAddressName + actualNicName = nicName return "127.0.0.1", nil }, - say: func(message string) {}, - error: func(e error) {}, + endpoint: PublicEndpoint, + say: func(message string) {}, + error: func(e error) {}, } stateBag := createTestStateBagStepGetIPAddress() @@ -73,6 +78,7 @@ func TestStepGetIPAddressShouldTakeStepArgumentsFromStateBag(t *testing.T) { var expectedResourceGroupName = stateBag.Get(constants.ArmResourceGroupName).(string) var expectedIPAddressName = stateBag.Get(constants.ArmPublicIPAddressName).(string) + var expectedNicName = stateBag.Get(constants.ArmNicName).(string) if actualIPAddressName != expectedIPAddressName { t.Fatal("Expected StepGetIPAddress to source 'constants.ArmIPAddressName' from the state bag, but it did not.") @@ -82,6 +88,10 @@ func TestStepGetIPAddressShouldTakeStepArgumentsFromStateBag(t *testing.T) { t.Fatal("Expected StepGetIPAddress to source 'constants.ArmResourceGroupName' from the state bag, but it did not.") } + if actualNicName != expectedNicName { + t.Fatalf("Expected StepGetIPAddress to source 'constants.ArmNetworkInterfaceName' from the state bag, but it did not.") + } + expectedIPAddress, ok := stateBag.GetOk(constants.SSHHost) if !ok { t.Fatalf("Expected the state bag to have a value for '%s', but it did not.", constants.SSHHost) @@ -96,6 +106,7 @@ func createTestStateBagStepGetIPAddress() multistep.StateBag { stateBag := new(multistep.BasicStateBag) stateBag.Put(constants.ArmPublicIPAddressName, "Unit Test: PublicIPAddressName") + stateBag.Put(constants.ArmNicName, "Unit Test: NicName") stateBag.Put(constants.ArmResourceGroupName, "Unit Test: ResourceGroupName") return stateBag diff --git a/builder/azure/arm/template_factory.go b/builder/azure/arm/template_factory.go index ca0f95fb2..31faa797f 100644 --- a/builder/azure/arm/template_factory.go +++ b/builder/azure/arm/template_factory.go @@ -51,6 +51,13 @@ func GetVirtualMachineDeployment(config *Config) (*resources.Deployment, error) builder.SetMarketPlaceImage(config.ImagePublisher, config.ImageOffer, config.ImageSku, config.ImageVersion) } + if config.VirtualNetworkName != "" { + builder.SetVirtualNetwork( + config.VirtualNetworkResourceGroupName, + config.VirtualNetworkName, + config.VirtualNetworkSubnetName) + } + doc, _ := builder.ToJSON() return createDeploymentParameters(*doc, params) } diff --git a/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment03.approved.json b/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment03.approved.json index a6fce9d67..e0d7b10bb 100644 --- a/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment03.approved.json +++ b/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment03.approved.json @@ -154,7 +154,8 @@ "subnetName": "packerSubnet", "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", "virtualNetworkName": "packerNetwork", - "vmStorageAccountContainerName": "images", - "vnetID": "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" - } + "virtualNetworkResourceGroup": "[resourceGroup().name]", + "vmStorageAccountContainerName": "images", + "vnetID": "[resourceId(variables('virtualNetworkResourceGroup'), 'Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" + } } \ No newline at end of file diff --git a/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment04.approved.json b/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment04.approved.json index 74a4b767f..faa51be82 100644 --- a/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment04.approved.json +++ b/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment04.approved.json @@ -152,7 +152,8 @@ "subnetName": "packerSubnet", "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", "virtualNetworkName": "packerNetwork", - "vmStorageAccountContainerName": "images", - "vnetID": "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" - } + "virtualNetworkResourceGroup": "[resourceGroup().name]", + "vmStorageAccountContainerName": "images", + "vnetID": "[resourceId(variables('virtualNetworkResourceGroup'), 'Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" + } } \ No newline at end of file diff --git a/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment05.approved.json b/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment05.approved.json new file mode 100644 index 000000000..ae13afab1 --- /dev/null +++ b/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment05.approved.json @@ -0,0 +1,120 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json", + "contentVersion": "1.0.0.0", + "parameters": { + "adminPassword": { + "type": "string" + }, + "adminUsername": { + "type": "string" + }, + "dnsNameForPublicIP": { + "type": "string" + }, + "osDiskName": { + "type": "string" + }, + "storageAccountBlobEndpoint": { + "type": "string" + }, + "vmName": { + "type": "string" + }, + "vmSize": { + "type": "string" + } + }, + "resources": [ + { + "apiVersion": "[variables('apiVersion')]", + "dependsOn": [], + "location": "[variables('location')]", + "name": "[variables('nicName')]", + "properties": { + "ipConfigurations": [ + { + "name": "ipconfig", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "subnet": { + "id": "[variables('subnetRef')]" + } + } + } + ] + }, + "type": "Microsoft.Network/networkInterfaces" + }, + { + "apiVersion": "[variables('apiVersion')]", + "dependsOn": [ + "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]" + ], + "location": "[variables('location')]", + "name": "[parameters('vmName')]", + "properties": { + "diagnosticsProfile": { + "bootDiagnostics": { + "enabled": false + } + }, + "hardwareProfile": { + "vmSize": "[parameters('vmSize')]" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]" + } + ] + }, + "osProfile": { + "adminPassword": "[parameters('adminPassword')]", + "adminUsername": "[parameters('adminUsername')]", + "computerName": "[parameters('vmName')]", + "linuxConfiguration": { + "ssh": { + "publicKeys": [ + { + "keyData": "", + "path": "[variables('sshKeyPath')]" + } + ] + } + } + }, + "storageProfile": { + "osDisk": { + "caching": "ReadWrite", + "createOption": "FromImage", + "image": { + "uri": "https://localhost/custom.vhd" + }, + "name": "osdisk", + "osType": "Linux", + "vhd": { + "uri": "[concat(parameters('storageAccountBlobEndpoint'),variables('vmStorageAccountContainerName'),'/', parameters('osDiskName'),'.vhd')]" + } + } + } + }, + "type": "Microsoft.Compute/virtualMachines" + } + ], + "variables": { + "addressPrefix": "10.0.0.0/16", + "apiVersion": "2015-06-15", + "location": "[resourceGroup().location]", + "nicName": "packerNic", + "publicIPAddressName": "packerPublicIP", + "publicIPAddressType": "Dynamic", + "sshKeyPath": "[concat('/home/',parameters('adminUsername'),'/.ssh/authorized_keys')]", + "subnetAddressPrefix": "10.0.0.0/24", + "subnetName": "virtualNetworkSubnetName", + "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", + "virtualNetworkName": "virtualNetworkName", + "virtualNetworkResourceGroup": "virtualNetworkResourceGroupName", + "vmStorageAccountContainerName": "images", + "vnetID": "[resourceId(variables('virtualNetworkResourceGroup'), 'Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" + } +} \ No newline at end of file diff --git a/builder/azure/arm/template_factory_test.go b/builder/azure/arm/template_factory_test.go index 4ab976f9c..708d7f379 100644 --- a/builder/azure/arm/template_factory_test.go +++ b/builder/azure/arm/template_factory_test.go @@ -145,6 +145,38 @@ func TestVirtualMachineDeployment04(t *testing.T) { } } +func TestVirtualMachineDeployment05(t *testing.T) { + config := map[string]string{ + "capture_name_prefix": "ignore", + "capture_container_name": "ignore", + "location": "ignore", + "image_url": "https://localhost/custom.vhd", + "resource_group_name": "ignore", + "storage_account": "ignore", + "subscription_id": "ignore", + "os_type": constants.Target_Linux, + "communicator": "none", + "virtual_network_name": "virtualNetworkName", + "virtual_network_resource_group_name": "virtualNetworkResourceGroupName", + "virtual_network_subnet_name": "virtualNetworkSubnetName", + } + + c, _, err := newConfig(config, getPackerConfiguration()) + if err != nil { + t.Fatal(err) + } + + deployment, err := GetVirtualMachineDeployment(c) + if err != nil { + t.Fatal(err) + } + + err = approvaltests.VerifyJSONStruct(t, deployment.Properties.Template) + if err != nil { + t.Fatal(err) + } +} + // Ensure the link values are not set, and the concrete values are set. func TestKeyVaultDeployment00(t *testing.T) { c, _, _ := newConfig(getArmBuilderConfiguration(), getPackerConfiguration()) diff --git a/builder/azure/common/constants/stateBag.go b/builder/azure/common/constants/stateBag.go index 25451967d..0955994e6 100644 --- a/builder/azure/common/constants/stateBag.go +++ b/builder/azure/common/constants/stateBag.go @@ -18,6 +18,7 @@ const ( ArmComputeName string = "arm.ComputeName" ArmCertificateUrl string = "arm.CertificateUrl" ArmDeploymentName string = "arm.DeploymentName" + ArmNicName string = "arm.NicName" ArmKeyVaultName string = "arm.KeyVaultName" ArmLocation string = "arm.Location" ArmOSDiskVhd string = "arm.OSDiskVhd" diff --git a/builder/azure/common/template/TestBuildLinux02.approved.txt b/builder/azure/common/template/TestBuildLinux02.approved.txt new file mode 100644 index 000000000..c2533dd2d --- /dev/null +++ b/builder/azure/common/template/TestBuildLinux02.approved.txt @@ -0,0 +1,120 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json", + "contentVersion": "1.0.0.0", + "parameters": { + "adminPassword": { + "type": "string" + }, + "adminUsername": { + "type": "string" + }, + "dnsNameForPublicIP": { + "type": "string" + }, + "osDiskName": { + "type": "string" + }, + "storageAccountBlobEndpoint": { + "type": "string" + }, + "vmName": { + "type": "string" + }, + "vmSize": { + "type": "string" + } + }, + "variables": { + "addressPrefix": "10.0.0.0/16", + "apiVersion": "2015-06-15", + "location": "[resourceGroup().location]", + "nicName": "packerNic", + "publicIPAddressName": "packerPublicIP", + "publicIPAddressType": "Dynamic", + "sshKeyPath": "[concat('/home/',parameters('adminUsername'),'/.ssh/authorized_keys')]", + "subnetAddressPrefix": "10.0.0.0/24", + "subnetName": "--subnet-name--", + "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", + "virtualNetworkName": "--virtual-network--", + "virtualNetworkResourceGroup": "--virtual-network-resource-group--", + "vmStorageAccountContainerName": "images", + "vnetID": "[resourceId(variables('virtualNetworkResourceGroup'), 'Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" + }, + "resources": [ + { + "apiVersion": "[variables('apiVersion')]", + "name": "[variables('nicName')]", + "type": "Microsoft.Network/networkInterfaces", + "location": "[variables('location')]", + "dependsOn": [], + "properties": { + "ipConfigurations": [ + { + "properties": { + "privateIPAllocationMethod": "Dynamic", + "subnet": { + "id": "[variables('subnetRef')]" + } + }, + "name": "ipconfig" + } + ] + } + }, + { + "apiVersion": "[variables('apiVersion')]", + "name": "[parameters('vmName')]", + "type": "Microsoft.Compute/virtualMachines", + "location": "[variables('location')]", + "dependsOn": [ + "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]" + ], + "properties": { + "diagnosticsProfile": { + "bootDiagnostics": { + "enabled": false + } + }, + "hardwareProfile": { + "vmSize": "[parameters('vmSize')]" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]" + } + ] + }, + "osProfile": { + "computerName": "[parameters('vmName')]", + "adminUsername": "[parameters('adminUsername')]", + "adminPassword": "[parameters('adminPassword')]", + "linuxConfiguration": { + "ssh": { + "publicKeys": [ + { + "path": "[variables('sshKeyPath')]", + "keyData": "--test-ssh-authorized-key--" + } + ] + } + } + }, + "storageProfile": { + "osDisk": { + "osType": "Linux", + "name": "osdisk", + "vhd": { + "uri": "[concat(parameters('storageAccountBlobEndpoint'),variables('vmStorageAccountContainerName'),'/', parameters('osDiskName'),'.vhd')]" + }, + "image": { + "uri": "http://azure/custom.vhd" + }, + "caching": "ReadWrite", + "createOption": "FromImage" + } + } + } + } + ] +} \ No newline at end of file diff --git a/builder/azure/common/template/template_builder.go b/builder/azure/common/template/template_builder.go index d69d782e7..c8cecfcab 100644 --- a/builder/azure/common/template/template_builder.go +++ b/builder/azure/common/template/template_builder.go @@ -13,8 +13,11 @@ const ( jsonPrefix = "" jsonIndent = " " - resourceVirtualMachine = "Microsoft.Compute/virtualMachines" - resourceKeyVaults = "Microsoft.KeyVault/vaults" + resourceKeyVaults = "Microsoft.KeyVault/vaults" + resourceNetworkInterfaces = "Microsoft.Network/networkInterfaces" + resourcePublicIPAddresses = "Microsoft.Network/publicIPAddresses" + resourceVirtualMachine = "Microsoft.Compute/virtualMachines" + resourceVirtualNetworks = "Microsoft.Network/virtualNetworks" variableSshKeyPath = "sshKeyPath" ) @@ -125,6 +128,28 @@ func (s *TemplateBuilder) SetImageUrl(imageUrl string, osType compute.OperatingS return nil } +func (s *TemplateBuilder) SetVirtualNetwork(virtualNetworkResourceGroup, virtualNetworkName, subnetName string) error { + s.setVariable("virtualNetworkResourceGroup", virtualNetworkResourceGroup) + s.setVariable("virtualNetworkName", virtualNetworkName) + s.setVariable("subnetName", subnetName) + + s.deleteResourceByType(resourceVirtualNetworks) + s.deleteResourceByType(resourcePublicIPAddresses) + resource, err := s.getResourceByType(resourceNetworkInterfaces) + if err != nil { + return err + } + + s.deleteResourceDependency(resource, func(s string) bool { + return strings.Contains(s, "Microsoft.Network/virtualNetworks") || + strings.Contains(s, "Microsoft.Network/publicIPAddresses") + }) + + (*resource.Properties.IPConfigurations)[0].Properties.PublicIPAddress = nil + + return nil +} + func (s *TemplateBuilder) ToJSON() (*string, error) { bs, err := json.MarshalIndent(s.template, jsonPrefix, jsonIndent) @@ -144,6 +169,10 @@ func (s *TemplateBuilder) getResourceByType(t string) (*Resource, error) { return nil, fmt.Errorf("template: could not find a resource of type %s", t) } +func (s *TemplateBuilder) setVariable(name string, value string) { + (*s.template.Variables)[name] = value +} + func (s *TemplateBuilder) toKeyVaultID(name string) string { return s.toResourceID(resourceKeyVaults, name) } @@ -156,6 +185,31 @@ func (s *TemplateBuilder) toVariable(name string) string { return fmt.Sprintf("[variables('%s')]", name) } +func (s *TemplateBuilder) deleteResourceByType(resourceType string) { + resources := make([]Resource, 0) + + for _, resource := range *s.template.Resources { + if *resource.Type == resourceType { + continue + } + resources = append(resources, resource) + } + + s.template.Resources = &resources +} + +func (s *TemplateBuilder) deleteResourceDependency(resource *Resource, predicate func(string) bool) { + deps := make([]string, 0) + + for _, dep := range *resource.DependsOn { + if !predicate(dep) { + deps = append(deps, dep) + } + } + + *resource.DependsOn = deps +} + const basicTemplate = `{ "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json", "contentVersion": "1.0.0.0", @@ -194,8 +248,9 @@ const basicTemplate = `{ "subnetAddressPrefix": "10.0.0.0/24", "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", "virtualNetworkName": "packerNetwork", + "virtualNetworkResourceGroup": "[resourceGroup().name]", "vmStorageAccountContainerName": "images", - "vnetID": "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" + "vnetID": "[resourceId(variables('virtualNetworkResourceGroup'), 'Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" }, "resources": [ { diff --git a/builder/azure/common/template/template_builder_test.TestBuildLinux00.approved.json b/builder/azure/common/template/template_builder_test.TestBuildLinux00.approved.json index a8ecfc0be..59b5da643 100644 --- a/builder/azure/common/template/template_builder_test.TestBuildLinux00.approved.json +++ b/builder/azure/common/template/template_builder_test.TestBuildLinux00.approved.json @@ -154,7 +154,8 @@ "subnetName": "packerSubnet", "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", "virtualNetworkName": "packerNetwork", - "vmStorageAccountContainerName": "images", - "vnetID": "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" - } + "virtualNetworkResourceGroup": "[resourceGroup().name]", + "vmStorageAccountContainerName": "images", + "vnetID": "[resourceId(variables('virtualNetworkResourceGroup'), 'Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" + } } \ No newline at end of file diff --git a/builder/azure/common/template/template_builder_test.TestBuildLinux01.approved.json b/builder/azure/common/template/template_builder_test.TestBuildLinux01.approved.json index 45bf5b271..f26c3f1c2 100644 --- a/builder/azure/common/template/template_builder_test.TestBuildLinux01.approved.json +++ b/builder/azure/common/template/template_builder_test.TestBuildLinux01.approved.json @@ -152,7 +152,8 @@ "subnetName": "packerSubnet", "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", "virtualNetworkName": "packerNetwork", - "vmStorageAccountContainerName": "images", - "vnetID": "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" - } + "virtualNetworkResourceGroup": "[resourceGroup().name]", + "vmStorageAccountContainerName": "images", + "vnetID": "[resourceId(variables('virtualNetworkResourceGroup'), 'Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" + } } \ No newline at end of file diff --git a/builder/azure/common/template/template_builder_test.TestBuildLinux02.approved.json b/builder/azure/common/template/template_builder_test.TestBuildLinux02.approved.json new file mode 100644 index 000000000..217c1f415 --- /dev/null +++ b/builder/azure/common/template/template_builder_test.TestBuildLinux02.approved.json @@ -0,0 +1,120 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json", + "contentVersion": "1.0.0.0", + "parameters": { + "adminPassword": { + "type": "string" + }, + "adminUsername": { + "type": "string" + }, + "dnsNameForPublicIP": { + "type": "string" + }, + "osDiskName": { + "type": "string" + }, + "storageAccountBlobEndpoint": { + "type": "string" + }, + "vmName": { + "type": "string" + }, + "vmSize": { + "type": "string" + } + }, + "resources": [ + { + "apiVersion": "[variables('apiVersion')]", + "dependsOn": [], + "location": "[variables('location')]", + "name": "[variables('nicName')]", + "properties": { + "ipConfigurations": [ + { + "name": "ipconfig", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "subnet": { + "id": "[variables('subnetRef')]" + } + } + } + ] + }, + "type": "Microsoft.Network/networkInterfaces" + }, + { + "apiVersion": "[variables('apiVersion')]", + "dependsOn": [ + "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]" + ], + "location": "[variables('location')]", + "name": "[parameters('vmName')]", + "properties": { + "diagnosticsProfile": { + "bootDiagnostics": { + "enabled": false + } + }, + "hardwareProfile": { + "vmSize": "[parameters('vmSize')]" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]" + } + ] + }, + "osProfile": { + "adminPassword": "[parameters('adminPassword')]", + "adminUsername": "[parameters('adminUsername')]", + "computerName": "[parameters('vmName')]", + "linuxConfiguration": { + "ssh": { + "publicKeys": [ + { + "keyData": "--test-ssh-authorized-key--", + "path": "[variables('sshKeyPath')]" + } + ] + } + } + }, + "storageProfile": { + "osDisk": { + "caching": "ReadWrite", + "createOption": "FromImage", + "image": { + "uri": "http://azure/custom.vhd" + }, + "name": "osdisk", + "osType": "Linux", + "vhd": { + "uri": "[concat(parameters('storageAccountBlobEndpoint'),variables('vmStorageAccountContainerName'),'/', parameters('osDiskName'),'.vhd')]" + } + } + } + }, + "type": "Microsoft.Compute/virtualMachines" + } + ], + "variables": { + "addressPrefix": "10.0.0.0/16", + "apiVersion": "2015-06-15", + "location": "[resourceGroup().location]", + "nicName": "packerNic", + "publicIPAddressName": "packerPublicIP", + "publicIPAddressType": "Dynamic", + "sshKeyPath": "[concat('/home/',parameters('adminUsername'),'/.ssh/authorized_keys')]", + "subnetAddressPrefix": "10.0.0.0/24", + "subnetName": "--subnet-name--", + "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", + "virtualNetworkName": "--virtual-network--", + "virtualNetworkResourceGroup": "--virtual-network-resource-group--", + "vmStorageAccountContainerName": "images", + "vnetID": "[resourceId(variables('virtualNetworkResourceGroup'), 'Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" + } +} \ No newline at end of file diff --git a/builder/azure/common/template/template_builder_test.TestBuildWindows00.approved.json b/builder/azure/common/template/template_builder_test.TestBuildWindows00.approved.json index d2460d7b4..67dec1b2d 100644 --- a/builder/azure/common/template/template_builder_test.TestBuildWindows00.approved.json +++ b/builder/azure/common/template/template_builder_test.TestBuildWindows00.approved.json @@ -168,7 +168,8 @@ "subnetName": "packerSubnet", "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", "virtualNetworkName": "packerNetwork", - "vmStorageAccountContainerName": "images", - "vnetID": "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" - } + "virtualNetworkResourceGroup": "[resourceGroup().name]", + "vmStorageAccountContainerName": "images", + "vnetID": "[resourceId(variables('virtualNetworkResourceGroup'), 'Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" + } } \ No newline at end of file diff --git a/builder/azure/common/template/template_builder_test.go b/builder/azure/common/template/template_builder_test.go index f203acace..1097eab8b 100644 --- a/builder/azure/common/template/template_builder_test.go +++ b/builder/azure/common/template/template_builder_test.go @@ -64,6 +64,32 @@ func TestBuildLinux01(t *testing.T) { } } +// Ensure that a user can specify an existing Virtual Network +func TestBuildLinux02(t *testing.T) { + testSubject, err := NewTemplateBuilder() + if err != nil { + t.Fatal(err) + } + + testSubject.BuildLinux("--test-ssh-authorized-key--") + testSubject.SetImageUrl("http://azure/custom.vhd", compute.Linux) + + err = testSubject.SetVirtualNetwork("--virtual-network-resource-group--", "--virtual-network--", "--subnet-name--") + if err != nil { + t.Fatal(err) + } + + doc, err := testSubject.ToJSON() + if err != nil { + t.Fatal(err) + } + + err = approvaltests.VerifyJSONBytes(t, []byte(*doc)) + if err != nil { + t.Fatal(err) + } +} + // Ensure that a Windows template is configured as expected. // * Include WinRM configuration. // * Include KeyVault configuration, which is needed for WinRM. diff --git a/website/source/docs/builders/azure.html.md b/website/source/docs/builders/azure.html.md index adc5655b8..71b6006db 100644 --- a/website/source/docs/builders/azure.html.md +++ b/website/source/docs/builders/azure.html.md @@ -83,6 +83,19 @@ builder. configures your Tenant ID, Object ID, Key Vault Name, Key Vault Secret, and WinRM certificate URL. +- `virtual_network_name` (string) 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**. This value should only be set if + Packer is executed from a host on the same subnet / virtual network. + +- `virtual_network_resource_group_name` (string) 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. + +- `virtual_network_subnet_name` (string) 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. + - `vm_size` (string) 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`.