tests/image: boot the vhd images on Azure

Previously, vhd images were tested using QEMU. This commit changes that to
boot them in the actual Azure infrastructure.

Azure VMs have quite a lot of dependencies - a network interface, a virtual
network, a network security group, a public ip address and a disk. Azure CLI
and Azure Portal handle the creation of all these resources internally.
However, when using the API, the caller is responsible to create all these
resources before creating an actual VM.

To handle the creation of all the resources in the right order, a deployment
is used. A deployment is a set of resources defined in a JSON document.
It can optionally take parameters to customize each deployment. After the
deployment is finished, the VM is up and ready to be tested using SSH.

Sadly, the deployments are a bit hard to clean-up. One would expect that
deleting a deployment removes all the deployed resources. However, it doesn't
work this way and therefore it's needed to clean up all resources "manually".
For this reason, our deployment sets a unique tag on all the resources created
by the deployment. After this test is finished, the API is queried for all
the resources with the tag and then, they're deleted in the right order.
This commit is contained in:
Ondřej Budai 2020-04-27 12:07:16 +02:00 committed by Tom Gundersen
parent 10c016edca
commit e235fdedb3
11 changed files with 669 additions and 25 deletions

View file

@ -0,0 +1,328 @@
// +build integration
package azuretest
import (
"context"
"errors"
"fmt"
"io/ioutil"
"net/url"
"os"
"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-09-01/network"
"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2019-05-01/resources"
"github.com/Azure/azure-storage-blob-go/azblob"
"github.com/Azure/go-autorest/autorest/azure/auth"
"github.com/osbuild/osbuild-composer/internal/upload/azure"
)
// wrapErrorf returns error constructed using fmt.Errorf from format and any
// other args. If innerError != nil, it's appended at the end of the new
// error.
func wrapErrorf(innerError error, format string, a ...interface{}) error {
if innerError != nil {
a = append(a, innerError)
return fmt.Errorf(format+"\n\ninner error: %#s", a...)
}
return fmt.Errorf(format, a...)
}
type azureCredentials struct {
azure.Credentials
ContainerName string
SubscriptionID string
ClientID string
ClientSecret string
TenantID string
Location string
ResourceGroup string
}
// getAzureCredentialsFromEnv gets the credentials from environment variables
// If none of the environment variables is set, it returns nil.
// If some but not all environment variables are set, it returns an error.
func GetAzureCredentialsFromEnv() (*azureCredentials, error) {
storageAccount, saExists := os.LookupEnv("AZURE_STORAGE_ACCOUNT")
storageAccessKey, sakExists := os.LookupEnv("AZURE_STORAGE_ACCESS_KEY")
containerName, cExists := os.LookupEnv("AZURE_CONTAINER_NAME")
subscriptionId, siExists := os.LookupEnv("AZURE_SUBSCRIPTION_ID")
clientId, ciExists := os.LookupEnv("AZURE_CLIENT_ID")
clientSecret, csExists := os.LookupEnv("AZURE_CLIENT_SECRET")
tenantId, tiExists := os.LookupEnv("AZURE_TENANT_ID")
location, lExists := os.LookupEnv("AZURE_LOCATION")
resourceGroup, rgExists := os.LookupEnv("AZURE_RESOURCE_GROUP")
// Workaround Travis security feature. If non of the variables is set, just ignore the test
if !saExists && !sakExists && !cExists && !siExists && !ciExists && !csExists && !tiExists && !lExists && !rgExists {
return nil, nil
}
// If only one/two of them are not set, then fail
if !saExists || !sakExists || !cExists || !siExists || !ciExists || !csExists || !tiExists || !lExists || !rgExists {
return nil, errors.New("not all required env variables were set")
}
return &azureCredentials{
Credentials: azure.Credentials{
StorageAccount: storageAccount,
StorageAccessKey: storageAccessKey,
},
ContainerName: containerName,
SubscriptionID: subscriptionId,
ClientID: clientId,
ClientSecret: clientSecret,
TenantID: tenantId,
Location: location,
ResourceGroup: resourceGroup,
}, nil
}
// UploadImageToAzure mimics the upload feature of osbuild-composer.
func UploadImageToAzure(c *azureCredentials, imagePath string, imageName string) error {
metadata := azure.ImageMetadata{
ContainerName: c.ContainerName,
ImageName: imageName,
}
err := azure.UploadImage(c.Credentials, metadata, imagePath, 16)
if err != nil {
return fmt.Errorf("upload to azure failed: %v", err)
}
return nil
}
// DeleteImageFromAzure deletes the image uploaded by osbuild-composer
// (or UpluadImageToAzure method).
func DeleteImageFromAzure(c *azureCredentials, imageName string) error {
// Create a default request pipeline using your storage account name and account key.
credential, err := azblob.NewSharedKeyCredential(c.StorageAccount, c.StorageAccessKey)
if err != nil {
return err
}
p := azblob.NewPipeline(credential, azblob.PipelineOptions{})
// get storage account blob service URL endpoint.
URL, _ := url.Parse(fmt.Sprintf("https://%s.blob.core.windows.net/%s", c.StorageAccount, c.ContainerName))
// Create a ContainerURL object that wraps the container URL and a request
// pipeline to make requests.
containerURL := azblob.NewContainerURL(*URL, p)
// Create the container, use a never-expiring context
ctx := context.Background()
blobURL := containerURL.NewPageBlobURL(imageName)
_, err = blobURL.Delete(ctx, azblob.DeleteSnapshotsOptionInclude, azblob.BlobAccessConditions{})
if err != nil {
return fmt.Errorf("cannot delete the image: %v", err)
}
return nil
}
type resourceType struct {
resType string
apiVersion string
}
// resourcesTypesToDelete serves two purposes:
// 1) The WithBootedImageInAzure method tags all the created resources and
// we can get the list of resources with that tag. However, it's needed to
// delete them in right order because of inner dependencies.
// 2) The resources.Client.DeleteByID method requires the API version to be
// passed in. Therefore we need to way to get API version for a given
// resource type.
var resourcesTypesToDelete = []resourceType{
{
resType: "Microsoft.Compute/virtualMachines",
apiVersion: "2019-07-01",
},
{
resType: "Microsoft.Network/networkInterfaces",
apiVersion: "2019-09-01",
},
{
resType: "Microsoft.Network/publicIPAddresses",
apiVersion: "2019-09-01",
},
{
resType: "Microsoft.Network/networkSecurityGroups",
apiVersion: "2019-09-01",
},
{
resType: "Microsoft.Network/virtualNetworks",
apiVersion: "2019-09-01",
},
{
resType: "Microsoft.Compute/disks",
apiVersion: "2019-07-01",
},
{
resType: "Microsoft.Compute/images",
apiVersion: "2019-07-01",
},
}
// readPublicKey reads the public key from a file and returns it as a string
func readPublicKey(publicKeyFile string) (string, error) {
publicKey, err := ioutil.ReadFile(publicKeyFile)
if err != nil {
return "", fmt.Errorf("cannot read the public key file: %v", err)
}
return string(publicKey), nil
}
// deleteResource is a convenient wrapper around Azure SDK to delete a resource
func deleteResource(client resources.Client, id string, apiVersion string) error {
deleteFuture, err := client.DeleteByID(context.Background(), id, apiVersion)
if err != nil {
return fmt.Errorf("cannot delete the resourceType %s: %v", id, err)
}
err = deleteFuture.WaitForCompletionRef(context.Background(), client.BaseClient.Client)
if err != nil {
return fmt.Errorf("waiting for the resourceType %s deletion failed: %v", id, err)
}
_, err = deleteFuture.Result(client)
if err != nil {
return fmt.Errorf("cannot retrieve the result of %s deletion: %v", id, err)
}
return nil
}
// withBootedImageInAzure runs the function f in the context of booted
// image in Azure
func WithBootedImageInAzure(creds *azureCredentials, imageName, testId, publicKeyFile string, f func(address string) error) (retErr error) {
publicKey, err := readPublicKey(publicKeyFile)
if err != nil {
return err
}
clientCredentialsConfig := auth.NewClientCredentialsConfig(creds.ClientID, creds.ClientSecret, creds.TenantID)
authorizer, err := clientCredentialsConfig.Authorizer()
if err != nil {
return fmt.Errorf("cannot create the authorizer: %v", err)
}
template, err := loadDeploymentTemplate()
if err != nil {
return err
}
// Azure requires a lot of names - for a virtual machine, a virtual network,
// a virtual interface and so on and so forth.
// In the Go code, it's just need to know the public IP address name,
// the tag name and the deployment name. Let's set these here to names
// based on the test id.
// The rest of the names are set from the test id inside the deployment
// template because they're irrelevant to the Go code.
deploymentName := testId
tag := "tag-" + testId
publicIPAddressName := "address-" + testId
imagePath := fmt.Sprintf("https://%s.blob.core.windows.net/%s/%s", creds.StorageAccount, creds.ContainerName, imageName)
parameters := deploymentParameters{
Location: newDeploymentParameter(creds.Location),
TestId: newDeploymentParameter(testId),
Tag: newDeploymentParameter(tag),
PublicIPAddressName: newDeploymentParameter(publicIPAddressName),
ImagePath: newDeploymentParameter(imagePath),
AdminUsername: newDeploymentParameter("redhat"),
AdminPublicKey: newDeploymentParameter(publicKey),
}
deploymentsClient := resources.NewDeploymentsClient(creds.SubscriptionID)
deploymentsClient.Authorizer = authorizer
deploymentFuture, err := deploymentsClient.CreateOrUpdate(context.Background(), creds.ResourceGroup, deploymentName, resources.Deployment{
Properties: &resources.DeploymentProperties{
Mode: resources.Incremental,
Template: template,
Parameters: parameters,
},
})
// Let's registed the clean-up function as soon as possible.
defer func() {
resourcesClient := resources.NewClient(creds.SubscriptionID)
resourcesClient.Authorizer = authorizer
// find all the resources we marked with a tag during the deployment
filter := fmt.Sprintf("tagName eq 'osbuild-composer-image-test' and tagValue eq '%s'", tag)
resourceList, err := resourcesClient.ListByResourceGroup(context.Background(), creds.ResourceGroup, filter, "", nil)
if err != nil {
retErr = wrapErrorf(retErr, "listing of resources failed: %v", err)
} else {
// delete all the found resources
for _, resourceType := range resourcesTypesToDelete {
for _, resource := range resourceList.Values() {
if *resource.Type != resourceType.resType {
continue
}
err := deleteResource(resourcesClient, *resource.ID, resourceType.apiVersion)
if err != nil {
retErr = wrapErrorf(retErr, "cannot delete the resource %s: %v", *resource.ID, err)
// do not return here, try deleting as much as possible
}
}
}
}
// Delete the deployment
// This actually does not delete any resources created by the
// deployment as one might think. Therefore the code above
// and the tagging are needed.
result, err := deploymentsClient.Delete(context.Background(), creds.ResourceGroup, deploymentName)
if err != nil {
retErr = wrapErrorf(retErr, "cannot create the request for the deployment deletion: %v", err)
return
}
err = result.WaitForCompletionRef(context.Background(), deploymentsClient.Client)
if err != nil {
retErr = wrapErrorf(retErr, "waiting for the deployment deletion failed: %v", err)
return
}
_, err = result.Result(deploymentsClient)
if err != nil {
retErr = wrapErrorf(retErr, "cannot retrieve the deployment deletion result: %v", err)
return
}
}()
if err != nil {
return fmt.Errorf("creating a deployment failed: %v", err)
}
err = deploymentFuture.WaitForCompletionRef(context.Background(), deploymentsClient.Client)
if err != nil {
return fmt.Errorf("waiting for deployment completion failed: %v", err)
}
_, err = deploymentFuture.Result(deploymentsClient)
if err != nil {
return fmt.Errorf("retrieving the deployment result failed: %v", err)
}
// get the IP address
publicIPAddressClient := network.NewPublicIPAddressesClient(creds.SubscriptionID)
publicIPAddressClient.Authorizer = authorizer
publicIPAddress, err := publicIPAddressClient.Get(context.Background(), creds.ResourceGroup, publicIPAddressName, "")
if err != nil {
return fmt.Errorf("cannot get the ip address details: %v", err)
}
return f(*publicIPAddress.IPAddress)
}

