Task for connecting and discovering instance disk
New nova_powervm.virt.powervm.tasks.storage.InstanceDiskToMgmt Task class encompassing a) connecting an instance's boot disk to the management partition, and b) discovering that disk and figuring out its device path. Change-Id: I77670ff970325781a69d96e24d5e69a23b6c195b
This commit is contained in:
109
nova_powervm/tests/virt/powervm/tasks/test_storage.py
Normal file
109
nova_powervm/tests/virt/powervm/tasks/test_storage.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from nova import test
|
||||||
|
|
||||||
|
from nova_powervm.virt.powervm import exception as npvmex
|
||||||
|
from nova_powervm.virt.powervm.tasks import storage as tf_stg
|
||||||
|
|
||||||
|
|
||||||
|
class TestStorage(test.TestCase):
|
||||||
|
|
||||||
|
@mock.patch('pypowervm.tasks.scsi_mapper.find_maps')
|
||||||
|
@mock.patch('nova_powervm.virt.powervm.mgmt.discover_vscsi_disk')
|
||||||
|
@mock.patch('nova_powervm.virt.powervm.mgmt.remove_block_dev')
|
||||||
|
def test_instance_disk_to_mgmt(self, mock_rm, mock_discover, mock_find):
|
||||||
|
mock_discover.return_value = '/dev/disk'
|
||||||
|
mock_instance = mock.Mock()
|
||||||
|
mock_instance.name = 'instance_name'
|
||||||
|
mock_stg = mock.Mock()
|
||||||
|
mock_stg.name = 'stg_name'
|
||||||
|
mock_vwrap = mock.Mock()
|
||||||
|
mock_vwrap.name = 'vios_name'
|
||||||
|
mock_vwrap.uuid = 'vios_uuid'
|
||||||
|
mock_vwrap.scsi_mappings = ['mapping1']
|
||||||
|
|
||||||
|
def verify_connect(inst):
|
||||||
|
self.assertEqual(mock_instance, inst)
|
||||||
|
return mock_stg, mock_vwrap
|
||||||
|
|
||||||
|
def verify_disconnect(vios_uuid, stg_name):
|
||||||
|
self.assertEqual('vios_uuid', vios_uuid)
|
||||||
|
self.assertEqual('stg_name', stg_name)
|
||||||
|
|
||||||
|
disk_dvr = mock.MagicMock()
|
||||||
|
disk_dvr.mp_uuid = 'mp_uuid'
|
||||||
|
disk_dvr.connect_instance_disk_to_mgmt = verify_connect
|
||||||
|
disk_dvr.disconnect_disk_from_mgmt = verify_disconnect
|
||||||
|
|
||||||
|
# Good path - find_maps returns one result
|
||||||
|
mock_find.return_value = ['one_mapping']
|
||||||
|
tf = tf_stg.InstanceDiskToMgmt(disk_dvr, mock_instance)
|
||||||
|
self.assertEqual('connect_and_discover_instance_disk_to_mgmt', tf.name)
|
||||||
|
self.assertEqual((mock_stg, mock_vwrap, '/dev/disk'), tf.execute())
|
||||||
|
mock_find.assert_called_with(['mapping1'], 'mp_uuid',
|
||||||
|
stg_elem=mock_stg)
|
||||||
|
mock_discover.assert_called_with('one_mapping')
|
||||||
|
tf.revert('result', 'failures')
|
||||||
|
mock_rm.assert_called_with('/dev/disk')
|
||||||
|
|
||||||
|
# Good path - find_maps returns >1 result
|
||||||
|
mock_find.reset_mock()
|
||||||
|
mock_discover.reset_mock()
|
||||||
|
mock_rm.reset_mock()
|
||||||
|
mock_find.return_value = ['first_mapping', 'second_mapping']
|
||||||
|
tf = tf_stg.InstanceDiskToMgmt(disk_dvr, mock_instance)
|
||||||
|
self.assertEqual((mock_stg, mock_vwrap, '/dev/disk'), tf.execute())
|
||||||
|
mock_find.assert_called_with(['mapping1'], 'mp_uuid',
|
||||||
|
stg_elem=mock_stg)
|
||||||
|
mock_discover.assert_called_with('first_mapping')
|
||||||
|
tf.revert('result', 'failures')
|
||||||
|
mock_rm.assert_called_with('/dev/disk')
|
||||||
|
|
||||||
|
# Bad path - find_maps returns no results
|
||||||
|
mock_find.reset_mock()
|
||||||
|
mock_discover.reset_mock()
|
||||||
|
mock_rm.reset_mock()
|
||||||
|
mock_find.return_value = []
|
||||||
|
tf = tf_stg.InstanceDiskToMgmt(disk_dvr, mock_instance)
|
||||||
|
self.assertRaises(npvmex.NewMgmtMappingNotFoundException, tf.execute)
|
||||||
|
# find_maps was still called
|
||||||
|
mock_find.assert_called_with(['mapping1'], 'mp_uuid',
|
||||||
|
stg_elem=mock_stg)
|
||||||
|
# discover_vscsi_disk didn't get called
|
||||||
|
self.assertEqual(0, mock_discover.call_count)
|
||||||
|
tf.revert('result', 'failures')
|
||||||
|
# disconnect_disk_from_mgmt got called (still checked by
|
||||||
|
# verify_disconnect above), but remove_block_dev did not.
|
||||||
|
self.assertEqual(0, mock_rm.call_count)
|
||||||
|
|
||||||
|
# Bad path - connect raises
|
||||||
|
mock_find.reset_mock()
|
||||||
|
mock_discover.reset_mock()
|
||||||
|
mock_rm.reset_mock()
|
||||||
|
disk_dvr.connect_instance_disk_to_mgmt = mock.Mock(
|
||||||
|
side_effect=npvmex.InstanceDiskMappingFailed(
|
||||||
|
instance_name='inst_name'))
|
||||||
|
tf = tf_stg.InstanceDiskToMgmt(disk_dvr, mock_instance)
|
||||||
|
self.assertRaises(npvmex.InstanceDiskMappingFailed, tf.execute)
|
||||||
|
self.assertEqual(0, mock_find.call_count)
|
||||||
|
self.assertEqual(0, mock_discover.call_count)
|
||||||
|
# revert shouldn't call disconnect or remove
|
||||||
|
disk_dvr.disconnect_disk_from_mgmt = mock.Mock(side_effect=self.fail)
|
||||||
|
tf.revert('result', 'failures')
|
||||||
|
self.assertEqual(0, mock_rm.call_count)
|
||||||
@@ -70,6 +70,13 @@ class InstanceDiskMappingFailed(AbstractDiskException):
|
|||||||
"the management partition from any Virtual I/O Server.")
|
"the management partition from any Virtual I/O Server.")
|
||||||
|
|
||||||
|
|
||||||
|
class NewMgmtMappingNotFoundException(nex.NovaException):
|
||||||
|
"""Just created a mapping to the mgmt partition, but can't find it."""
|
||||||
|
msg_fmt = _("Failed to find newly-created mapping of storage element "
|
||||||
|
"%(stg_name)s from Virtual I/O Server %(vios_name)s to the "
|
||||||
|
"management partition.")
|
||||||
|
|
||||||
|
|
||||||
class VGNotFound(AbstractDiskException):
|
class VGNotFound(AbstractDiskException):
|
||||||
msg_fmt = _("Unable to locate the volume group '%(vg_name)s' for this "
|
msg_fmt = _("Unable to locate the volume group '%(vg_name)s' for this "
|
||||||
"operation.")
|
"operation.")
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ The PowerVM Nova Compute service runs on the management partition.
|
|||||||
"""
|
"""
|
||||||
import glob
|
import glob
|
||||||
from nova import exception
|
from nova import exception
|
||||||
from nova.i18n import _LI
|
|
||||||
from nova.storage import linuxscsi
|
from nova.storage import linuxscsi
|
||||||
import os
|
import os
|
||||||
from os import path
|
from os import path
|
||||||
@@ -80,8 +79,8 @@ def discover_vscsi_disk(mapping, scan_timeout=10):
|
|||||||
# 32 chars of the field we get from PowerVM.
|
# 32 chars of the field we get from PowerVM.
|
||||||
udid = mapping.backing_storage.udid[-32:]
|
udid = mapping.backing_storage.udid[-32:]
|
||||||
|
|
||||||
LOG.info(_LI("Trying to discover VSCSI disk with UDID %(udid)s on slot "
|
LOG.debug("Trying to discover VSCSI disk with UDID %(udid)s on slot "
|
||||||
"%(slot)x."), {'udid': udid, 'slot': lslot})
|
"%(slot)x.", {'udid': udid, 'slot': lslot})
|
||||||
|
|
||||||
# Find the special file to scan the bus, and scan it.
|
# 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.
|
# This glob should yield exactly one result, but use the loop just in case.
|
||||||
@@ -115,9 +114,9 @@ def discover_vscsi_disk(mapping, scan_timeout=10):
|
|||||||
|
|
||||||
# The by-id path is a symlink. Resolve to the /dev/sdX path
|
# The by-id path is a symlink. Resolve to the /dev/sdX path
|
||||||
dpath = path.realpath(disks[0])
|
dpath = path.realpath(disks[0])
|
||||||
LOG.info(_LI("Discovered VSCSI disk with UDID %(udid)s on slot %(slot)x "
|
LOG.debug("Discovered VSCSI disk with UDID %(udid)s on slot %(slot)x at "
|
||||||
"at path %(devname)s."),
|
"path %(devname)s.",
|
||||||
{'udid': udid, 'slot': lslot, 'devname': dpath})
|
{'udid': udid, 'slot': lslot, 'devname': dpath})
|
||||||
return dpath
|
return dpath
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,13 +15,16 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
from nova.i18n import _LI, _LW
|
from nova.i18n import _LI, _LW
|
||||||
|
from pypowervm.tasks import scsi_mapper as pvm_smap
|
||||||
|
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from taskflow import task
|
from taskflow import task
|
||||||
from taskflow.types import failure as task_fail
|
from taskflow.types import failure as task_fail
|
||||||
|
|
||||||
from nova_powervm.virt.powervm.disk import driver as disk_dvr
|
from nova_powervm.virt.powervm.disk import driver as disk_driver
|
||||||
|
from nova_powervm.virt.powervm import exception as npvmex
|
||||||
from nova_powervm.virt.powervm import media
|
from nova_powervm.virt.powervm import media
|
||||||
|
from nova_powervm.virt.powervm import mgmt
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -131,7 +134,7 @@ class CreateDiskForImg(task.Task):
|
|||||||
"""The Task to create the disk from an image in the storage."""
|
"""The Task to create the disk from an image in the storage."""
|
||||||
|
|
||||||
def __init__(self, disk_dvr, context, instance, image_meta, disk_size=0,
|
def __init__(self, disk_dvr, context, instance, image_meta, disk_size=0,
|
||||||
image_type=disk_dvr.DiskType.BOOT):
|
image_type=disk_driver.DiskType.BOOT):
|
||||||
"""Create the Task.
|
"""Create the Task.
|
||||||
|
|
||||||
Provides the 'disk_dev_info' for other tasks. Comes from the disk_dvr
|
Provides the 'disk_dev_info' for other tasks. Comes from the disk_dvr
|
||||||
@@ -208,6 +211,88 @@ class ConnectDisk(task.Task):
|
|||||||
lpar_wrap.uuid)
|
lpar_wrap.uuid)
|
||||||
|
|
||||||
|
|
||||||
|
class InstanceDiskToMgmt(task.Task):
|
||||||
|
"""Connect an instance's disk to the management partition, discover it.
|
||||||
|
|
||||||
|
We do these two pieces together because their reversion doesn't happen in
|
||||||
|
the opposite order.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, disk_dvr, instance):
|
||||||
|
"""Create the Task for connecting boot disk to mgmt partition.
|
||||||
|
|
||||||
|
Provides:
|
||||||
|
stg_elem: The storage element wrapper (pypowervm LU, PV, etc.) that was
|
||||||
|
connected.
|
||||||
|
vios_wrap: The Virtual I/O Server wrapper
|
||||||
|
(pypowervm.wrappers.virtual_io_server.VIOS) from which the
|
||||||
|
storage element was mapped.
|
||||||
|
disk_path: The local path to the mapped-and-discovered device, e.g.
|
||||||
|
'/dev/sde'
|
||||||
|
|
||||||
|
:param disk_dvr: The disk driver.
|
||||||
|
:param instance: The nova instance whose boot disk is to be connected.
|
||||||
|
"""
|
||||||
|
super(InstanceDiskToMgmt, self).__init__(
|
||||||
|
name='connect_and_discover_instance_disk_to_mgmt',
|
||||||
|
provides=['stg_elem', 'vios_wrap', 'disk_path'])
|
||||||
|
self.disk_dvr = disk_dvr
|
||||||
|
self.instance = instance
|
||||||
|
self.stg_elem = None
|
||||||
|
self.vios_wrap = None
|
||||||
|
self.disk_path = None
|
||||||
|
|
||||||
|
def execute(self):
|
||||||
|
"""Map the instance's boot disk and discover it."""
|
||||||
|
LOG.info(_LI("Mapping boot disk of instance %(instance_name)s to "
|
||||||
|
"management partition."),
|
||||||
|
{'instance_name': self.instance.name})
|
||||||
|
self.stg_elem, self.vios_wrap = (
|
||||||
|
self.disk_dvr.connect_instance_disk_to_mgmt(self.instance))
|
||||||
|
new_maps = pvm_smap.find_maps(
|
||||||
|
self.vios_wrap.scsi_mappings, self.disk_dvr.mp_uuid,
|
||||||
|
stg_elem=self.stg_elem)
|
||||||
|
if not new_maps:
|
||||||
|
raise npvmex.NewMgmtMappingNotFoundException(
|
||||||
|
stg_name=self.stg_elem.name, vios_name=self.vios_wrap.name)
|
||||||
|
|
||||||
|
# new_maps should be length 1, but even if it's not - i.e. we somehow
|
||||||
|
# matched more than one mapping of the same dev to the management
|
||||||
|
# partition from the same VIOS - it is safe to use the first one.
|
||||||
|
the_map = new_maps[0]
|
||||||
|
# Scan the SCSI bus, discover the disk, find its canonical path.
|
||||||
|
LOG.info(_LI("Discovering device and path for mapping of %(dev_name)s "
|
||||||
|
"on the management partition."),
|
||||||
|
{'dev_name': self.stg_elem.name})
|
||||||
|
self.disk_path = mgmt.discover_vscsi_disk(the_map)
|
||||||
|
return self.stg_elem, self.vios_wrap, self.disk_path
|
||||||
|
|
||||||
|
def revert(self, result, flow_failures):
|
||||||
|
"""Unmap the disk and then remove it from the management partition.
|
||||||
|
|
||||||
|
We use this order to avoid rediscovering the device in case some other
|
||||||
|
thread scans the SCSI bus between when we remove and when we unmap.
|
||||||
|
"""
|
||||||
|
if self.vios_wrap is None or self.stg_elem is None:
|
||||||
|
# We never even got connected - nothing to do
|
||||||
|
return
|
||||||
|
LOG.warn(_LW("Unmapping boot disk %(disk_name)s of instance "
|
||||||
|
"%(instance_name)s from management partition via Virtual "
|
||||||
|
"I/O Server %(vios_name)s."),
|
||||||
|
{'disk_name': self.stg_elem.name,
|
||||||
|
'instance_name': self.instance.name,
|
||||||
|
'vios_name': self.vios_wrap.name})
|
||||||
|
self.disk_dvr.disconnect_disk_from_mgmt(self.vios_wrap.uuid,
|
||||||
|
self.stg_elem.name)
|
||||||
|
|
||||||
|
if self.disk_path is None:
|
||||||
|
# We did not discover the disk - nothing else to do.
|
||||||
|
return
|
||||||
|
LOG.warn(_LW("Removing disk %(disk_path)s from the management "
|
||||||
|
"partition."), {'disk_path': self.disk_path})
|
||||||
|
mgmt.remove_block_dev(self.disk_path)
|
||||||
|
|
||||||
|
|
||||||
class CreateAndConnectCfgDrive(task.Task):
|
class CreateAndConnectCfgDrive(task.Task):
|
||||||
"""The task to create the configuration drive."""
|
"""The task to create the configuration drive."""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user