Skip to content

Commit 4d65098

Browse files
committed
platform/azure: support additional data disks
Add support for attaching additional data disks to instances created in Azure. Disks are defined through the machines options as the Size in GB and the 'sku', or storage type, e.g. '["100G:sku=UltraSSD_LRS"]' for NVMe disks.
1 parent 701df4a commit 4d65098

File tree

6 files changed

+188
-26
lines changed

6 files changed

+188
-26
lines changed

mantle/cmd/kola/kola.go

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -300,13 +300,14 @@ func writeProps() error {
300300
InstanceType string `json:"type"`
301301
}
302302
type Azure struct {
303-
DiskURI string `json:"diskUri"`
304-
Publisher string `json:"publisher"`
305-
Offer string `json:"offer"`
306-
Sku string `json:"sku"`
307-
Version string `json:"version"`
308-
Location string `json:"location"`
309-
Size string `json:"size"`
303+
DiskURI string `json:"diskUri"`
304+
Publisher string `json:"publisher"`
305+
Offer string `json:"offer"`
306+
Sku string `json:"sku"`
307+
Version string `json:"version"`
308+
Location string `json:"location"`
309+
Size string `json:"size"`
310+
AvailabilityZone string `json:"availability_zone"`
310311
}
311312
type DO struct {
312313
Region string `json:"region"`
@@ -355,13 +356,14 @@ func writeProps() error {
355356
InstanceType: kola.AWSOptions.InstanceType,
356357
},
357358
Azure: Azure{
358-
DiskURI: kola.AzureOptions.DiskURI,
359-
Publisher: kola.AzureOptions.Publisher,
360-
Offer: kola.AzureOptions.Offer,
361-
Sku: kola.AzureOptions.Sku,
362-
Version: kola.AzureOptions.Version,
363-
Location: kola.AzureOptions.Location,
364-
Size: kola.AzureOptions.Size,
359+
DiskURI: kola.AzureOptions.DiskURI,
360+
Publisher: kola.AzureOptions.Publisher,
361+
Offer: kola.AzureOptions.Offer,
362+
Sku: kola.AzureOptions.Sku,
363+
Version: kola.AzureOptions.Version,
364+
Location: kola.AzureOptions.Location,
365+
Size: kola.AzureOptions.Size,
366+
AvailabilityZone: kola.AzureOptions.AvailabilityZone,
365367
},
366368
DO: DO{
367369
Region: kola.DOOptions.Region,

mantle/cmd/kola/options.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ func init() {
100100
sv(&kola.AzureOptions.Version, "azure-version", "", "Azure image version")
101101
sv(&kola.AzureOptions.Location, "azure-location", "westus", "Azure location (default \"westus\"")
102102
sv(&kola.AzureOptions.Size, "azure-size", "Standard_D2_v2", "Azure machine size (default \"Standard_D2_v2\")")
103+
sv(&kola.AzureOptions.AvailabilityZone, "azure-availability-zone", "1", "Azure Availability Zone (default \"1\")")
103104

104105
// do-specific options
105106
sv(&kola.DOOptions.ConfigPath, "do-config-file", "", "DigitalOcean config file (default \"~/"+auth.DOConfigPath+"\")")

mantle/platform/api/azure/disk.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright 2025 Red Hat
2+
// Copyright 2016 CoreOS, Inc.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package azure
17+
18+
import (
19+
"context"
20+
"fmt"
21+
"strings"
22+
23+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
24+
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute"
25+
26+
"github.com/coreos/coreos-assembler/mantle/util"
27+
)
28+
29+
// CreateDisk provisions a new managed disk in the specified Azure resource group using
30+
// the given name, size (in GiB), and SKU (e.g., Premium_LRS). The disk is created in
31+
// the location and availability zone specified in the API options.
32+
func (a *API) CreateDisk(name, resourceGroup string, sizeGB int32, sku armcompute.DiskStorageAccountTypes) (string, error) {
33+
ctx := context.Background()
34+
poller, err := a.diskClient.BeginCreateOrUpdate(ctx, resourceGroup, name, armcompute.Disk{
35+
Location: &a.opts.Location,
36+
Zones: []*string{&a.opts.AvailabilityZone},
37+
Tags: map[string]*string{
38+
"createdBy": to.Ptr("mantle"),
39+
},
40+
SKU: &armcompute.DiskSKU{
41+
Name: to.Ptr(sku),
42+
},
43+
Properties: &armcompute.DiskProperties{
44+
DiskSizeGB: to.Ptr(sizeGB),
45+
CreationData: &armcompute.CreationData{
46+
CreateOption: to.Ptr(armcompute.DiskCreateOptionEmpty),
47+
},
48+
},
49+
}, nil)
50+
51+
if err != nil {
52+
return "", fmt.Errorf("failed to create azure disk %v", err)
53+
}
54+
55+
diskResponse, err := poller.PollUntilDone(context.Background(), nil)
56+
if err != nil {
57+
return "", err
58+
}
59+
60+
if diskResponse.Disk.ID == nil {
61+
return "", fmt.Errorf("failed to get azure disk id")
62+
}
63+
64+
return *diskResponse.Disk.ID, nil
65+
}
66+
67+
// DeleteDisk deletes a managed disk by name from the specified Azure resource group.
68+
func (a *API) DeleteDisk(name, resourceGroup string) error {
69+
ctx := context.Background()
70+
poller, err := a.diskClient.BeginDelete(ctx, resourceGroup, name, nil)
71+
if err != nil {
72+
return err
73+
}
74+
_, err = poller.PollUntilDone(ctx, nil)
75+
return err
76+
}
77+
78+
// example: ["10G:sku=UltraSSD_LRS"], the default disk type is "Standard_LRS"
79+
func (a *API) ParseDisk(spec string) (int64, armcompute.DiskStorageAccountTypes, error) {
80+
sku := armcompute.DiskStorageAccountTypes(armcompute.DiskStorageAccountTypesStandardLRS)
81+
size, diskmap, err := util.ParseDiskSpec(spec, false)
82+
if err != nil {
83+
return size, sku, fmt.Errorf("failed to parse disk spec %q: %w", spec, err)
84+
}
85+
for key, value := range diskmap {
86+
switch key {
87+
case "sku":
88+
normalizedSku := strings.ToUpper(value)
89+
foundSku := false
90+
for _, validSku := range armcompute.PossibleDiskStorageAccountTypesValues() {
91+
if strings.EqualFold(normalizedSku, string(validSku)) {
92+
sku = validSku
93+
foundSku = true
94+
break
95+
}
96+
}
97+
if !foundSku {
98+
return size, sku, fmt.Errorf("unsupported disk sku: %s", value)
99+
}
100+
default:
101+
return size, sku, fmt.Errorf("invalid key: %s", key)
102+
}
103+
}
104+
return size, sku, nil
105+
}

mantle/platform/api/azure/instance.go

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute"
3131
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork"
3232

33+
"github.com/coreos/coreos-assembler/mantle/platform"
3334
"github.com/coreos/coreos-assembler/mantle/util"
3435
)
3536

@@ -108,6 +109,7 @@ func (a *API) getVMParameters(name, userdata, sshkey, storageAccountURI string,
108109
return armcompute.VirtualMachine{
109110
Name: &name,
110111
Location: &a.opts.Location,
112+
Zones: []*string{&a.opts.AvailabilityZone},
111113
Tags: map[string]*string{
112114
"createdBy": to.Ptr("mantle"),
113115
},
@@ -142,7 +144,7 @@ func (a *API) getVMParameters(name, userdata, sshkey, storageAccountURI string,
142144
}
143145
}
144146

145-
func (a *API) CreateInstance(name, userdata, sshkey, resourceGroup, storageAccount string) (*Machine, error) {
147+
func (a *API) CreateInstance(name, userdata, sshkey, resourceGroup, storageAccount string, opts platform.MachineOptions) (*Machine, error) {
146148
subnet, err := a.getSubnet(resourceGroup)
147149
if err != nil {
148150
return nil, fmt.Errorf("preparing network resources: %v", err)
@@ -206,6 +208,18 @@ func (a *API) CreateInstance(name, userdata, sshkey, resourceGroup, storageAccou
206208
return nil, fmt.Errorf("couldn't get VM ID")
207209
}
208210

211+
for i, spec := range opts.AdditionalDisks {
212+
size, sku, err := a.ParseDisk(spec)
213+
if err != nil {
214+
return nil, fmt.Errorf("error parsing additional disk: %v", err)
215+
}
216+
diskName := util.RandomName(fmt.Sprintf("disk-%d", i))
217+
err = a.AttachDiskToInstance(*vm.Name, resourceGroup, diskName, sku, int32(size), int32(i))
218+
if err != nil {
219+
return nil, fmt.Errorf("failed to attach disk to vm: %v", err)
220+
}
221+
}
222+
209223
publicaddr, privaddr, err := a.GetIPAddresses(*nic.Name, *ip.Name, resourceGroup)
210224
if err != nil {
211225
return nil, err
@@ -272,3 +286,45 @@ func (a *API) GetConsoleOutput(name, resourceGroup, storageAccount string) ([]by
272286

273287
return io.ReadAll(data)
274288
}
289+
290+
func (a *API) AttachDiskToInstance(instanceName, resourceGroup, diskName string, sku armcompute.DiskStorageAccountTypes, sizeGB int32, lun int32) error {
291+
ctx := context.Background()
292+
293+
vm, err := a.getInstance(instanceName, resourceGroup)
294+
if err != nil {
295+
return err
296+
}
297+
298+
diskID, err := a.CreateDisk(diskName, resourceGroup, sizeGB, sku)
299+
if err != nil {
300+
return err
301+
}
302+
303+
newDisk := armcompute.DataDisk{
304+
Lun: to.Ptr(lun),
305+
Name: to.Ptr(diskName),
306+
CreateOption: to.Ptr(armcompute.DiskCreateOptionTypesAttach),
307+
ManagedDisk: &armcompute.ManagedDiskParameters{
308+
ID: to.Ptr(diskID),
309+
},
310+
}
311+
312+
if vm.Properties.StorageProfile.DataDisks == nil {
313+
vm.Properties.StorageProfile.DataDisks = []*armcompute.DataDisk{}
314+
}
315+
vm.Properties.StorageProfile.DataDisks = append(vm.Properties.StorageProfile.DataDisks, &newDisk)
316+
317+
poller, err := a.compClient.BeginUpdate(ctx, resourceGroup, instanceName, armcompute.VirtualMachineUpdate{
318+
Properties: &armcompute.VirtualMachineProperties{
319+
StorageProfile: &armcompute.StorageProfile{
320+
DataDisks: vm.Properties.StorageProfile.DataDisks,
321+
},
322+
},
323+
}, nil)
324+
if err != nil {
325+
return fmt.Errorf("failed to attach disk to VM %s: %v", instanceName, err)
326+
}
327+
328+
_, err = poller.PollUntilDone(ctx, nil)
329+
return err
330+
}

mantle/platform/api/azure/options.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,14 @@ type Options struct {
2525
AzureCredentials string
2626
AzureSubscription string
2727

28-
DiskURI string
29-
Publisher string
30-
Offer string
31-
Sku string
32-
Version string
33-
Size string
34-
Location string
28+
DiskURI string
29+
Publisher string
30+
Offer string
31+
Sku string
32+
Version string
33+
Size string
34+
Location string
35+
AvailabilityZone string
3536

3637
SubscriptionName string
3738
SubscriptionID string

mantle/platform/machine/azure/cluster.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,6 @@ func (ac *cluster) NewMachine(userdata *conf.UserData) (platform.Machine, error)
4646
}
4747

4848
func (ac *cluster) NewMachineWithOptions(userdata *conf.UserData, options platform.MachineOptions) (platform.Machine, error) {
49-
if len(options.AdditionalDisks) > 0 {
50-
return nil, errors.New("platform azure does not yet support additional disks")
51-
}
5249
if options.MultiPathDisk {
5350
return nil, errors.New("platform azure does not support multipathed disks")
5451
}
@@ -69,7 +66,7 @@ func (ac *cluster) NewMachineWithOptions(userdata *conf.UserData, options platfo
6966
return nil, err
7067
}
7168

72-
instance, err := ac.flight.api.CreateInstance(ac.vmname(), conf.String(), ac.sshKey, ac.ResourceGroup, ac.StorageAccount)
69+
instance, err := ac.flight.api.CreateInstance(ac.vmname(), conf.String(), ac.sshKey, ac.ResourceGroup, ac.StorageAccount, options)
7370
if err != nil {
7471
return nil, err
7572
}

0 commit comments

Comments
 (0)