perfkitbenchmarker/providers/azure/azure_disk.py (345 lines of code) (raw):

# Copyright 2019 PerfKitBenchmarker Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Module containing classes related to Azure disks. Disks can be created, deleted, attached to VMs, and detached from VMs. At this time, Azure only supports one disk type, so the disk spec's disk type is ignored. See http://msdn.microsoft.com/en-us/library/azure/dn790303.aspx for more information about azure disks. """ import itertools import json import re import threading import time from absl import flags from perfkitbenchmarker import background_tasks from perfkitbenchmarker import disk from perfkitbenchmarker import errors from perfkitbenchmarker import vm_util from perfkitbenchmarker.providers import azure from perfkitbenchmarker.providers.azure import azure_network from perfkitbenchmarker.providers.azure import flags as azure_flags from perfkitbenchmarker.providers.azure import util FLAGS = flags.FLAGS MAX_DRIVE_SUFFIX_LENGTH = 2 # Last allowable device is /dev/sdzz. PREMIUM_STORAGE_V2 = 'PremiumV2_LRS' PREMIUM_STORAGE = 'Premium_LRS' PREMIUM_ZRS = 'Premium_ZRS' STANDARD_SSD_LRS = 'StandardSSD_LRS' STANDARD_SSD_ZRS = 'StandardSSD_ZRS' STANDARD_DISK = 'Standard_LRS' ULTRA_STORAGE = 'UltraSSD_LRS' # https://learn.microsoft.com/en-us/rest/api/compute/disks/list?view=rest-compute-2023-10-02&tabs=HTTP#diskstorageaccounttypes AZURE_REMOTE_DISK_TYPES = [ PREMIUM_STORAGE, PREMIUM_STORAGE_V2, STANDARD_SSD_LRS, STANDARD_SSD_ZRS, STANDARD_DISK, ULTRA_STORAGE, PREMIUM_ZRS, ] HOST_CACHING = 'host_caching' AZURE = 'Azure' AZURE_REPLICATION_MAP = { azure_flags.LRS: disk.ZONE, azure_flags.ZRS: disk.REGION, # Deliberately omitting PLRS, because that is set explicty in __init__, # and (RA)GRS, because those are asynchronously replicated. } LOCAL_SSD_PREFIXES = {'Standard_D', 'Standard_G', 'Standard_L'} AZURE_NVME_TYPES = [ r'(Standard_L[0-9]+s_v2)', r'(Standard_L[0-9]+a?s_v3)', ] # https://docs.microsoft.com/en-us/azure/virtual-machines/azure-vms-no-temp-disk # D/Ev4 and D/E(i)sv4 VMs do not have tmp/OS disk; Dv3, Dsv3, and Ddv4 VMs do. AZURE_NO_TMP_DISK_TYPES = [ r'(Standard_D[0-9]+s?_v4)', r'(Standard_E[0-9]+i?s?_v4)', ] def _ProductWithIncreasingLength(iterable, max_length): """Yields increasing length cartesian products of iterable.""" for length in range(1, max_length + 1): yield from itertools.product(iterable, repeat=length) def _GenerateDrivePathSuffixes(): """Yields drive path suffix strings. Drive path suffixes in the form 'a', 'b', 'c', 'd', ..., 'z', 'aa', 'ab', etc. Note: the os-disk will be /dev/sda, and the temporary disk will be /dev/sdb: https://docs.microsoft.com/en-us/azure/virtual-machines/linux/faq#can-i-use-the-temporary-disk-devsdb1-to-store-data Some newer VMs (e.g. Dsv4 VMs) do not have temporary disks. The linux kernel code that determines this naming can be found here: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/scsi/sd.c?h=v2.6.37#n2262 Quoting the link from above: SCSI disk names starts at sda. The 26th device is sdz and the 27th is sdaa. The last one for two lettered suffix is sdzz which is followed by sdaaa. """ character_range = range(ord('a'), ord('z') + 1) products = _ProductWithIncreasingLength( character_range, MAX_DRIVE_SUFFIX_LENGTH ) for p in products: yield ''.join(chr(c) for c in p) REMOTE_DRIVE_PATH_SUFFIXES = list(_GenerateDrivePathSuffixes()) class TooManyAzureDisksError(Exception): """Exception raised when too many disks are attached.""" pass def LocalDiskIsSSD(machine_type): """Check whether the local disk is an SSD drive.""" return any(machine_type.startswith(prefix) for prefix in LOCAL_SSD_PREFIXES) def LocalDriveIsNvme(machine_type): """Check if the machine type uses NVMe driver.""" return any( re.search(machine_series, machine_type) for machine_series in AZURE_NVME_TYPES ) def HasTempDrive(machine_type): """Check if the machine type has the temp drive (sdb).""" # Only applies to some gen 4 VMs. series_number = util.GetMachineSeriesNumber(machine_type) if series_number > 5: # This method is assuming it's a SCSI drive. # TODO(pclay): Support NVMe temp drives in v6 VMs. return False if series_number == 5: return machine_type.endswith('ds_v5') return not any( re.search(machine_series, machine_type) for machine_series in AZURE_NO_TMP_DISK_TYPES ) class AzureDisk(disk.BaseDisk): """Object representing an Azure Disk.""" _lock = threading.Lock() def __init__(self, disk_spec, vm, lun, is_image=False): super().__init__(disk_spec) self.host_caching = FLAGS.azure_host_caching self.vm = vm self.vm_name = vm.name self.name = self.vm_name + str(lun) self.zone = vm.zone self.availability_zone = vm.availability_zone self.region = util.GetRegionFromZone(self.zone) self.resource_group = azure_network.GetResourceGroup() self.storage_account = vm.storage_account # lun is Azure's abbreviation for "logical unit number" self.lun = lun self.is_image = is_image self._deleted = False self.machine_type = vm.machine_type self.provisioned_iops = disk_spec.provisioned_iops self.provisioned_throughput = disk_spec.provisioned_throughput if ( self.disk_type == PREMIUM_STORAGE or self.disk_type == PREMIUM_STORAGE_V2 or self.disk_type == ULTRA_STORAGE ): self.metadata.update({ disk.MEDIA: disk.SSD, disk.REPLICATION: disk.ZONE, HOST_CACHING: self.host_caching, }) elif self.disk_type == STANDARD_DISK: self.metadata.update({ disk.MEDIA: disk.HDD, disk.REPLICATION: AZURE_REPLICATION_MAP[FLAGS.azure_storage_type], HOST_CACHING: self.host_caching, }) elif self.disk_type == PREMIUM_ZRS: self.metadata.update({ disk.MEDIA: disk.SSD, disk.REPLICATION: disk.REGION, HOST_CACHING: self.host_caching, }) elif self.disk_type == disk.LOCAL: media = disk.SSD if LocalDiskIsSSD(self.machine_type) else disk.HDD self.metadata.update({ disk.MEDIA: media, disk.REPLICATION: disk.NONE, }) def _Create(self): """Creates the disk.""" assert not self.is_image if self.disk_type == ULTRA_STORAGE and not self.vm.availability_zone: raise Exception( f'Azure Ultradisk is being created in zone "{self.zone}"' 'which was not specified to have an availability zone. ' 'Availability zones are specified with zone-\\d e.g. ' 'eastus1-2 for availability zone 2 in zone eastus1' ) with self._lock: if FLAGS.azure_attach_disk_with_create: cmd = [ azure.AZURE_PATH, 'vm', 'disk', 'attach', '--new', '--caching', self.host_caching, '--name', self.name, '--lun', str(self.lun), '--sku', self.disk_type, '--vm-name', self.vm_name, '--size-gb', str(self.disk_size), ] + self.resource_group.args else: cmd = [ azure.AZURE_PATH, 'disk', 'create', '--name', self.name, '--size-gb', str(self.disk_size), '--sku', self.disk_type, '--location', self.region, '--zone', self.availability_zone, ] + self.resource_group.args self.create_disk_start_time = time.time() _, stderr, retcode = vm_util.IssueCommand( cmd, raise_on_failure=False, timeout=600 ) self.create_disk_end_time = time.time() if retcode: raise errors.Resource.RetryableCreationError( f'Error creating Azure disk:\n{stderr}' ) _, _, retcode = vm_util.IssueCommand( [ azure.AZURE_PATH, 'disk', 'update', '--name', self.name, '--set', util.GetTagsJson(self.resource_group.timeout_minutes), ] + self.resource_group.args, raise_on_failure=False, ) if retcode: raise errors.Resource.RetryableCreationError( 'Error tagging Azure disk.' ) if self.disk_type in [ULTRA_STORAGE, PREMIUM_STORAGE_V2] and ( self.provisioned_iops or self.provisioned_throughput ): args = [ azure.AZURE_PATH, 'disk', 'update', '--name', self.name, ] + self.resource_group.args if self.provisioned_iops: args = args + [ '--disk-iops-read-write', str(self.provisioned_iops), ] if self.provisioned_throughput: args = args + [ '--disk-mbps-read-write', str(self.provisioned_throughput), ] _, _, _ = vm_util.IssueCommand(args, raise_on_failure=True) # create disk end time includes disk update command as well def _Delete(self): """Deletes the disk.""" assert not self.is_image self._deleted = True def _Exists(self): """Returns true if the disk exists.""" assert not self.is_image if self._deleted: return False stdout, _, _ = vm_util.IssueCommand( [ azure.AZURE_PATH, 'disk', 'show', '--output', 'json', '--name', self.name, ] + self.resource_group.args, raise_on_failure=False, ) try: json.loads(stdout) return True except: return False def _Attach(self, vm): """Attaches the disk to a VM. Args: vm: The AzureVirtualMachine instance to which the disk will be attached. """ if FLAGS.azure_attach_disk_with_create: return self.attach_start_time = time.time() _, _, retcode = vm_util.IssueCommand( [ azure.AZURE_PATH, 'vm', 'disk', 'attach', '--vm-name', vm.name, '--name', self.name, ] + self.resource_group.args, raise_on_failure=False, timeout=600, ) self.attach_end_time = time.time() if retcode: raise errors.Resource.RetryableCreationError( 'Error attaching Azure disk to VM.' ) def _Detach(self): """Detaches the disk from a VM.""" _, _, retcode = vm_util.IssueCommand( [ azure.AZURE_PATH, 'vm', 'disk', 'detach', '--vm-name', self.vm.name, '--name', self.name, ] + self.resource_group.args, raise_on_failure=False, timeout=600, ) if retcode: raise errors.Resource.RetryableCreationError( 'Error detaching Azure disk from VM.' ) def GetDevicePath(self): """Returns the path to the device inside the VM.""" if self.disk_type == disk.LOCAL: if LocalDriveIsNvme(self.machine_type): return '/dev/nvme%sn1' % str(self.lun) # Temp disk naming isn't always /dev/sdb: # https://github.com/MicrosoftDocs/azure-docs/issues/54055 return '/dev/disk/cloud/azure_resource' else: try: start_index = 1 # the os drive is always at index 0; skip the OS drive. if self.vm.SupportsNVMe(): # boot disk is nvme0n1. temp drive, if exists, uses scsi. return '/dev/nvme0n%s' % str(1 + start_index + self.lun) if HasTempDrive(self.machine_type): start_index += 1 return '/dev/sd%s' % REMOTE_DRIVE_PATH_SUFFIXES[start_index + self.lun] except IndexError: raise TooManyAzureDisksError() def IsNvme(self): if self.disk_type == disk.LOCAL: return LocalDriveIsNvme(self.machine_type) elif self.disk_type in AZURE_REMOTE_DISK_TYPES: return self.vm.SupportsNVMe() else: return False class AzureStripedDisk(disk.StripedDisk): """Object representing multiple azure disks striped together.""" def _Create(self): create_tasks = [] for disk_details in self.disks: create_tasks.append((disk_details.Create, (), {})) background_tasks.RunParallelThreads(create_tasks, max_concurrency=200) def _Attach(self, vm): if FLAGS.azure_attach_disk_with_create: return disk_names = [disk_details.name for disk_details in self.disks] self.attach_start_time = time.time() _, _, retcode = vm_util.IssueCommand( [ azure.AZURE_PATH, 'vm', 'disk', 'attach', '--vm-name', vm.name, '--disks', ] + disk_names + vm.resource_group.args, raise_on_failure=False, timeout=600, ) if retcode: raise errors.Resource.RetryableCreationError( 'Error attaching Multiple Azure disks to VM.' ) self.attach_end_time = time.time() def _Detach(self): for disk_details in self.disks: disk_details.Detach() def GetAttachTime(self): if self.attach_start_time and self.attach_end_time: return self.attach_end_time - self.attach_start_time