# Copyright 2015 IBM Corp. # # 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. """Utilities related to the PowerVM management partition. The management partition is a special LPAR that runs the PowerVM REST API service. It itself appears through the REST API as a LogicalPartition of type aixlinux, but with the is_mgmt_partition property set to True. The PowerVM Nova Compute service runs on the management partition. """ import glob from nova import exception from nova import utils import os from os import path from oslo_concurrency import lockutils from oslo_log import log as logging from pypowervm.tasks import partition as pvm_par import retrying from nova_powervm.virt.powervm import exception as npvmex LOG = logging.getLogger(__name__) _MP_UUID = None @lockutils.synchronized("mgmt_lpar_uuid") def mgmt_uuid(adapter): """Returns the management partitions UUID.""" global _MP_UUID if not _MP_UUID: _MP_UUID = pvm_par.get_this_partition(adapter).uuid return _MP_UUID def _tee_as_root(fpath, payload): """Executes 'echo $payload | sudo tee -a $fpath'. :param fpath: The file system path to which to tee. :param payload: The string to pipe to the file. """ utils.execute('tee', '-a', fpath, process_input=payload, run_as_root=True) def discover_vscsi_disk(mapping, scan_timeout=300): """Bring a mapped device into the management partition and find its name. Based on a VSCSIMapping, scan the appropriate virtual SCSI host bus, causing the operating system to discover the mapped device. Find and return the path of the newly-discovered device based on its UDID in the mapping. Note: scanning the bus will cause the operating system to discover *all* devices on that bus. However, this method will only return the path for the specific device from the input mapping, based on its UDID. :param mapping: The pypowervm.wrappers.virtual_io_server.VSCSIMapping representing the mapping of the desired disk to the management partition. :param scan_timeout: The maximum number of seconds after scanning to wait for the specified device to appear. :return: The udev-generated ("/dev/sdX") name of the discovered disk. :raise NoDiskDiscoveryException: If the disk did not appear after the specified timeout. :raise UniqueDiskDiscoveryException: If more than one disk appears with the expected UDID. """ # TODO(IBM): Support for other host platforms. # Calculate the Linux slot number from the client adapter slot number. lslot = 0x30000000 | mapping.client_adapter.lpar_slot_num # We'll match the device ID based on the UDID, which is actually the last # 32 chars of the field we get from PowerVM. udid = mapping.backing_storage.udid[-32:] LOG.debug("Trying to discover VSCSI disk with UDID %(udid)s on slot " "%(slot)x.", {'udid': udid, 'slot': lslot}) # Find the special file to scan the bus, and scan it. # This glob should yield exactly one result, but use the loop just in case. for scanpath in glob.glob( '/sys/bus/vio/devices/%x/host*/scsi_host/host*/scan' % lslot): # echo '- - -' | sudo tee -a /path/to/scan _tee_as_root(scanpath, '- - -') # Now see if our device showed up. If so, we can reliably match it based # on its Linux ID, which ends with the disk's UDID. dpathpat = '/dev/disk/by-id/*%s' % udid # The bus scan is asynchronous. Need to poll, waiting for the device to # spring into existence. Stop when glob finds at least one device, or # after the specified timeout. Sleep 1/4 second between polls. @retrying.retry(retry_on_result=lambda result: not result, wait_fixed=250, stop_max_delay=scan_timeout * 1000) def _poll_for_dev(globpat): return glob.glob(globpat) try: disks = _poll_for_dev(dpathpat) except retrying.RetryError as re: raise npvmex.NoDiskDiscoveryException( bus=lslot, udid=udid, polls=re.last_attempt.attempt_number, timeout=scan_timeout) # If we get here, _poll_for_dev returned a nonempty list. If not exactly # one entry, this is an error. if len(disks) != 1: raise npvmex.UniqueDiskDiscoveryException(path_pattern=dpathpat, count=len(disks)) # The by-id path is a symlink. Resolve to the /dev/sdX path dpath = path.realpath(disks[0]) LOG.debug("Discovered VSCSI disk with UDID %(udid)s on slot %(slot)x at " "path %(devname)s.", {'udid': udid, 'slot': lslot, 'devname': dpath}) return dpath def remove_block_dev(devpath, scan_timeout=10): """Remove a block device from the management partition. This method causes the operating system of the management partition to delete the device special files associated with the specified block device. :param devpath: Any path to the block special file associated with the device to be removed. :param scan_timeout: The maximum number of seconds after scanning to wait for the specified device to disappear. :raise InvalidDevicePath: If the specified device or its 'delete' special file cannot be found. :raise DeviceDeletionException: If the deletion was attempted, but the device special file is still present afterward. """ # Resolve symlinks, if any, to get to the /dev/sdX path devpath = path.realpath(devpath) try: os.stat(devpath) except OSError: raise exception.InvalidDevicePath(path=devpath) devname = devpath.rsplit('/', 1)[-1] delpath = '/sys/block/%s/device/delete' % devname try: os.stat(delpath) except OSError: raise exception.InvalidDevicePath(path=delpath) LOG.debug("Deleting block device %(devpath)s from the management " "partition via special file %(delpath)s.", {'devpath': devpath, 'delpath': delpath}) _tee_as_root(delpath, '1') # The bus scan is asynchronous. Need to poll, waiting for the device to # disappear. Stop when stat raises OSError (dev file not found) - which is # success - or after the specified timeout (which is failure). Sleep 1/4 # second between polls. @retrying.retry(retry_on_result=lambda result: result, wait_fixed=250, stop_max_delay=scan_timeout * 1000) def _poll_for_del(statpath): try: os.stat(statpath) return True except OSError: # Device special file is absent, as expected return False try: _poll_for_del(devpath) except retrying.RetryError as re: # stat just kept returning (dev file continued to exist). raise npvmex.DeviceDeletionException( devpath=devpath, polls=re.last_attempt.attempt_number, timeout=scan_timeout) # Else stat raised - the device disappeared - all done.