View file

@ -0,0 +1,52 @@
// +build integration
package azuretest
import (
"encoding/json"
"fmt"
"os"
"github.com/osbuild/osbuild-composer/cmd/osbuild-image-tests/constants"
)
// loadDeploymentTemplate loads the deployment template from the specified
// path and return it as a "dynamically" typed object
func loadDeploymentTemplate() (interface{}, error) {
f, err := os.Open(constants.TestPaths.AzureDeploymentTemplate)
if err != nil {
return nil, fmt.Errorf("cannot open the deployment file: %v", err)
}
defer f.Close()
var result interface{}
err = json.NewDecoder(f).Decode(&result)
if err != nil {
return nil, fmt.Errorf("cannot decode the deployment file: %v", err)
}
return result, nil
}
// struct for encoding a deployment parameter
type deploymentParameter struct {
Value interface{} `json:"value"`
}
func newDeploymentParameter(value interface{}) deploymentParameter {
return deploymentParameter{Value: value}
}
// struct for encoding deployment parameters
type deploymentParameters struct {
Location deploymentParameter `json:"location"`
TestId deploymentParameter `json:"testId"`
Tag deploymentParameter `json:"tag"`
PublicIPAddressName deploymentParameter `json:"publicIPAddressName"`
ImagePath deploymentParameter `json:"imagePath"`
AdminUsername deploymentParameter `json:"adminUsername"`
AdminPublicKey deploymentParameter `json:"adminPublicKey"`
}

