perfkitbenchmarker/providers/alicloud/ali_virtual_machine.py (391 lines of code) (raw):

# Copyright 2015 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. """Class to represent an Ali Virtual Machine object. All VM specifics are self-contained and the class provides methods to operate on the VM: boot, shutdown, etc. """ import base64 import json import logging import threading from absl import flags from perfkitbenchmarker import disk from perfkitbenchmarker import linux_virtual_machine from perfkitbenchmarker import provider_info from perfkitbenchmarker import virtual_machine from perfkitbenchmarker import vm_util from perfkitbenchmarker.providers.alicloud import ali_disk from perfkitbenchmarker.providers.alicloud import ali_network from perfkitbenchmarker.providers.alicloud import util import six FLAGS = flags.FLAGS NON_HVM_PREFIXES = ['t1', 's1', 's2', 's3', 'm1'] DRIVE_START_LETTER = 'b' DEFAULT_DISK_SIZE = 500 SSH_PORT = 22 NUM_LOCAL_VOLUMES = { 'ecs.t1.small': 4, 'ecs.s1.small': 4, 'ecs.s1.medium': 4, 'ecs.s2.small': 4, 'ecs.s2.large': 4, 'ecs.s2.xlarge': 4, 'ecs.s3.medium': 4, 'ecs.s3.large': 4, 'ecs.m1.medium': 4, } INSTANCE_EXISTS_STATUSES = frozenset( ['Starting', 'Running', 'Stopping', 'Stopped'] ) INSTANCE_DELETED_STATUSES = frozenset([]) INSTANCE_KNOWN_STATUSES = INSTANCE_EXISTS_STATUSES | INSTANCE_DELETED_STATUSES class AliVirtualMachine(virtual_machine.BaseVirtualMachine): """Object representing an AliCloud Virtual Machine.""" CLOUD = provider_info.ALICLOUD DEFAULT_ZONE = 'us-west-1a' DEFAULT_MACHINE_TYPE = 'ecs.s3.large' _lock = threading.Lock() imported_keyfile_set = set() deleted_keyfile_set = set() def __init__(self, vm_spec): """Initialize a AliCloud virtual machine. Args: vm_spec: virtual_machine.BaseVirtualMachineSpec object of the VM. """ super().__init__(vm_spec) self.image = FLAGS.image self.user_name = FLAGS.ali_user_name self.key_pair_name = None self.region = util.GetRegionByZone(self.zone) self.bandwidth_in = FLAGS.ali_bandwidth_in self.bandwidth_out = FLAGS.ali_bandwidth_out self.scratch_disk_size = FLAGS.data_disk_size or DEFAULT_DISK_SIZE self.system_disk_type = FLAGS.ali_system_disk_type self.system_disk_size = FLAGS.ali_system_disk_size self.eip_address_bandwidth = FLAGS.ali_eip_address_bandwidth self.network = ali_network.AliNetwork.GetNetwork(self) self.firewall = ali_network.AliFirewall.GetFirewall() @vm_util.Retry(poll_interval=1, log_errors=False) def _WaitForInstanceStatus(self, status_list): """Waits until the instance's status is in status_list.""" logging.info( "Waits until the instance's status is one of statuses: %s", status_list ) describe_cmd = util.ALI_PREFIX + [ 'ecs', 'DescribeInstances', '--RegionId %s' % self.region, '--InstanceIds \'["%s"]\'' % self.id, ] describe_cmd = util.GetEncodedCmd(describe_cmd) stdout, _ = vm_util.IssueRetryableCommand(describe_cmd) response = json.loads(stdout) instances = response['Instances']['Instance'] assert len(instances) == 1 status = instances[0]['Status'] assert status in status_list @vm_util.Retry(poll_interval=5, max_retries=30, log_errors=False) def _WaitForEipStatus(self, status_list): """Waits until the instance's status is in status_list.""" logging.info( "Waits until the eip's status is one of statuses: %s", status_list ) describe_cmd = util.ALI_PREFIX + [ 'ecs', 'DescribeEipAddresses', '--RegionId %s' % self.region, '--AllocationId %s' % self.eip_id, ] describe_cmd = util.GetEncodedCmd(describe_cmd) stdout, _ = vm_util.IssueRetryableCommand(describe_cmd) response = json.loads(stdout) EipAddresses = response['EipAddresses']['EipAddress'] assert len(EipAddresses) == 1 status = EipAddresses[0]['Status'] assert status in status_list def _AllocatePubIp(self, region, instance_id): """Allocate a public ip address and associate it to the instance.""" if FLAGS.ali_use_vpc: allocatip_cmd = util.ALI_PREFIX + [ 'ecs', 'AllocateEipAddress', '--RegionId %s' % region, '--InternetChargeType PayByTraffic', '--Bandwidth %s' % self.eip_address_bandwidth, ] allocatip_cmd = util.GetEncodedCmd(allocatip_cmd) stdout, _ = vm_util.IssueRetryableCommand(allocatip_cmd) response = json.loads(stdout) self.ip_address = response['EipAddress'] self.eip_id = response['AllocationId'] self._WaitForInstanceStatus(['Stopped', 'Running']) associate_cmd = util.ALI_PREFIX + [ 'ecs', 'AssociateEipAddress', '--RegionId %s' % region, '--AllocationId %s' % self.eip_id, '--InstanceId %s' % instance_id, '--InstanceType EcsInstance', ] associate_cmd = util.GetEncodedCmd(associate_cmd) vm_util.IssueRetryableCommand(associate_cmd) util.VPCAddDefaultTags(self.eip_id, util.ResourceTypes.EIP, self.region) else: allocatip_cmd = util.ALI_PREFIX + [ 'ecs', 'AllocatePublicIpAddress', '--RegionId %s' % region, '--InstanceId %s' % instance_id, ] allocatip_cmd = util.GetEncodedCmd(allocatip_cmd) stdout, _ = vm_util.IssueRetryableCommand(allocatip_cmd) response = json.loads(stdout) self.ip_address = response['IpAddress'] @classmethod def _GetDefaultImage(cls, region): """Returns the default image given the machine type and region. If no default is configured, this will return None. """ if cls.IMAGE_NAME_FILTER is None: # pytype: disable=attribute-error return None describe_cmd = util.ALI_PREFIX + [ 'ecs', 'DescribeImages', '--RegionId %s' % region, "--ImageName '%s'" % cls.IMAGE_NAME_FILTER, # pytype: disable=attribute-error ] describe_cmd = util.GetEncodedCmd(describe_cmd) stdout, _ = vm_util.IssueRetryableCommand(describe_cmd) if not stdout: return None images = json.loads(stdout)['Images']['Image'] # We want to return the latest version of the image, and since the wildcard # portion of the image name is the image's creation date, we can just take # the image with the 'largest' name. return max(images, key=lambda image: image['ImageName'])['ImageId'] @vm_util.Retry() def _PostCreate(self): """Get the instance's data and tag it.""" describe_cmd = util.ALI_PREFIX + [ 'ecs', 'DescribeInstances', '--RegionId %s' % self.region, '--InstanceIds \'["%s"]\'' % self.id, ] logging.info( 'Getting instance %s public IP. This will fail until ' 'a public IP is available, but will be retried.', self.id, ) describe_cmd = util.GetEncodedCmd(describe_cmd) stdout, _ = vm_util.IssueRetryableCommand(describe_cmd) response = json.loads(stdout) instance = response['Instances']['Instance'][0] if self.network.use_vpc: pub_ip_address = instance['EipAddress']['IpAddress'] self.internal_ip = instance['VpcAttributes']['PrivateIpAddress'][ 'IpAddress' ][0] else: pub_ip_address = instance['PublicIpAddress']['IpAddress'][0] self.internal_ip = instance['InnerIpAddress']['IpAddress'][0] assert self.ip_address == pub_ip_address self.group_id = instance['SecurityGroupIds']['SecurityGroupId'][0] self._WaitForInstanceStatus(['Running']) self.firewall.AllowPort(self, SSH_PORT) tags = {} tags.update(self.vm_metadata) util.AddTags(self.id, util.ResourceTypes.INSTANCE, self.region, **tags) util.AddDefaultTags(self.id, util.ResourceTypes.INSTANCE, self.region) # Find and tag attached disks describe_cmd = util.ALI_PREFIX + [ 'ecs', 'DescribeDisks', '--InstanceId %s' % self.id, ] describe_cmd_encoded = util.GetEncodedCmd(describe_cmd) stdout, _ = vm_util.IssueRetryableCommand(describe_cmd_encoded) response = json.loads(stdout) disks = list(response['Disks']['Disk']) while response['NextToken']: describe_cmd_encoded = util.GetEncodedCmd( describe_cmd + ['--NextToken', response['NextToken']] ) stdout, _ = vm_util.IssueRetryableCommand(describe_cmd_encoded) response = json.loads(stdout) disks.extend(list(response['Disks']['Disk'])) for attached_disk in disks: disk_id = attached_disk['DiskId'] util.AddTags(disk_id, util.ResourceTypes.DISK, self.region, **tags) util.AddDefaultTags(disk_id, util.ResourceTypes.DISK, self.region) # Tag KeyPair util.AddTags( self.key_pair_name, util.ResourceTypes.KEYPAIR, self.region, **tags ) util.AddDefaultTags( self.key_pair_name, util.ResourceTypes.KEYPAIR, self.region ) def _CreateDependencies(self): """Create VM dependencies.""" self.key_pair_name = AliCloudKeyFileManager.ImportKeyfile(self.region) def _DeleteDependencies(self): """Delete VM dependencies.""" if self.key_pair_name: AliCloudKeyFileManager.DeleteKeyfile(self.region, self.key_pair_name) def _Create(self): """Create a VM instance.""" if self.image is None: # This is here and not in the __init__ method bceauese _GetDefaultImage # does a nontrivial amount of work (it calls the aliyuncli). self.image = self._GetDefaultImage(self.region) create_cmd = util.ALI_PREFIX + [ 'ecs', 'CreateInstance', '--InstanceName perfkit-%s' % FLAGS.run_uri, '--RegionId %s' % self.region, '--ZoneId %s' % self.zone, '--ImageId %s' % self.image, '--InstanceType %s' % self.machine_type, '--SecurityGroupId %s' % self.network.security_group.group_id, '--KeyPairName %s' % self.key_pair_name, '--SystemDisk.Category %s' % self.system_disk_type, '--SystemDisk.Size %s' % self.system_disk_size, ] if FLAGS.data_disk_type == disk.LOCAL: disk_cmd = [ '--DataDisk1Category ephemeral_ssd', '--DataDisk1Size %s' % self.scratch_disk_size, '--DataDisk1Device %s%s' % (util.GetDrivePathPrefix(), DRIVE_START_LETTER), ] create_cmd.extend(disk_cmd) if FLAGS.ali_io_optimized is not None: create_cmd.extend(['--IoOptimized optimized']) if FLAGS.ali_use_vpc: create_cmd.extend(['--VSwitchId %s' % self.network.vswitch.id]) else: create_cmd.extend([ '--InternetChargeType PayByTraffic', '--InternetMaxBandwidthIn %s' % self.bandwidth_in, '--InternetMaxBandwidthOut %s' % self.bandwidth_out, ]) # Create user and add SSH key public_key = AliCloudKeyFileManager.GetPublicKey() user_data = util.ADD_USER_TEMPLATE.format( user_name=self.user_name, public_key=public_key ) logging.debug('encoding startup script: %s', user_data) create_cmd.extend([ '--UserData', six.ensure_str(base64.b64encode(user_data.encode('utf-8'))), ]) create_cmd = util.GetEncodedCmd(create_cmd) stdout, _ = vm_util.IssueRetryableCommand(create_cmd) response = json.loads(stdout) self.id = response['InstanceId'] self._AllocatePubIp(self.region, self.id) start_cmd = util.ALI_PREFIX + [ 'ecs', 'StartInstance', '--InstanceId %s' % self.id, ] start_cmd = util.GetEncodedCmd(start_cmd) vm_util.IssueRetryableCommand(start_cmd) def _Delete(self): """Delete a VM instance.""" stop_cmd = util.ALI_PREFIX + [ 'ecs', 'StopInstance', '--InstanceId %s' % self.id, ] stop_cmd = util.GetEncodedCmd(stop_cmd) vm_util.IssueRetryableCommand(stop_cmd) self._WaitForInstanceStatus(['Stopped']) delete_cmd = util.ALI_PREFIX + [ 'ecs', 'DeleteInstance', '--InstanceId %s' % self.id, ] delete_cmd = util.GetEncodedCmd(delete_cmd) vm_util.IssueRetryableCommand(delete_cmd) if FLAGS.ali_use_vpc: self._WaitForEipStatus(['Available']) release_eip_cmd = util.ALI_PREFIX + [ 'ecs', 'ReleaseEipAddress', '--RegionId %s' % self.region, '--AllocationId %s' % self.eip_id, ] release_eip_cmd = util.GetEncodedCmd(release_eip_cmd) vm_util.IssueRetryableCommand(release_eip_cmd) def _Exists(self): """Returns true if the VM exists.""" describe_cmd = util.ALI_PREFIX + [ 'ecs', 'DescribeInstances', '--RegionId %s' % self.region, '--InstanceIds \'["%s"]\'' % str(self.id), ] describe_cmd = util.GetEncodedCmd(describe_cmd) stdout, _ = vm_util.IssueRetryableCommand(describe_cmd) response = json.loads(stdout) instances = response['Instances']['Instance'] assert len(instances) < 2, 'Too many instances.' if not instances: return False assert len(instances) == 1, 'Wrong number of instances.' status = instances[0]['Status'] assert status in INSTANCE_KNOWN_STATUSES, status return status in INSTANCE_EXISTS_STATUSES def CreateScratchDisk(self, disk_spec): """Create a VM's scratch disk. Args: disk_spec: virtual_machine.BaseDiskSpec object of the disk. """ data_disk = ali_disk.AliDisk(disk_spec, self.zone) self.scratch_disks.append(data_disk) if disk_spec.disk_type != disk.LOCAL: data_disk.Create() data_disk.Attach(self) data_disk.WaitForDiskStatus(['In_use']) else: data_disk.device_letter = DRIVE_START_LETTER self.FormatDisk(data_disk.GetDevicePath(), disk_spec.disk_type) # pytype: disable=attribute-error self.MountDisk( data_disk.GetDevicePath(), disk_spec.mount_point, disk_spec.disk_type, data_disk.mount_options, data_disk.fstab_options, ) # pytype: disable=attribute-error def AddMetadata(self, **kwargs): """Adds metadata to the VM.""" util.AddTags(self.id, util.ResourceTypes.INSTANCE, self.region, **kwargs) class AliCloudKeyFileManager: """Object for managing AliCloud Keyfiles.""" _lock = threading.Lock() imported_keyfile_set = set() deleted_keyfile_set = set() run_uri_key_names = {} @classmethod def ImportKeyfile(cls, region): """Imports the public keyfile to AliCloud.""" with cls._lock: if FLAGS.run_uri in cls.run_uri_key_names: return cls.run_uri_key_names[FLAGS.run_uri] public_key = cls.GetPublicKey() key_name = cls.GetKeyNameForRun() import_cmd = util.ALI_PREFIX + [ 'ecs', 'ImportKeyPair', '--RegionId', region, '--KeyPairName', key_name, '--PublicKeyBody', json.dumps(public_key), ] vm_util.IssueRetryableCommand(import_cmd) cls.run_uri_key_names[FLAGS.run_uri] = key_name return key_name @classmethod def DeleteKeyfile(cls, region, key_name): """Deletes the imported KeyPair for a run_uri.""" with cls._lock: if FLAGS.run_uri not in cls.run_uri_key_names: return delete_cmd = util.ALI_PREFIX + [ 'ecs', 'DeleteKeyPairs', '--RegionId', region, '--KeyPairNames', json.dumps([key_name]), ] vm_util.IssueRetryableCommand(delete_cmd) del cls.run_uri_key_names[FLAGS.run_uri] @classmethod def GetKeyNameForRun(cls): return 'perfkit_key_{}'.format(FLAGS.run_uri) @classmethod def GetPublicKey(cls): cat_cmd = ['cat', vm_util.GetPublicKeyPath()] keyfile, _ = vm_util.IssueRetryableCommand(cat_cmd) return keyfile.strip() class Debian113BasedAliVirtualMachine( AliVirtualMachine, linux_virtual_machine.Debian11Mixin ): IMAGE_NAME_FILTER = 'debian_11_3_x64_20G*alibase*.vhd' class Ubuntu2004BasedAliVirtualMachine( AliVirtualMachine, linux_virtual_machine.Ubuntu2004Mixin ): IMAGE_NAME_FILTER = 'ubuntu_20_04_x64*alibase*.vhd' class Ubuntu2204BasedAliVirtualMachine( AliVirtualMachine, linux_virtual_machine.Ubuntu2204Mixin ): IMAGE_NAME_FILTER = 'ubuntu_22_04_x64*alibase*.vhd' class Ubuntu2404BasedAliVirtualMachine( AliVirtualMachine, linux_virtual_machine.Ubuntu2404Mixin ): IMAGE_NAME_FILTER = 'ubuntu_24_04_x64*alibase*.vhd'