Skip to content

Commit 67e3bed

Browse files
committed
roachprod: azure GetVMSpecs implementation
Implementation for previously unimplemented interface method `GetVMSpecs()` for Azure. This will allow for cluster information to be fetched in roachtest via `FetchVMSpecs` Informs #146202 Epic: None Release note: None
1 parent a4e8d27 commit 67e3bed

File tree

5 files changed

+191
-5
lines changed

5 files changed

+191
-5
lines changed

pkg/cmd/roachtest/tests/roachtest.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,18 @@ func registerRoachtest(r registry.Registry) {
8181
monitorFatalTestGlobal(ctx, t, c)
8282
},
8383
})
84+
85+
// Manual test for verifying framework behavior in a test success scenario
86+
r.Add(registry.TestSpec{
87+
Name: "roachtest/manual/success",
88+
Owner: registry.OwnerTestEng,
89+
Cluster: r.MakeClusterSpec(3),
90+
CompatibleClouds: registry.AllClouds,
91+
Suites: registry.ManualOnly,
92+
Run: func(ctx context.Context, t test.Test, c cluster.Cluster) {
93+
t.L().Printf("hello")
94+
},
95+
})
8496
}
8597

8698
// monitorFatalTest will always fail with a node logging a fatal error in a

pkg/roachprod/vm/azure/BUILD.bazel

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ go_library(
4141

4242
go_test(
4343
name = "azure_test",
44-
srcs = ["utils_test.go"],
44+
srcs = [
45+
"ids_test.go",
46+
"utils_test.go",
47+
],
4548
data = glob(["testdata/**"]),
4649
embed = [":azure"],
4750
deps = [

pkg/roachprod/vm/azure/azure.go

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,10 +210,49 @@ func (p *Provider) GetLiveMigrationVMs(
210210
return liveMigrationVMs, nil
211211
}
212212

213+
// GetVMSpecs implements the vm.GetVMSpecs interface method which returns a
214+
// map from VM.Name to a map of VM attributes
213215
func (p *Provider) GetVMSpecs(
214216
l *logger.Logger, vms vm.List,
215217
) (map[string]map[string]interface{}, error) {
216-
return nil, nil
218+
ctx, cancel := context.WithTimeout(context.Background(), p.OperationTimeout)
219+
defer cancel()
220+
sub, err := p.getSubscription(ctx)
221+
if err != nil {
222+
return nil, err
223+
}
224+
client := compute.NewVirtualMachinesClient(sub)
225+
if client.Authorizer, err = p.getAuthorizer(); err != nil {
226+
return nil, err
227+
}
228+
229+
// Extract the spec of all VMs and create a map from VM name to spec.
230+
vmSpecs := make(map[string]map[string]interface{})
231+
for _, vmInstance := range vms {
232+
if vmInstance.ProviderID == "" {
233+
return nil, errors.Errorf("provider id not found for vm: %s", vmInstance.Name)
234+
}
235+
azureVmId, err := parseAzureID(vmInstance.ProviderID)
236+
if err != nil {
237+
return nil, err
238+
}
239+
l.Printf("Getting VM Specs for VM: %s", vmInstance.Name)
240+
azureVm, err := client.Get(ctx, azureVmId.resourceGroup, azureVmId.resourceName, "")
241+
if err != nil {
242+
return nil, errors.Wrapf(err, "failed to get vm information for vm %s", vmInstance.Name)
243+
}
244+
// Marshaling & unmarshalling struct to match interface method return type
245+
rawJSON, err := azureVm.MarshalJSON()
246+
if err != nil {
247+
return nil, errors.Wrapf(err, "failed to marshal vm information for vm %s", vmInstance.Name)
248+
}
249+
var vmSpec map[string]interface{}
250+
if err := json.Unmarshal(rawJSON, &vmSpec); err != nil {
251+
return nil, errors.Wrapf(err, "failed to parse raw json")
252+
}
253+
vmSpecs[vmInstance.Name] = vmSpec
254+
}
255+
return vmSpecs, nil
217256
}
218257

219258
func (p *Provider) CreateVolumeSnapshot(

pkg/roachprod/vm/azure/ids.go

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,35 @@ import (
2020
// https://github.com/Azure/azure-sdk-for-go/issues/3080
2121
// is solved.
2222

23+
// azureIDPattern matches
24+
// /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{namespace}/{type}/{name}[/childType/childName ...]
25+
// If child resource type name pair(s) are not present, the last matching
26+
// group will be empty. Otherwise, the last matching group will contain all
27+
// additional pairs.
28+
//
29+
// Currently, we are not using any Azure Resource IDs that contain child
30+
// resource type name pairs, so we do not save them as a field in azureID. If
31+
// we need to use them in the future we can add a field to azureID and set
32+
// that field in parseAzureID.
33+
// Note: the number of child resource pairs is ambiguous and currently the
34+
// entire remainder of the string gets matched to the last group if a trailing
35+
// '/' is present after resourceName.
2336
var azureIDPattern = regexp.MustCompile(
24-
"/subscriptions/(.+)/resourceGroups/(.+)/providers/(.+?)/(.+?)/(.+)")
37+
`/subscriptions/([^/]+)/resourceGroups/([^/]+)/providers/([^/]+)/([^/]+)/([^/]+)(?:/(.*))?`)
2538

39+
// azureID defines a fully qualified Azure Resource ID which must contain a
40+
// subscription ID, a resourceGroup name, a provider namespace, and at least
41+
// one type and name pair. The first type name pair describes the resourceType
42+
// and resourceName. For examples, reference unit tests.
43+
//
44+
// Additional type name pairs are optional, and if they exist, this Resource ID
45+
// is describing a child resource or sometimes referred to as a subresource.
46+
// This struct does not contain a field for child resources. If a
47+
// child resource ID needs to be parsed, this struct must be extended.
48+
//
49+
// Template:
50+
//
51+
// /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{namespace}/{type}/{name}[/childType/childName ...]
2652
type azureID struct {
2753
provider string
2854
resourceGroup string
@@ -31,14 +57,17 @@ type azureID struct {
3157
subscription string
3258
}
3359

60+
// String does not account for child resources since they are not saved after
61+
// being parsed
3462
func (id azureID) String() string {
35-
return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/%s/%s/%s",
63+
s := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/%s/%s/%s",
3664
id.subscription, id.resourceGroup, id.provider, id.resourceType, id.resourceName)
65+
return s
3766
}
3867

3968
func parseAzureID(id string) (azureID, error) {
4069
parts := azureIDPattern.FindStringSubmatch(id)
41-
if len(parts) == 0 {
70+
if len(parts) != 7 {
4271
return azureID{}, errors.Errorf("could not parse Azure ID %q", id)
4372
}
4473
ret := azureID{

pkg/roachprod/vm/azure/ids_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright 2025 The Cockroach Authors.
2+
//
3+
// Use of this software is governed by the CockroachDB Software License
4+
// included in the /LICENSE file.
5+
6+
package azure
7+
8+
import "testing"
9+
10+
func TestParseAzureID(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
input string
14+
expectedString string
15+
expectErr bool
16+
expected azureID
17+
}{
18+
{
19+
name: "Valid VM",
20+
input: "/subscriptions/1234-abcd/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachines/n01",
21+
expectedString: "/subscriptions/1234-abcd/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachines/n01",
22+
expected: azureID{
23+
subscription: "1234-abcd",
24+
resourceGroup: "my-rg",
25+
provider: "Microsoft.Compute",
26+
resourceType: "virtualMachines",
27+
resourceName: "n01",
28+
},
29+
},
30+
{
31+
name: "Valid NIC",
32+
input: "/subscriptions/1234-abcd/resourceGroups/test-rg/providers/Microsoft.Network/networkInterfaces/test-nic01",
33+
expectedString: "/subscriptions/1234-abcd/resourceGroups/test-rg/providers/Microsoft.Network/networkInterfaces/test-nic01",
34+
expected: azureID{
35+
subscription: "1234-abcd",
36+
resourceGroup: "test-rg",
37+
provider: "Microsoft.Network",
38+
resourceType: "networkInterfaces",
39+
resourceName: "test-nic01",
40+
},
41+
},
42+
{
43+
// Since child resources are not implemented, all we are checking here is
44+
// that the resourceName gets selected out properly
45+
name: "Valid with child resource",
46+
input: "/subscriptions/1111/resourceGroups/rg1/providers/Microsoft.Network/loadBalancers/lb01/frontendIPConfigurations/ipconfig1",
47+
expectedString: "/subscriptions/1111/resourceGroups/rg1/providers/Microsoft.Network/loadBalancers/lb01",
48+
expected: azureID{
49+
subscription: "1111",
50+
resourceGroup: "rg1",
51+
provider: "Microsoft.Network",
52+
resourceType: "loadBalancers",
53+
resourceName: "lb01",
54+
},
55+
},
56+
{
57+
name: "Invalid - missing fields",
58+
input: "/subscriptions/abcd/resourceGroups//providers/Microsoft.Compute/virtualMachines/vm1",
59+
expectErr: true,
60+
},
61+
{
62+
name: "Invalid - not an Azure ID",
63+
input: "/this/is/not/an/azure/id",
64+
expectErr: true,
65+
},
66+
}
67+
68+
for _, tt := range tests {
69+
t.Run(tt.name, func(t *testing.T) {
70+
actual, err := parseAzureID(tt.input)
71+
if tt.expectErr {
72+
if err == nil {
73+
t.Errorf("expected error but got none")
74+
}
75+
return
76+
}
77+
if err != nil {
78+
t.Errorf("unexpected error: %v", err)
79+
return
80+
}
81+
// Compare fields
82+
if actual.subscription != tt.expected.subscription {
83+
t.Errorf("subscription: actual %q, expected %q", actual.subscription, tt.expected.subscription)
84+
}
85+
if actual.resourceGroup != tt.expected.resourceGroup {
86+
t.Errorf("resourceGroup: actual %q, expected %q", actual.resourceGroup, tt.expected.resourceGroup)
87+
}
88+
if actual.provider != tt.expected.provider {
89+
t.Errorf("provider: actual %q, expected %q", actual.provider, tt.expected.provider)
90+
}
91+
if actual.resourceType != tt.expected.resourceType {
92+
t.Errorf("resourceType: actual %q, expected %q", actual.resourceType, tt.expected.resourceType)
93+
}
94+
if actual.resourceName != tt.expected.resourceName {
95+
t.Errorf("resourceName: actual %q, expected %q", actual.resourceName, tt.expected.resourceName)
96+
}
97+
// Ensure String() reconstructs the same input
98+
if actual.String() != tt.expectedString {
99+
t.Errorf("String(): actual %q, expected %q", actual.String(), tt.expectedString)
100+
}
101+
})
102+
}
103+
}

0 commit comments

Comments
 (0)