View file

@ -18,15 +18,17 @@ func GetOsbuildCommand(store string) *exec.Cmd {
}
var TestPaths = struct {
ImageInfo string
PrivateKey string
TestCasesDirectory string
UserData string
MetaData string
ImageInfo string
PrivateKey string
TestCasesDirectory string
UserData string
MetaData string
AzureDeploymentTemplate string
}{
ImageInfo: "tools/image-info",
PrivateKey: "test/keyring/id_rsa",
TestCasesDirectory: "test/cases",
UserData: "test/cloud-init/user-data",
MetaData: "test/cloud-init/meta-data",
ImageInfo: "tools/image-info",
PrivateKey: "test/keyring/id_rsa",
TestCasesDirectory: "test/cases",
UserData: "test/cloud-init/user-data",
MetaData: "test/cloud-init/meta-data",
AzureDeploymentTemplate: "test/azure-deployment-template.json",
}

View file

@ -14,15 +14,17 @@ func GetOsbuildCommand(store string) *exec.Cmd {
}
var TestPaths = struct {
ImageInfo string
PrivateKey string
TestCasesDirectory string
UserData string
MetaData string
ImageInfo string
PrivateKey string
TestCasesDirectory string
UserData string
MetaData string
AzureDeploymentTemplate string
}{
ImageInfo: "/usr/libexec/osbuild-composer/image-info",
PrivateKey: "/usr/share/tests/osbuild-composer/keyring/id_rsa",
TestCasesDirectory: "/usr/share/tests/osbuild-composer/cases",
UserData: "/usr/share/tests/osbuild-composer/cloud-init/user-data",
MetaData: "/usr/share/tests/osbuild-composer/cloud-init/meta-data",
ImageInfo: "/usr/libexec/osbuild-composer/image-info",
PrivateKey: "/usr/share/tests/osbuild-composer/keyring/id_rsa",
TestCasesDirectory: "/usr/share/tests/osbuild-composer/cases",
UserData: "/usr/share/tests/osbuild-composer/cloud-init/user-data",
MetaData: "/usr/share/tests/osbuild-composer/cloud-init/meta-data",
AzureDeploymentTemplate: "/usr/share/tests/osbuild-composer/azure-deployment-template.json",
}

