perfkitbenchmarker/providers/ibmcloud/ibmcloud_virtual_machine.py (379 lines of code) (raw):

# Copyright 2020 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 a IBM Cloud Virtual Machine.""" import base64 import json import logging import os import sys import threading import time from absl import flags from perfkitbenchmarker import disk from perfkitbenchmarker import disk_strategies from perfkitbenchmarker import errors from perfkitbenchmarker import linux_virtual_machine from perfkitbenchmarker import provider_info from perfkitbenchmarker import virtual_machine from perfkitbenchmarker import vm_util from perfkitbenchmarker import windows_virtual_machine from perfkitbenchmarker.providers.ibmcloud import ibm_api as ibm from perfkitbenchmarker.providers.ibmcloud import ibmcloud_disk from perfkitbenchmarker.providers.ibmcloud import ibmcloud_network from perfkitbenchmarker.providers.ibmcloud import util FLAGS = flags.FLAGS _DEFAULT_VOLUME_IOPS = 3000 _WAIT_TIME_DEBIAN = 60 _WAIT_TIME_RHEL = 60 _WAIT_TIME_UBUNTU = 600 class IbmCloudVirtualMachine(virtual_machine.BaseVirtualMachine): """Object representing a IBM Cloud Virtual Machine.""" CLOUD = provider_info.IBMCLOUD IMAGE_NAME_PREFIX = None _lock = threading.Lock() validated_resources_set = set() validated_subnets = 0 def __init__(self, vm_spec: virtual_machine.BaseVmSpec): """Initialize a IBM Cloud virtual machine. Args: vm_spec: virtual_machine.BaseVirtualMachineSpec object of the vm. """ super().__init__(vm_spec) self.user_name = FLAGS.ibmcloud_image_username self.boot_volume_size = FLAGS.ibmcloud_boot_volume_size self.boot_volume_iops = FLAGS.ibmcloud_boot_volume_iops self.volume_iops = _DEFAULT_VOLUME_IOPS if FLAGS.ibmcloud_volume_iops: self.volume_iops = FLAGS.ibmcloud_volume_iops self.volume_profile = FLAGS.ibmcloud_volume_profile self.image = FLAGS.image self.os_data = None self.user_data = None self.vmid = None self.vm_created = False self.vm_deleted = False self.profile = FLAGS.machine_type self.prefix = FLAGS.ibmcloud_prefix self.zone = 'us-south-1' # default self.fip_address = None self.fip_id = None self.network: ibmcloud_network.IbmCloudNetwork self.subnet = FLAGS.ibmcloud_subnet self.subnets = {} self.vpcid = FLAGS.ibmcloud_vpcid self.key = FLAGS.ibmcloud_pub_keyid self.boot_encryption_key = None self.data_encryption_key = None self.extra_vdisks_created = False self.device_paths_detected = set() def _CreateRiasKey(self): """Creates a ibmcloud key from the generated ssh key.""" logging.info('Creating rias key') with open(vm_util.GetPublicKeyPath()) as keyfile: pubkey = keyfile.read() logging.info( 'ssh private key file: %s, public key file: %s', vm_util.GetPrivateKeyPath(), vm_util.GetPublicKeyPath(), ) cmd = ibm.IbmAPICommand(self) cmd.flags['name'] = self.prefix + str(flags.FLAGS.run_uri) + 'key' cmd.flags['pubkey'] = pubkey return cmd.CreateKey() def _Suspend(self): raise NotImplementedError() def _Resume(self): raise NotImplementedError() def _CheckImage(self): """Verifies we have an imageid to use.""" cmd = ibm.IbmAPICommand(self) cmd.flags['image_name'] = self.image or self._GetDefaultImageName() logging.info('Looking up image: %s', cmd.flags['image_name']) self.imageid = cmd.GetImageId() if self.imageid is None: logging.info('Failed to find valid image id') sys.exit(1) else: logging.info('Image id found: %s', self.imageid) @classmethod def _GetDefaultImageName(cls): """Returns the default image name prefx.""" return cls.IMAGE_NAME_PREFIX def _SetupResources(self): """Looks up the resources needed, if not found, creates new.""" logging.info('Checking resources') cmd = ibm.IbmAPICommand(self) cmd.flags.update( {'prefix': self.prefix, 'zone': self.zone, 'items': 'vpcs'} ) self.vpcid = cmd.GetResource() logging.info('Vpc found: %s', self.vpcid) cmd.flags['items'] = 'subnets' self.subnet = cmd.GetResource() logging.info('Subnet found: %s', self.subnet) if not self.vpcid: logging.info('Creating a vpc') self.network.Create() self.vpcid = self.network.vpcid if not self.subnet: logging.info('Creating a subnet') self.network.CreateSubnet(self.vpcid) self.subnet = self.network.subnet if FLAGS.ibmcloud_subnets_extra > 0: # these are always created outside perfkit cmd.flags['prefix'] = ibmcloud_network.SUBNET_SUFFIX_EXTRA self.subnets = cmd.ListSubnetsExtra() logging.info('Extra subnets found: %s', self.subnets) # look up for existing key that matches this run uri cmd.flags['items'] = 'keys' cmd.flags['prefix'] = self.prefix + FLAGS.run_uri self.key = cmd.GetResource() logging.info('Key found: %s', self.key) if self.key is None: cmd.flags['items'] = 'keys' cmd.flags['prefix'] = self.prefix + FLAGS.run_uri self.key = self._CreateRiasKey() if self.key is None: raise errors.Error('IBM Cloud ERROR: Failed to create a rias key') logging.info('Created a new key: %s', self.key) logging.info('Looking up the image: %s', self.imageid) cmd.flags['imageid'] = self.imageid self.os_data = util.GetOsInfo(cmd.ImageShow()) logging.info('Image os: %s', self.os_data) logging.info('Checking resources finished') def _DeleteKey(self): """Deletes the rias key.""" with self._lock: # key is not dependent on vpc, one key is used if self.key not in IbmCloudVirtualMachine.validated_resources_set: time.sleep(5) cmd = ibm.IbmAPICommand(self) cmd.flags['items'] = 'keys' cmd.flags['id'] = self.key cmd.DeleteResource() IbmCloudVirtualMachine.validated_resources_set.add(self.key) def _Create(self): """Creates and starts a IBM Cloud VM instance.""" self._CreateInstance() if self.subnet: # this is for the primary vnic and fip self.fip_address, self.fip_id = self.network.CreateFip( self.name + 'fip', self.vmid ) self.ip_address = self.fip_address self.internal_ip = self._WaitForIPAssignment(self.subnet) logging.info('Fip: %s, ip: %s', self.ip_address, self.internal_ip) if self.subnets: # create the extra vnics cmd = ibm.IbmAPICommand(self) cmd.flags['instanceid'] = self.vmid for subnet_name in self.subnets.keys(): cmd.flags['name'] = subnet_name cmd.flags['subnet'] = self.subnets[subnet_name]['id'] # subnet id logging.info( 'Creating extra vnic for vmid: %s, subnet: %s', self.vmid, cmd.flags['subnet'], ) vnicid, ip_addr = cmd.InstanceVnicCreate() logging.info( 'Extra vnic created for vmid: %s, vnicid: %s, ip_addr: %s', self.vmid, vnicid, ip_addr, ) self.subnets[subnet_name]['vnicid'] = vnicid self.subnets[subnet_name]['ip_addr'] = ip_addr logging.info( 'Extra vnics created for vmid: %s, subnets: %s', self.vmid, self.subnets, ) def _Delete(self): """Delete all the resources that were created.""" if self.vm_deleted: return self._StopInstance() if self.fip_address: self.network.DeleteFip(self.vmid, self.fip_address, self.fip_id) time.sleep(10) self._DeleteInstance() if not FLAGS.ibmcloud_resources_keep: self.network.Delete() self._DeleteKey() def _DeleteDependencies(self): """Delete dependencies.""" pass def _Exists(self): return self.vm_created and not self.vm_deleted def _CreateDependencies(self): """Validate and Create dependencies prior creating the VM.""" self._CheckPrerequisites() def _CheckPrerequisites(self): """Checks prerequisites are met otherwise aborts execution.""" with self._lock: logging.info('Validating prerequisites.') logging.info('zones: %s', FLAGS.zone) if len(FLAGS.zones) > 1: for zone in FLAGS.zone: if zone not in IbmCloudVirtualMachine.validated_resources_set: self.zone = zone break else: self.zone = FLAGS.zone[0] logging.info('zone to use %s', self.zone) self._CheckImage() self.network = ibmcloud_network.IbmCloudNetwork(self.prefix, self.zone) self._SetupResources() IbmCloudVirtualMachine.validated_resources_set.add(self.zone) logging.info('Prerequisites validated.') def _CreateInstance(self): """Creates IBM Cloud VM instance.""" cmd = ibm.IbmAPICommand(self) cmd.flags.update({ 'name': self.name, 'imageid': self.imageid, 'profile': self.profile, 'vpcid': self.vpcid, 'subnet': self.subnet, 'key': self.key, 'zone': self.zone, }) cmd.user_data = self.user_data if self.boot_volume_size > 0: cmd.flags['capacity'] = self.boot_volume_size if self.boot_encryption_key: cmd.flags['encryption_key'] = self.boot_encryption_key logging.info('Creating instance, flags: %s', cmd.flags) resp = json.loads(cmd.CreateInstance()) if 'id' not in resp: raise errors.Error(f'IBM Cloud ERROR: Failed to create instance: {resp}') self.vmid = resp['id'] self.vm_created = True logging.info('Instance created, id: %s', self.vmid) logging.info('Waiting for instance to start, id: %s', self.vmid) cmd.flags['instanceid'] = self.vmid status = cmd.InstanceStatus() assert status == ibm.States.RUNNING if status != ibm.States.RUNNING: logging.error('Instance start failed, status: %s', status) logging.info('Instance %s status %s', self.vmid, status) def _StartInstance(self): """Starts a IBM Cloud VM instance.""" cmd = ibm.IbmAPICommand(self) cmd.flags['instanceid'] = self.vmid status = cmd.InstanceStart() logging.info('start_instance_poll: last status is %s', status) assert status == ibm.States.RUNNING if status != ibm.States.RUNNING: logging.error('Instance start failed, status: %s', status) def _WaitForIPAssignment(self, networkid: str): """Finds the IP address assigned to the vm.""" ip_v4_address = '0.0.0.0' count = 0 while ( ip_v4_address == '0.0.0.0' and count * FLAGS.ibmcloud_polling_delay < 240 ): time.sleep(FLAGS.ibmcloud_polling_delay) count += 1 cmd = ibm.IbmAPICommand(self) cmd.flags['instanceid'] = self.vmid logging.info( 'Looking for IP for instance %s, networkid: %s', self.vmid, networkid ) resp = cmd.InstanceShow() for network in resp['network_interfaces']: if network['subnet']['id'] == networkid: ip_v4_address = network['primary_ip']['address'] break logging.info('Waiting on ip assignment: %s', ip_v4_address) if ip_v4_address == '0.0.0.0': raise ValueError('Failed to retrieve ip address') return ip_v4_address def _StopInstance(self): """Stops a IBM Cloud VM instance.""" cmd = ibm.IbmAPICommand(self) cmd.flags['instanceid'] = self.vmid status = cmd.InstanceStop() logging.info('stop_instance_poll: last status is %s', status) if status != ibm.States.STOPPED: logging.error('Instance stop failed, status: %s', status) def _DeleteInstance(self): """Deletes a IBM Cloud VM instance.""" cmd = ibm.IbmAPICommand(self) cmd.flags['instanceid'] = self.vmid cmd.InstanceDelete() self.vm_deleted = True logging.info('Instance deleted: %s', cmd.flags['instanceid']) def CreateScratchDisk(self, _, disk_spec: disk.BaseDiskSpec): """Create a VM's scratch disk. Args: disk_spec: virtual_machine.BaseDiskSpec object of the disk. """ disks_names = ( '%s-data-%d-%d' % (self.name, len(self.scratch_disks), i) for i in range(disk_spec.num_striped_disks) ) disks = [ ibmcloud_disk.IbmCloudDisk( disk_spec, name, self.zone, encryption_key=self.data_encryption_key ) for name in disks_names ] scratch_disk = self._CreateScratchDiskFromDisks(disk_spec, disks) disk_strategies.PrepareScratchDiskStrategy().PrepareScratchDisk( self, scratch_disk, disk_spec ) def DownloadPreprovisionedData( self, install_path, module_name, filename, timeout=virtual_machine.PREPROVISIONED_DATA_TIMEOUT, ): """Creats a temp file, no download.""" self.RemoteCommand( 'echo "1234567890" > ' + os.path.join(install_path, filename), timeout=timeout, ) def ShouldDownloadPreprovisionedData(self, module_name, filename): """Returns whether or not preprovisioned data is available.""" return False class DebianBasedIbmCloudVirtualMachine( IbmCloudVirtualMachine, linux_virtual_machine.BaseDebianMixin ): def PrepareVMEnvironment(self): time.sleep(_WAIT_TIME_DEBIAN) self.RemoteCommand('DEBIAN_FRONTEND=noninteractive apt-get -y update') self.RemoteCommand('DEBIAN_FRONTEND=noninteractive apt-get -y install sudo') super().PrepareVMEnvironment() class Debian11BasedIbmCloudVirtualMachine( DebianBasedIbmCloudVirtualMachine, linux_virtual_machine.Debian11Mixin ): IMAGE_NAME_PREFIX = 'ibm-debian-11-' class Ubuntu2004BasedIbmCloudVirtualMachine( IbmCloudVirtualMachine, linux_virtual_machine.Ubuntu2004Mixin ): IMAGE_NAME_PREFIX = 'ibm-ubuntu-20-04-' class RhelBasedIbmCloudVirtualMachine( IbmCloudVirtualMachine, linux_virtual_machine.BaseRhelMixin ): def PrepareVMEnvironment(self): time.sleep(_WAIT_TIME_RHEL) super().PrepareVMEnvironment() class Rhel8BasedIbmCloudVirtualMachine( RhelBasedIbmCloudVirtualMachine, linux_virtual_machine.Rhel8Mixin ): IMAGE_NAME_PREFIX = 'ibm-redhat-8-' class WindowsIbmCloudVirtualMachine( IbmCloudVirtualMachine, windows_virtual_machine.BaseWindowsMixin ): """Support for Windows machines on IBMCloud.""" def __init__(self, vm_spec): super().__init__(vm_spec) self.user_name = 'Administrator' self.user_data = util.USER_DATA @vm_util.Retry() def _GetDecodedPasswordData(self): # Retrieve a base64 encoded, encrypted password for the VM. cmd = ibm.IbmAPICommand(self) cmd.flags['instanceid'] = self.vmid resp = cmd.InstanceInitializationShow() logging.info('Instance %s, resp %s', self.vmid, resp) encrypted = None if resp and 'password' in resp and 'encrypted_password' in resp['password']: encrypted = resp['password']['encrypted_password'] if encrypted is None: raise ValueError('Failed to retrieve encrypted password') return base64.b64decode(encrypted) def _PostCreate(self): """Retrieve generic VM info and then retrieve the VM's password.""" super()._PostCreate() # Get the decoded password data. decoded_password_data = self._GetDecodedPasswordData() # Write the encrypted data to a file, and use openssl to # decrypt the password. with vm_util.NamedTemporaryFile() as tf: tf.write(decoded_password_data) tf.close() decrypt_cmd = [ 'openssl', 'rsautl', '-decrypt', '-in', tf.name, '-inkey', vm_util.GetPrivateKeyPath(), ] password, _ = vm_util.IssueRetryableCommand(decrypt_cmd) self.password = password logging.info('Password decrypted for %s, %s', self.fip_address, self.vmid) class Windows2016CoreIbmCloudVirtualMachine( WindowsIbmCloudVirtualMachine, windows_virtual_machine.Windows2016CoreMixin ): IMAGE_NAME_PREFIX = 'ibm-windows-server-2016-full' class Windows2019CoreIbmCloudVirtualMachine( WindowsIbmCloudVirtualMachine, windows_virtual_machine.Windows2019CoreMixin ): IMAGE_NAME_PREFIX = 'ibm-windows-server-2019-full'