Skip to content

Commit a721929

Browse files
pmkccopybara-github
authored andcommitted
In addition to regional VM groups support listing all zones of group explicitly.
This is necessary on AWS when using manual zonal subnets to know where to create them. PiperOrigin-RevId: 934477697
1 parent c8b1160 commit a721929

7 files changed

Lines changed: 138 additions & 35 deletions

File tree

perfkitbenchmarker/benchmark_spec.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -823,13 +823,17 @@ def ConstructVirtualMachineGroup(
823823
# If the VM group is managed, create a managed VM group and return.
824824
if is_managed:
825825
managed_vm_group_class = managed_vm_group.GetManagedVmGroupClass(cloud)
826-
# TODO(pclay): support multiple zones:
827-
if FLAGS.zone:
828-
assert len(FLAGS.zone) == 1, 'Managed VM groups only support one zone.'
829-
group_spec.vm_spec.zone = FLAGS.zone[0]
830-
vm_config = self._CreateVirtualMachine(group_spec.vm_spec, os_type, cloud)
826+
zones = FLAGS.zone or [group_spec.vm_spec.zone]
827+
vm_configs = []
828+
# VM groups needs a VM for each zone to create subnets in each zone.
829+
for zone in zones:
830+
spec = copy.copy(group_spec.vm_spec)
831+
spec.zone = zone
832+
vm_config = self._CreateVirtualMachine(spec, os_type, cloud)
833+
vm_config.zone = zone
834+
vm_configs.append(vm_config)
831835
group = managed_vm_group_class(
832-
group_spec, vm_config
836+
group_spec, vm_configs
833837
) # pytype: disable=not-instantiable
834838
self.managed_vm_groups[group_name] = group
835839
# Report resource provisioning times.

perfkitbenchmarker/managed_vm_group.py

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,21 +32,50 @@ def GetManagedVmGroupClass(cloud):
3232

3333

3434
class BaseManagedVmGroup(resource.BaseResource):
35-
"""Base class representing a Managed VM group."""
35+
"""Base class representing a Managed VM group.
36+
37+
Attributes:
38+
name: The name of the managed VM group.
39+
region: The region where the managed VM group is located.
40+
vms: A sequence of all VMs in the managed VM group.
41+
42+
Internal Attributes:
43+
spec: The spec of the managed VM group.
44+
vm_config: A virtual machine instance representing the prototype/template VM
45+
configuration used to clone new VMs in the group. Individual VM operations
46+
like `_Create()` and `_Delete()` are NOT called on this object (creation
47+
and deletion are managed by the VM group at the cloud level), but it
48+
defines the template settings.
49+
zoned_vm_configs: A list of all template VM instances, one for each
50+
configured zone in the group. Used to pre-create and cleanup zone-specific
51+
VM dependencies (like firewalls, networks, subnets) in every candidate
52+
zone.
53+
_vms: A mapping of VM names to active VM objects representing actual
54+
instances running in the cloud.
55+
"""
3656

3757
name: str
58+
region: str
3859

3960
RESOURCE_TYPE = 'BaseManagedVmGroup'
4061
CLOUD = None
4162

4263
def __init__(
4364
self,
4465
spec: vm_group_decoders.VmGroupSpec,
45-
vm_config: virtual_machine.BaseVirtualMachine,
66+
vm_configs: list[virtual_machine.BaseVirtualMachine],
4667
):
4768
super().__init__()
4869
self.spec: vm_group_decoders.VmGroupSpec = spec
49-
self.vm_config = vm_config
70+
assert vm_configs, 'No VM configs provided.'
71+
# `vm_config` is the primary prototype/template VM configuration.
72+
# We do NOT run `_Create` or `_Delete` on it. Instead, we use it to
73+
# copy-clone the target VMs.
74+
self.vm_config = vm_configs[0]
75+
# `zoned_vm_configs` contains a template VM configuration for each zone.
76+
# Used to create/delete dependencies (e.g. networks, firewalls) in all
77+
# zones.
78+
self.zoned_vm_configs = vm_configs
5079
self.vm_config.metadata['in_managed_vm_group'] = True
5180
# When we clone the VM config and rename it, our assumptions about the
5281
# disk names are wrong.
@@ -56,22 +85,37 @@ def __init__(
5685

5786
self.name = self.vm_config.name
5887
self.vm_count = self.spec.vm_count
88+
# `_vms` mapping VM names to active VM objects representing actual instances
89+
# running in the cloud. They are populated by copy-cloning `vm_config` and
90+
# initializing names/zones. Then passed back to the benchmark spec for
91+
# benchmarking.
5992
self._vms: dict[str, virtual_machine.BaseVirtualMachine] = {}
6093
self._deleted_vms: list[virtual_machine.BaseVirtualMachine] = []
6194

62-
self.region: str | None = None
63-
self.zone: str | None = None
95+
self.zones: list[str] = [vm.zone for vm in vm_configs]
6496

6597
self.last_operation_start_time: float | None = None
6698
self.last_ready_time: float | None = None
6799

100+
@property
101+
def is_regional(self) -> bool:
102+
assert self.zones
103+
return len(self.zones) > 1 or self.zones[0] == self.region
104+
105+
@property
106+
def zone(self) -> str | None:
107+
assert self.zones
108+
if self.is_regional:
109+
return None
110+
return self.zones[0]
111+
68112
def GetResourceMetadata(self) -> dict[Any, Any]:
69113
metadata = super().GetResourceMetadata().copy()
70114
vm = (list(self.vms) + self._deleted_vms + [self.vm_config])[0]
71115
metadata.update(vm.GetResourceMetadata())
72116
metadata['region'] = self.region
73-
metadata['zone'] = self.zone
74-
metadata['is_regional'] = self.zone is None
117+
metadata['zones'] = self.zones
118+
metadata['is_regional'] = self.is_regional
75119
return metadata
76120

77121
@property
@@ -80,10 +124,16 @@ def vms(self) -> Sequence[virtual_machine.BaseVirtualMachine]:
80124
return list(self._vms.values())
81125

82126
def _CreateDependencies(self):
83-
self.vm_config._CreateDependencies() # pylint: disable=protected-access
127+
background_tasks.RunThreaded(
128+
lambda vm: vm._CreateDependencies(), # pylint: disable=protected-access
129+
self.zoned_vm_configs,
130+
)
84131

85132
def _DeleteDependencies(self):
86-
self.vm_config._DeleteDependencies() # pylint: disable=protected-access
133+
background_tasks.RunThreaded(
134+
lambda vm: vm._DeleteDependencies(), # pylint: disable=protected-access
135+
self.zoned_vm_configs,
136+
)
87137

88138
@dataclasses.dataclass
89139
class VmReference:

perfkitbenchmarker/providers/aws/aws_auto_scaling_group.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,15 +116,17 @@ class AwsAutoScalingGroup(managed_vm_group.BaseManagedVmGroup):
116116
def __init__(
117117
self,
118118
spec: vm_group_decoders.VmGroupSpec,
119-
vm_config: virtual_machine.BaseVirtualMachine,
119+
vm_configs: list[virtual_machine.BaseVirtualMachine],
120120
):
121-
super().__init__(spec, vm_config)
121+
super().__init__(spec, vm_configs)
122122
self.vm_config: aws_virtual_machine.AwsVirtualMachine = cast(
123123
aws_virtual_machine.AwsVirtualMachine, self.vm_config
124124
)
125+
self.zoned_vm_configs: list[aws_virtual_machine.AwsVirtualMachine] = cast(
126+
list[aws_virtual_machine.AwsVirtualMachine], self.zoned_vm_configs
127+
)
125128
self.name = self.vm_config.name
126129
self.region = self.vm_config.region
127-
self.zone = self.vm_config.zone
128130

129131
self.launch_template = AwsLaunchTemplate(self.vm_config, name=self.name)
130132
self.base_cmd = util.AWS_PREFIX + ['autoscaling', '--region', self.region]
@@ -134,7 +136,9 @@ def _CreateDependencies(self):
134136
self.launch_template.Create()
135137

136138
def _Create(self):
137-
subnets = self.vm_config.network.subnet.id
139+
subnets = []
140+
for vm in self.zoned_vm_configs:
141+
subnets.append(vm.network.subnet.id)
138142
cmd = self.base_cmd + [
139143
'create-auto-scaling-group',
140144
'--auto-scaling-group-name',
@@ -150,7 +154,7 @@ def _Create(self):
150154
'--desired-capacity',
151155
str(self.vm_count),
152156
'--vpc-zone-identifier',
153-
subnets,
157+
','.join(subnets),
154158
]
155159
if self.vm_config.aws_tags:
156160
cmd.append('--tags')

perfkitbenchmarker/providers/azure/azure_vm_scaling_set.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,18 @@ class AzureVmScalingSet(managed_vm_group.BaseManagedVmGroup):
3737
def __init__(
3838
self,
3939
spec: vm_group_decoders.VmGroupSpec,
40-
vm_config: virtual_machine.BaseVirtualMachine,
40+
vm_configs: list[virtual_machine.BaseVirtualMachine],
4141
):
42-
super().__init__(spec, vm_config)
42+
super().__init__(spec, vm_configs)
4343
self.vm_config: azure_virtual_machine.AzureVirtualMachine = cast(
4444
azure_virtual_machine.AzureVirtualMachine, self.vm_config
4545
)
46+
self.zoned_vm_configs: list[azure_virtual_machine.AzureVirtualMachine] = (
47+
cast(
48+
list[azure_virtual_machine.AzureVirtualMachine],
49+
self.zoned_vm_configs,
50+
)
51+
)
4652
# VMSS cannot use preconstructed NICs and IPs.
4753
# AFAICT there is no reason to pre-create them for VMs either.
4854
for resource in self.vm_config.nics + self.vm_config.public_ips:
@@ -92,7 +98,8 @@ def _Create(self):
9298
)
9399

94100
if self.vm_config.availability_zone:
95-
cmd.extend(['--zones', self.vm_config.availability_zone])
101+
zones = [vm.availability_zone for vm in self.zoned_vm_configs]
102+
cmd.extend(['--zones', ','.join(zones)])
96103

97104
if self.vm_config.boot_startup_script:
98105
cmd.extend(['--custom-data', self.vm_config.boot_startup_script])

perfkitbenchmarker/providers/gcp/gce_managed_instance_group.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -107,21 +107,22 @@ class GceManagedInstanceGroup(managed_vm_group.BaseManagedVmGroup):
107107
def __init__(
108108
self,
109109
spec: vm_group_decoders.VmGroupSpec,
110-
vm_config: virtual_machine.BaseVirtualMachine,
110+
vm_configs: list[virtual_machine.BaseVirtualMachine],
111111
):
112-
super().__init__(spec, vm_config)
112+
super().__init__(spec, vm_configs)
113113
self.vm_config: gce_virtual_machine.GceVirtualMachine = cast(
114114
gce_virtual_machine.GceVirtualMachine, self.vm_config
115115
)
116+
self.zoned_vm_configs: list[gce_virtual_machine.GceVirtualMachine] = cast(
117+
list[gce_virtual_machine.GceVirtualMachine], self.zoned_vm_configs
118+
)
116119
self.project = self.vm_config.project
117120
# in theory we could use the users defaults, but this is cleaner.
118-
assert self.vm_config.zone
119-
if util.IsZone(self.vm_config.zone):
120-
self.zone = self.vm_config.zone
121-
self.region = util.GetRegionFromZone(self.zone)
122-
elif util.IsRegion(self.vm_config.zone):
123-
self.region = self.vm_config.zone
124-
self.zone = None
121+
assert self.zones
122+
if util.IsZone(self.zones[0]):
123+
self.region = util.GetRegionFromZone(self.zones[0])
124+
elif util.IsRegion(self.zones[0]):
125+
self.region = self.zones[0]
125126
else:
126127
raise ValueError(
127128
f'Unsupported zone: {self.vm_config.zone}. Must be a zone or region.'
@@ -140,7 +141,7 @@ def _GcloudCmd(self, *args) -> util.GcloudCommand:
140141
self, 'compute', 'instance-groups', 'managed', *args
141142
)
142143
# GcloudCommand does not have support for regional resources.
143-
if not self.zone:
144+
if self.is_regional:
144145
cmd.flags['region'] = self.region
145146
return cmd
146147

@@ -153,6 +154,9 @@ def _Create(self):
153154
'--size',
154155
str(self.vm_count),
155156
)
157+
# --zone and --region are handled by GcloudCommand.
158+
if len(self.zones) > 1:
159+
cmd.args.append('--zones=' + ','.join(self.zones))
156160
# TODO(pclay): Consider beta and --resource-manager-tags for labels.
157161
cmd.Issue()
158162

