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.")
|
||||
|
||||
|
||||
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):
|
||||
msg_fmt = _("Unable to locate the volume group '%(vg_name)s' for this "
|
||||
"operation.")
|
||||
|
||||
@@ -24,7 +24,6 @@ The PowerVM Nova Compute service runs on the management partition.
|
||||
"""
|
||||
import glob
|
||||
from nova import exception
|
||||
from nova.i18n import _LI
|
||||
from nova.storage import linuxscsi
|
||||
import os
|
||||
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.
|
||||
udid = mapping.backing_storage.udid[-32:]
|
||||
|
||||
LOG.info(_LI("Trying to discover VSCSI disk with UDID %(udid)s on slot "
|
||||
"%(slot)x."), {'udid': udid, 'slot': lslot})
|
||||
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.
|
||||
@@ -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
|
||||
dpath = path.realpath(disks[0])
|
||||
LOG.info(_LI("Discovered VSCSI disk with UDID %(udid)s on slot %(slot)x "
|
||||
"at path %(devname)s."),
|
||||
{'udid': udid, 'slot': lslot, 'devname': dpath})
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -15,13 +15,16 @@
|
||||
# under the License.
|
||||
|
||||
from nova.i18n import _LI, _LW
|
||||
from pypowervm.tasks import scsi_mapper as pvm_smap
|
||||
|
||||
from oslo_log import log as logging
|
||||
from taskflow import task
|
||||
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 mgmt
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
@@ -131,7 +134,7 @@ class CreateDiskForImg(task.Task):
|
||||
"""The Task to create the disk from an image in the storage."""
|
||||
|
||||
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.
|
||||
|
||||
Provides the 'disk_dev_info' for other tasks. Comes from the disk_dvr
|
||||
@@ -208,6 +211,88 @@ class ConnectDisk(task.Task):
|
||||
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):
|
||||
"""The task to create the configuration drive."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user