View file

@ -21,6 +21,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/osbuild/osbuild-composer/cmd/osbuild-image-tests/azuretest"
"github.com/osbuild/osbuild-composer/cmd/osbuild-image-tests/constants"
"github.com/osbuild/osbuild-composer/internal/common"
)
@ -262,6 +263,43 @@ func testBootUsingAWS(t *testing.T, imagePath string) {
require.NoError(t, err)
}
func testBootUsingAzure(t *testing.T, imagePath string) {
creds, err := azuretest.GetAzureCredentialsFromEnv()
require.NoError(t, err)
// if no credentials are given, fall back to qemu
if creds == nil {
log.Print("no Azure credentials given, falling back to booting using qemu")
testBootUsingQemu(t, imagePath)
return
}
// create a random test id to name all the resources used in this test
testId, err := generateRandomString("")
require.NoError(t, err)
imageName := "image-" + testId + ".vhd"
// the following line should be done by osbuild-composer at some point
err = azuretest.UploadImageToAzure(creds, imagePath, imageName)
require.NoErrorf(t, err, "upload to azure failed, resources could have been leaked")
// delete the image after the test is over
defer func() {
err = azuretest.DeleteImageFromAzure(creds, imageName)
require.NoErrorf(t, err, "cannot delete the azure image, resources could have been leaked")
}()
// boot the uploaded image and try to connect to it
err = withSSHKeyPair(func(privateKey, publicKey string) error {
return azuretest.WithBootedImageInAzure(creds, imageName, testId, publicKey, func(address string) error {
testSSH(t, address, privateKey, nil)
return nil
})
})
require.NoError(t, err)
}
// testBoot tests if the image is able to successfully boot
// Before the test it boots the image respecting the specified bootType.
// The test passes if the function is able to connect to the image via ssh
@ -281,6 +319,9 @@ func testBoot(t *testing.T, imagePath string, bootType string, outputID string)
case "aws":
testBootUsingAWS(t, imagePath)
case "azure":
testBootUsingAzure(t, imagePath)
default:
panic("unknown boot type!")
}