tests/providers/azure/azure_vm_scaling_set_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def TestMig(
6969
os_type='debian12',
7070
vm_spec={'Azure': {'machine_type': 'Standard_D2s_v5'}},
7171
),
72-
vm_config,
72+
[vm_config],
7373
)
7474

7575
@mock.patch.object(

tests/providers/gcp/gce_managed_instance_group_test.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def TestMig(self, mock_get_network, *_, zone='us-central1-c'):
4545
os_type='debian12',
4646
vm_spec={'GCP': {'machine_type': 'n1-standard-4'}},
4747
),
48-
vm_config,
48+
[vm_config],
4949
)
5050

5151
def testCreate(self, *_):
@@ -72,6 +72,40 @@ def testCreateRegional(self, *_):
7272
self.mock_cmd.all_commands,
7373
)
7474

75+
@mock.patch.object(gcp_utils, 'GetRegionFromZone', return_value='us-central1')
76+
@mock.patch.object(gce_virtual_machine.gce_network.GceFirewall, 'GetFirewall')
77+
@mock.patch.object(gce_virtual_machine.gce_network.GceNetwork, 'GetNetwork')
78+
def testCreateMultiZone(self, mock_get_network, *_):
79+
mock_get_network.return_value.placement_group.name = 'test_placement_group'
80+
vm_configs = [
81+
pkb_common_test_case.TestGceVirtualMachine(
82+
gce_virtual_machine.GceVmSpec(
83+
'test_component',
84+
machine_type='n1-standard-4',
85+
zone=zone,
86+
)
87+
)
88+
for zone in ['us-central1-a', 'us-central1-b']
89+
]
90+
mig = gce_managed_instance_group.GceManagedInstanceGroup(
91+
vm_group_decoders.VmGroupSpec(
92+
'test_component',
93+
cloud='GCP',
94+
os_type='debian12',
95+
vm_spec={'GCP': {'machine_type': 'n1-standard-4'}},
96+
),
97+
vm_configs,
98+
)
99+
mig._Create()
100+
self.assertIn(
101+
'gcloud compute instance-groups managed create pkb-test_run-0'
102+
' --template'
103+
' projects/test_project/regions/us-central1/instanceTemplates/pkb-test_run-0'
104+
' --size 1 --zones=us-central1-a,us-central1-b --format json'
105+
' --project test_project --quiet --region us-central1',
106+
self.mock_cmd.all_commands,
107+
)
108+
75109
# SSH keys
76110
@mock.patch.object(builtins, 'open')
77111
@mock.patch.object(vm_util, 'NamedTemporaryFile')

0 commit comments

Comments
 (0)