View file

@ -26,6 +26,7 @@ BuildRequires: systemd
BuildRequires: systemd-rpm-macros
BuildRequires: git
BuildRequires: golang(github.com/aws/aws-sdk-go)
BuildRequires: golang(github.com/Azure/azure-sdk-for-go)
BuildRequires: golang(github.com/Azure/azure-storage-blob-go/azblob)
BuildRequires: golang(github.com/BurntSushi/toml)
BuildRequires: golang(github.com/coreos/go-semver/semver)
@ -113,6 +114,9 @@ install -m 0755 -vp _bin/osbuild-image-tests %{buildroot}%{_libex
install -m 0755 -vp _bin/osbuild-rcm-tests %{buildroot}%{_libexecdir}/tests/osbuild-composer/
install -m 0755 -vp tools/image-info %{buildroot}%{_libexecdir}/osbuild-composer/
install -m 0755 -vd %{buildroot}%{_datadir}/tests/osbuild-composer
install -m 0644 -vp test/azure-deployment-template.json %{buildroot}%{_datadir}/tests/osbuild-composer/
install -m 0755 -vd %{buildroot}%{_datadir}/tests/osbuild-composer/cases
install -m 0644 -vp test/cases/* %{buildroot}%{_datadir}/tests/osbuild-composer/cases/
install -m 0755 -vd %{buildroot}%{_datadir}/tests/osbuild-composer/keyring

View file

@ -30,6 +30,7 @@ BuildRequires: systemd
BuildRequires: systemd-rpm-macros
BuildRequires: git
BuildRequires: golang(github.com/aws/aws-sdk-go)
BuildRequires: golang(github.com/Azure/azure-sdk-for-go)
BuildRequires: golang(github.com/Azure/azure-storage-blob-go/azblob)
BuildRequires: golang(github.com/BurntSushi/toml)
BuildRequires: golang(github.com/coreos/go-semver/semver)
@ -127,6 +128,9 @@ install -m 0755 -vp _bin/osbuild-image-tests %{buildroot}%{_libex
install -m 0755 -vp _bin/osbuild-rcm-tests %{buildroot}%{_libexecdir}/tests/osbuild-composer/
install -m 0755 -vp tools/image-info %{buildroot}%{_libexecdir}/osbuild-composer/
install -m 0755 -vd %{buildroot}%{_datadir}/tests/osbuild-composer
install -m 0644 -vp test/azure-deployment-template.json %{buildroot}%{_datadir}/tests/osbuild-composer/
install -m 0755 -vd %{buildroot}%{_datadir}/tests/osbuild-composer/cases
install -m 0644 -vp test/cases/* %{buildroot}%{_datadir}/tests/osbuild-composer/cases/
install -m 0755 -vd %{buildroot}%{_datadir}/tests/osbuild-composer/keyring

View file

@ -0,0 +1,211 @@
{
"$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"testId": {
"type": "string"
},
"publicIPAddressName": {
"type": "string"
},
"tag": {
"type": "string"
},
"location": {
"type": "string"
},
"imagePath": {
"type": "string"
},
"adminUsername": {
"type": "string"
},
"adminPublicKey": {
"type": "secureString"
}
},
"variables": {
"subnetRef": "[concat(variables('vnetId'), '/subnets/default')]",
"networkInterfaceName": "[concat('iface-', parameters('testId'))]",
"networkSecurityGroupName": "[concat('nsg-', parameters('testId'))]",
"virtualNetworkName": "[concat('vnet-', parameters('testId'))]",
"publicIPAddressName": "[concat('ip-', parameters('testId'))]",
"virtualMachineName": "[concat('vm-', parameters('testId'))]",
"diskName": "[concat('disk-', parameters('testId'))]",
"imageName": "[concat('image-', parameters('testId'))]",
"nsgId": "[resourceId(resourceGroup().name, 'Microsoft.Network/networkSecurityGroups', variables('networkSecurityGroupName'))]",
"vnetId": "[resourceId(resourceGroup().name,'Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]"
},
"resources": [
{
"name": "[variables('networkInterfaceName')]",
"type": "Microsoft.Network/networkInterfaces",
"apiVersion": "2019-07-01",
"location": "[parameters('location')]",
"dependsOn": [
"[concat('Microsoft.Network/networkSecurityGroups/', variables('networkSecurityGroupName'))]",
"[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]",
"[concat('Microsoft.Network/publicIpAddresses/', parameters('publicIPAddressName'))]"
],
"properties": {
"ipConfigurations": [
{
"name": "ipconfig1",
"properties": {
"subnet": {
"id": "[variables('subnetRef')]"
},
"privateIPAllocationMethod": "Dynamic",
"publicIpAddress": {
"id": "[resourceId(resourceGroup().name, 'Microsoft.Network/publicIpAddresses', parameters('publicIPAddressName'))]"
}
}
}
],
"networkSecurityGroup": {
"id": "[variables('nsgId')]"
}
},
"tags": {
"osbuild-composer-image-test": "[parameters('tag')]"
}
},
{
"name": "[variables('networkSecurityGroupName')]",
"type": "Microsoft.Network/networkSecurityGroups",
"apiVersion": "2019-02-01",
"location": "[parameters('location')]",
"properties": {
"securityRules": [
{
"name": "SSH",
"properties": {
"priority": 300,
"protocol": "TCP",
"access": "Allow",
"direction": "Inbound",
"sourceAddressPrefix": "*",
"sourcePortRange": "*",
"destinationAddressPrefix": "*",
"destinationPortRange": "22"
}
}
]
},
"tags": {
"osbuild-composer-image-test": "[parameters('tag')]"
}
},
{
"name": "[variables('virtualNetworkName')]",
"type": "Microsoft.Network/virtualNetworks",
"apiVersion": "2019-09-01",
"location": "[parameters('location')]",
"properties": {
"addressSpace": {
"addressPrefixes": [
"10.0.5.0/24"
]
},
"subnets": [
{
"name": "default",
"properties": {
"addressPrefix": "10.0.5.0/24"
}
}
]
},
"tags": {
"osbuild-composer-image-test": "[parameters('tag')]"
}
},
{
"name": "[parameters('publicIPAddressName')]",
"type": "Microsoft.Network/publicIpAddresses",
"apiVersion": "2019-02-01",
"location": "[parameters('location')]",
"properties": {
"publicIpAllocationMethod": "Dynamic"
},
"sku": {
"name": "Basic"
},
"tags": {
"osbuild-composer-image-test": "[parameters('tag')]"
}
},
{
"name": "[variables('imageName')]",
"type": "Microsoft.Compute/images",
"apiVersion": "2019-07-01",
"location": "[parameters('location')]",
"properties": {
"hyperVGeneration": "V1",
"storageProfile": {
"osDisk": {
"osType": "Linux",
"blobUri": "[parameters('imagePath')]",
"osState": "Generalized"
}
}
},
"tags": {
"osbuild-composer-image-test": "[parameters('tag')]"
}
},
{
"name": "[variables('virtualMachineName')]",
"type": "Microsoft.Compute/virtualMachines",
"apiVersion": "2019-07-01",
"location": "[parameters('location')]",
"dependsOn": [
"[concat('Microsoft.Network/networkInterfaces/', variables('networkInterfaceName'))]",
"[concat('Microsoft.Compute/images/', variables('imageName'))]"
],
"properties": {
"hardwareProfile": {
"vmSize": "Standard_B1s"
},
"storageProfile": {
"imageReference": {
"id": "[resourceId(resourceGroup().name, 'Microsoft.Compute/images', variables('imageName'))]"
},
"osDisk": {
"caching": "ReadWrite",
"managedDisk": {
"storageAccountType": "Standard_LRS"
},
"name": "[variables('diskName')]",
"createOption": "FromImage"
}
},
"networkProfile": {
"networkInterfaces": [
{
"id": "[resourceId('Microsoft.Network/networkInterfaces', variables('networkInterfaceName'))]"
}
]
},
"osProfile": {
"computerName": "[variables('virtualMachineName')]",
"adminUsername": "[parameters('adminUsername')]",
"linuxConfiguration": {
"disablePasswordAuthentication": true,
"ssh": {
"publicKeys": [
{
"path": "[concat('/home/', parameters('adminUsername'), '/.ssh/authorized_keys')]",
"keyData": "[parameters('adminPublicKey')]"
}
]
}
}
}
},
"tags": {
"osbuild-composer-image-test": "[parameters('tag')]"
}
}
]
}

View file

@ -1,6 +1,6 @@
{
"boot": {
"type": "qemu"
"type": "azure"
},
"compose-request": {
"distro": "fedora-30",
@ -7427,4 +7427,4 @@
],
"timezone": "UTC"
}
}
}

View file

@ -1,6 +1,6 @@
{
"boot": {
"type": "qemu"
"type": "azure"
},
"compose-request": {
"distro": "fedora-31",
@ -9051,4 +9051,4 @@
"waagent.service"
]
}
}
}

View file

@ -159,7 +159,7 @@
},
"vhd": {
"boot": {
"type": "qemu"
"type": "azure"
},
"compose-request": {
"distro": "",