This change set adds support for cold migration and resize to a different host. Add methods to the local disk driver to retrieve and validate if the drivers are compatible between the source and destination host. These methods will only be called if the driver supports shared storage. Currently only the SSP driver supports shared storage. Add task flow implemention for migrate_disk_and_power_off. There is a peculiar case when resizing a VM on the same host. PowerVM doesn't allow two VMs to be defined with the same UUID, therefore there is only one VM present on the host and care must be taken during a resize revert not to delete the one and only VM we have. To more easily detect that case and to easily see when a VM is being resized or migrated, we rename the source VM during the resize/migration processes. The renamed VM will either be deleted or renamed back when the resize/migration operation is confirmed or reverted. Change-Id: Ie3afe7de9b6daca2396dbc9e2d1c1537cb85e8d9
677 lines
29 KiB
Python
677 lines
29 KiB
Python
# 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 fixtures
|
|
import mock
|
|
|
|
import copy
|
|
from nova import test
|
|
import pypowervm.adapter as pvm_adp
|
|
import pypowervm.entities as pvm_ent
|
|
from pypowervm.tests import test_fixtures as pvm_fx
|
|
from pypowervm.tests.test_utils import pvmhttp
|
|
from pypowervm.wrappers import cluster as pvm_clust
|
|
from pypowervm.wrappers import storage as pvm_stg
|
|
from pypowervm.wrappers import virtual_io_server as pvm_vios
|
|
|
|
from nova_powervm.tests.virt.powervm import fixtures as fx
|
|
from nova_powervm.virt.powervm.disk import driver as disk_dvr
|
|
from nova_powervm.virt.powervm.disk import ssp
|
|
from nova_powervm.virt.powervm import exception as npvmex
|
|
|
|
|
|
SSP = 'ssp.txt'
|
|
CLUST = 'cluster.txt'
|
|
VIO = 'fake_vios_with_volume_group_data.txt'
|
|
|
|
|
|
class SSPFixture(fixtures.Fixture):
|
|
"""Patch out PyPowerVM SSP EntryWrapper search and refresh."""
|
|
|
|
def __init__(self):
|
|
pass
|
|
|
|
def setUp(self):
|
|
super(SSPFixture, self).setUp()
|
|
self._search_patcher = mock.patch(
|
|
'pypowervm.wrappers.cluster.Cluster.search')
|
|
self.mock_search = self._search_patcher.start()
|
|
self.addCleanup(self._search_patcher.stop)
|
|
|
|
self._clust_refresh_patcher = mock.patch(
|
|
'pypowervm.wrappers.cluster.Cluster.refresh')
|
|
self.mock_clust_refresh = self._clust_refresh_patcher.start()
|
|
self.addCleanup(self._clust_refresh_patcher.stop)
|
|
|
|
self._ssp_refresh_patcher = mock.patch(
|
|
'pypowervm.wrappers.storage.SSP.refresh')
|
|
self.mock_ssp_refresh = self._ssp_refresh_patcher.start()
|
|
self.addCleanup(self._ssp_refresh_patcher.stop)
|
|
|
|
|
|
class TestSSPDiskAdapter(test.TestCase):
|
|
"""Unit Tests for the LocalDisk storage driver."""
|
|
|
|
def setUp(self):
|
|
super(TestSSPDiskAdapter, self).setUp()
|
|
|
|
class Instance(object):
|
|
uuid = fx.FAKE_INST_UUID
|
|
name = 'instance-name'
|
|
|
|
self.instance = Instance()
|
|
|
|
self.apt = self.useFixture(pvm_fx.AdapterFx()).adpt
|
|
|
|
def resp(file_name):
|
|
return pvmhttp.load_pvm_resp(
|
|
file_name, adapter=self.apt).get_response()
|
|
|
|
self.ssp_resp = resp(SSP)
|
|
self.clust_resp = resp(CLUST)
|
|
self.vio = resp(VIO)
|
|
|
|
self.sspfx = self.useFixture(SSPFixture())
|
|
|
|
# For _fetch_cluster() with configured name
|
|
self.mock_search = self.sspfx.mock_search
|
|
# EntryWrapper.search always returns a list of wrappers.
|
|
self.mock_search.return_value = [pvm_clust.Cluster.wrap(
|
|
self.clust_resp)]
|
|
|
|
# For _refresh_cluster()
|
|
self.mock_clust_refresh = self.sspfx.mock_clust_refresh
|
|
self.mock_clust_refresh.return_value = pvm_clust.Cluster.wrap(
|
|
self.clust_resp)
|
|
|
|
# For _fetch_ssp() fresh
|
|
self.apt.read_by_href.return_value = self.ssp_resp
|
|
|
|
# For _fetch_ssp() refresh
|
|
self.mock_ssp_refresh = self.sspfx.mock_ssp_refresh
|
|
self.mock_ssp_refresh.return_value = pvm_stg.SSP.wrap(self.ssp_resp)
|
|
|
|
# By default, assume the config supplied a Cluster name
|
|
self.flags(cluster_name='clust1', group='powervm')
|
|
|
|
def _get_ssp_stor(self):
|
|
ssp_stor = ssp.SSPDiskAdapter(
|
|
{'adapter': self.apt,
|
|
'host_uuid': '67dca605-3923-34da-bd8f-26a378fc817f',
|
|
'mp_uuid': 'mp_uuid'})
|
|
return ssp_stor
|
|
|
|
def _bld_resp(self, status=200, entry_or_list=None):
|
|
"""Build a pypowervm.adapter.Response for mocking Adapter.search/read.
|
|
|
|
:param status: HTTP status of the Response.
|
|
:param entry_or_list: pypowervm.entity.Entry or list thereof. If
|
|
None, the Response has no content. If an Entry,
|
|
the Response looks like it got back <entry/>. If
|
|
a list of Entry, the Response looks like it got
|
|
back <feed/>.
|
|
:return: pypowervm.adapter.Response suitable for mocking
|
|
pypowervm.adapter.Adapter.search or read.
|
|
"""
|
|
resp = pvm_adp.Response('meth', 'path', status, 'reason', {})
|
|
resp.entry = None
|
|
resp.feed = None
|
|
if entry_or_list is None:
|
|
resp.feed = pvm_ent.Feed({}, [])
|
|
else:
|
|
if isinstance(entry_or_list, list):
|
|
resp.feed = pvm_ent.Feed({}, entry_or_list)
|
|
else:
|
|
resp.entry = entry_or_list
|
|
return resp
|
|
|
|
def test_capabilities(self):
|
|
ssp_stor = self._get_ssp_stor()
|
|
# Ensure return shared storage
|
|
self.assertTrue(ssp_stor.capabilities.get('shared_storage'))
|
|
|
|
def test_get_info(self):
|
|
ssp_stor = self._get_ssp_stor()
|
|
expected = {'cluster_name': 'neoclust1',
|
|
'ssp_name': 'neossp1',
|
|
'ssp_uuid': 'e357a79a-7a3d-35b6-8405-55ab6a2d0de7'}
|
|
# Ensure the base method returns empty dict
|
|
self.assertEqual(expected, ssp_stor.get_info())
|
|
|
|
def test_validate(self):
|
|
ssp_stor = self._get_ssp_stor()
|
|
fake_data = {}
|
|
# Ensure returns error message when no data
|
|
self.assertIsNotNone(ssp_stor.validate(fake_data))
|
|
|
|
# Get our own data and it should always match!
|
|
fake_data = ssp_stor.get_info()
|
|
# Ensure returns no error on good data
|
|
self.assertIsNone(ssp_stor.validate(fake_data))
|
|
|
|
def test_init_green_with_config(self):
|
|
"""Bootstrap SSPStorage, testing call to _fetch_cluster.
|
|
|
|
Driver init should search for cluster by name.
|
|
"""
|
|
# Invoke __init__ => _fetch_cluster()
|
|
self._get_ssp_stor()
|
|
# _fetch_cluster() WITH configured name does a search, but not a read.
|
|
# Refresh shouldn't be invoked.
|
|
self.assertEqual(1, self.mock_search.call_count)
|
|
self.assertEqual(0, self.apt.read.call_count)
|
|
self.assertEqual(0, self.mock_clust_refresh.call_count)
|
|
|
|
def test_init_green_no_config(self):
|
|
"""No cluster name specified in config; one cluster on host - ok."""
|
|
self.flags(cluster_name='', group='powervm')
|
|
self.apt.read.return_value = self._bld_resp(
|
|
entry_or_list=[self.clust_resp.entry])
|
|
self._get_ssp_stor()
|
|
# _fetch_cluster() WITHOUT a configured name does a feed GET (read),
|
|
# not a search.
|
|
# Refresh shouldn't be invoked.
|
|
self.assertEqual(0, self.mock_search.call_count)
|
|
self.assertEqual(1, self.apt.read.call_count)
|
|
self.assertEqual(0, self.mock_clust_refresh.call_count)
|
|
self.apt.read.assert_called_with('Cluster')
|
|
|
|
def test_init_ClusterNotFoundByName(self):
|
|
"""Empty feed comes back from search - no cluster by that name."""
|
|
self.mock_search.return_value = []
|
|
self.assertRaises(npvmex.ClusterNotFoundByName, self._get_ssp_stor)
|
|
|
|
def test_init_TooManyClustersFound(self):
|
|
"""Search-by-name returns more than one result."""
|
|
clust1 = pvm_clust.Cluster.bld(None, 'newclust1',
|
|
pvm_stg.PV.bld(None, 'hdisk1'),
|
|
pvm_clust.Node.bld('vios1'))
|
|
clust2 = pvm_clust.Cluster.bld(None, 'newclust2',
|
|
pvm_stg.PV.bld(None, 'hdisk2'),
|
|
pvm_clust.Node.bld(None, 'vios2'))
|
|
self.mock_search.return_value = [clust1.entry, clust2.entry]
|
|
self.assertRaises(npvmex.TooManyClustersFound, self._get_ssp_stor)
|
|
|
|
def test_init_NoConfigNoClusterFound(self):
|
|
"""No cluster name specified in config, no clusters on host."""
|
|
self.flags(cluster_name='', group='powervm')
|
|
self.apt.read.return_value = self._bld_resp(status=204)
|
|
self.assertRaises(npvmex.NoConfigNoClusterFound, self._get_ssp_stor)
|
|
|
|
def test_init_NoConfigTooManyClusters(self):
|
|
"""No SSP name specified in config, more than one SSP on host."""
|
|
self.flags(cluster_name='', group='powervm')
|
|
clust1 = pvm_clust.Cluster.bld(None, 'newclust1',
|
|
pvm_stg.PV.bld(None, 'hdisk1'),
|
|
pvm_clust.Node.bld(None, 'vios1'))
|
|
clust2 = pvm_clust.Cluster.bld(None, 'newclust2',
|
|
pvm_stg.PV.bld(None, 'hdisk2'),
|
|
pvm_clust.Node.bld(None, 'vios2'))
|
|
self.apt.read.return_value = self._bld_resp(
|
|
entry_or_list=[clust1.entry, clust2.entry])
|
|
self.assertRaises(npvmex.NoConfigTooManyClusters, self._get_ssp_stor)
|
|
|
|
def test_refresh_cluster(self):
|
|
"""_refresh_cluster with cached wrapper."""
|
|
# Save original cluster wrapper for later comparison
|
|
orig_clust_wrap = pvm_clust.Cluster.wrap(self.clust_resp)
|
|
# Prime _clust_wrap
|
|
ssp_stor = self._get_ssp_stor()
|
|
# Verify baseline call counts
|
|
self.assertEqual(1, self.mock_search.call_count)
|
|
self.assertEqual(0, self.mock_clust_refresh.call_count)
|
|
clust_wrap = ssp_stor._refresh_cluster()
|
|
# This should call refresh
|
|
self.assertEqual(1, self.mock_search.call_count)
|
|
self.assertEqual(0, self.apt.read.call_count)
|
|
self.assertEqual(1, self.mock_clust_refresh.call_count)
|
|
self.assertEqual(clust_wrap.name, orig_clust_wrap.name)
|
|
|
|
def test_fetch_ssp(self):
|
|
# For later comparison
|
|
orig_ssp_wrap = pvm_stg.SSP.wrap(self.ssp_resp)
|
|
# Verify baseline call counts
|
|
self.assertEqual(0, self.apt.read_by_href.call_count)
|
|
self.assertEqual(0, self.mock_ssp_refresh.call_count)
|
|
# This should prime self._ssp_wrap: calls read_by_href but not refresh.
|
|
ssp_stor = self._get_ssp_stor()
|
|
self.assertEqual(1, self.apt.read_by_href.call_count)
|
|
self.assertEqual(0, self.mock_ssp_refresh.call_count)
|
|
# Accessing the @property will trigger refresh
|
|
ssp_wrap = ssp_stor._ssp
|
|
self.assertEqual(1, self.apt.read_by_href.call_count)
|
|
self.assertEqual(1, self.mock_ssp_refresh.call_count)
|
|
self.assertEqual(ssp_wrap.name, orig_ssp_wrap.name)
|
|
|
|
def test_vios_uuids(self):
|
|
ssp_stor = self._get_ssp_stor()
|
|
vios_uuids = ssp_stor.vios_uuids
|
|
self.assertEqual({'10B06F4B-437D-9C18-CA95-34006424120D',
|
|
'6424120D-CA95-437D-9C18-10B06F4B3400'},
|
|
set(vios_uuids))
|
|
s = set()
|
|
for i in range(1000):
|
|
u = ssp_stor._any_vios_uuid()
|
|
# Make sure we got a good value
|
|
self.assertIn(u, vios_uuids)
|
|
s.add(u)
|
|
# Make sure we hit all the values over 1000 iterations. This isn't
|
|
# guaranteed to work, but the odds of failure should be infinitesimal.
|
|
self.assertEqual(set(vios_uuids), s)
|
|
|
|
# Test VIOSes on other nodes, which won't have uuid or url
|
|
with mock.patch.object(ssp_stor, '_cluster') as mock_clust:
|
|
def mock_node(uuid, uri):
|
|
node = mock.MagicMock()
|
|
node.vios_uuid = uuid
|
|
node.vios_uri = uri
|
|
return node
|
|
uri = ('https://9.1.2.3:12443/rest/api/uom/ManagedSystem/67dca605-'
|
|
'3923-34da-bd8f-26a378fc817f/VirtualIOServer/10B06F4B-437D-'
|
|
'9C18-CA95-34006424120D')
|
|
node1 = mock_node(None, uri)
|
|
node2 = mock_node('2', None)
|
|
# This mock is good and should be returned
|
|
node3 = mock_node('3', uri)
|
|
mock_clust.nodes = [node1, node2, node3]
|
|
self.assertEqual(['3'], ssp_stor.vios_uuids)
|
|
|
|
def test_capacity(self):
|
|
ssp_stor = self._get_ssp_stor()
|
|
self.assertEqual(49.88, ssp_stor.capacity)
|
|
|
|
def test_capacity_used(self):
|
|
ssp_stor = self._get_ssp_stor()
|
|
self.assertEqual((49.88 - 48.98), ssp_stor.capacity_used)
|
|
|
|
@mock.patch('pypowervm.tasks.storage.crt_lu_linked_clone')
|
|
@mock.patch('pypowervm.tasks.storage.upload_new_lu')
|
|
@mock.patch('nova_powervm.virt.powervm.disk.driver.IterableToFileAdapter')
|
|
@mock.patch('nova.image.API')
|
|
def test_create_disk_from_new_image(self, mock_img_api, mock_it2fadp,
|
|
mock_upload_lu, mock_crt_lnk_cln):
|
|
b1G = 1024 * 1024 * 1024
|
|
b2G = 2 * b1G
|
|
ssp_stor = self._get_ssp_stor()
|
|
img = dict(name='image-name', id='image-id', size=b2G)
|
|
|
|
def verify_upload_new_lu(vios_uuid, ssp1, stream, lu_name, f_size):
|
|
self.assertIn(vios_uuid, ssp_stor.vios_uuids)
|
|
self.assertEqual(ssp_stor._ssp_wrap, ssp1)
|
|
# 'image' + '_' + s/-/_/g(image['id']), per _get_image_name
|
|
self.assertEqual('image_image_name', lu_name)
|
|
self.assertEqual(b2G, f_size)
|
|
return 'image_lu', None
|
|
|
|
def verify_create_lu_linked_clone(ssp1, clust1, imglu, lu_name, sz_gb):
|
|
# 'boot_' + sanitize('instance-name') _get_disk_name
|
|
self.assertEqual('boot_instance_name', lu_name)
|
|
self.assertEqual('image_lu', imglu)
|
|
return ssp1, 'new_lu'
|
|
|
|
mock_upload_lu.side_effect = verify_upload_new_lu
|
|
mock_crt_lnk_cln.side_effect = verify_create_lu_linked_clone
|
|
lu = ssp_stor.create_disk_from_image(None, self.instance, img, 1)
|
|
self.assertEqual('new_lu', lu)
|
|
|
|
@mock.patch('pypowervm.tasks.storage.crt_lu_linked_clone')
|
|
@mock.patch('nova_powervm.virt.powervm.disk.driver.IterableToFileAdapter')
|
|
@mock.patch('nova.image.API')
|
|
def test_create_disk_from_existing_image(self, mock_img_api, mock_it2fadp,
|
|
mock_crt_lnk_cln):
|
|
b1G = 1024 * 1024 * 1024
|
|
b2G = 2 * b1G
|
|
ssp_stor = self._get_ssp_stor()
|
|
img = dict(name='image-name', id='image-id', size=b2G)
|
|
# Mock the 'existing' image LU
|
|
img_lu = pvm_stg.LU.bld(None, 'image_image_name', 123,
|
|
typ=pvm_stg.LUType.IMAGE)
|
|
ssp_stor._ssp_wrap.logical_units.append(img_lu)
|
|
|
|
class Instance(object):
|
|
uuid = 'instance-uuid'
|
|
name = 'instance-name'
|
|
|
|
def verify_create_lu_linked_clone(ssp1, clust1, imglu, lu_name, sz_gb):
|
|
# 'boot_' + sanitize('instance-name') per _get_disk_name
|
|
self.assertEqual('boot_instance_name', lu_name)
|
|
self.assertEqual(img_lu, imglu)
|
|
return ssp1, 'new_lu'
|
|
|
|
mock_crt_lnk_cln.side_effect = verify_create_lu_linked_clone
|
|
lu = ssp_stor.create_disk_from_image(None, Instance(), img, 1)
|
|
self.assertEqual('new_lu', lu)
|
|
|
|
def test_find_lu(self):
|
|
# Bad path, lu not found, None returned
|
|
ssp = self._get_ssp_stor()
|
|
lu = ssp._find_lu('not_found_name', pvm_stg.LUType.DISK)
|
|
self.assertIsNone(lu)
|
|
|
|
# Good path, found correct name and type
|
|
lu_name = 'neolu1'
|
|
lu = ssp._find_lu(lu_name, pvm_stg.LUType.DISK)
|
|
self.assertIsNotNone(lu)
|
|
self.assertEqual(lu_name, lu.name)
|
|
self.assertEqual(pvm_stg.LUType.DISK, lu.lu_type)
|
|
|
|
def test_get_disk_ref(self):
|
|
ssp = self._get_ssp_stor()
|
|
with mock.patch.object(ssp, '_find_lu', return_value='foundit'):
|
|
lu = ssp.get_disk_ref(self.instance, disk_dvr.DiskType.BOOT)
|
|
self.assertEqual('foundit', lu)
|
|
|
|
# Assert handles not finding it.
|
|
with mock.patch.object(ssp, '_find_lu', return_value=None):
|
|
lu = ssp.get_disk_ref(self.instance, disk_dvr.DiskType.BOOT)
|
|
self.assertIsNone(lu)
|
|
|
|
@mock.patch('nova_powervm.virt.powervm.disk.ssp.SSPDiskAdapter.'
|
|
'vios_uuids')
|
|
@mock.patch('pypowervm.tasks.scsi_mapper.build_vscsi_mapping')
|
|
@mock.patch('pypowervm.tasks.scsi_mapper.add_map')
|
|
@mock.patch('nova_powervm.virt.powervm.vios.get_active_vioses')
|
|
def test_connect_disk(self, mock_active_vioses, mock_add_map,
|
|
mock_build_map, mock_vio_uuids):
|
|
# vio is a single-entry response. Wrap it and put it in a list
|
|
# to act as the feed for FeedTaskFx and FeedTask.
|
|
feed = [pvm_vios.VIOS.wrap(self.vio)]
|
|
mock_active_vioses.return_value = feed
|
|
ft_fx = pvm_fx.FeedTaskFx(feed)
|
|
self.useFixture(ft_fx)
|
|
|
|
# The mock return values
|
|
mock_add_map.return_value = True
|
|
mock_build_map.return_value = 'fake_map'
|
|
|
|
# Need the driver to return the actual UUID of the VIOS in the feed,
|
|
# to match the FeedTask.
|
|
ssp = self._get_ssp_stor()
|
|
ssp.vios_uuids = [feed[0].uuid]
|
|
inst = mock.Mock(uuid=fx.FAKE_INST_UUID)
|
|
|
|
# As initialized above, remove_maps returns True to trigger update.
|
|
ssp.connect_disk(mock.MagicMock(), inst, mock.MagicMock(),
|
|
stg_ftsk=None)
|
|
self.assertEqual(1, mock_add_map.call_count)
|
|
mock_add_map.assert_called_once_with(feed[0], 'fake_map')
|
|
self.assertEqual(1, ft_fx.patchers['update'].mock.call_count)
|
|
|
|
@mock.patch('nova_powervm.virt.powervm.disk.ssp.SSPDiskAdapter.'
|
|
'vios_uuids')
|
|
@mock.patch('pypowervm.tasks.scsi_mapper.build_vscsi_mapping')
|
|
@mock.patch('pypowervm.tasks.scsi_mapper.add_map')
|
|
@mock.patch('nova_powervm.virt.powervm.vios.get_active_vioses')
|
|
def test_connect_disk_no_update(self, mock_active_vioses, mock_add_map,
|
|
mock_build_map, mock_vio_uuids):
|
|
# vio is a single-entry response. Wrap it and put it in a list
|
|
# to act as the feed for FeedTaskFx and FeedTask.
|
|
feed = [pvm_vios.VIOS.wrap(self.vio)]
|
|
mock_active_vioses.return_value = feed
|
|
ft_fx = pvm_fx.FeedTaskFx(feed)
|
|
self.useFixture(ft_fx)
|
|
|
|
# The mock return values
|
|
mock_add_map.return_value = None
|
|
mock_build_map.return_value = 'fake_map'
|
|
|
|
# Need the driver to return the actual UUID of the VIOS in the feed,
|
|
# to match the FeedTask.
|
|
ssp = self._get_ssp_stor()
|
|
ssp.vios_uuids = [feed[0].uuid]
|
|
inst = mock.Mock(uuid=fx.FAKE_INST_UUID)
|
|
|
|
# As initialized above, remove_maps returns True to trigger update.
|
|
ssp.connect_disk(mock.MagicMock(), inst, mock.MagicMock(),
|
|
stg_ftsk=None)
|
|
self.assertEqual(1, mock_add_map.call_count)
|
|
mock_add_map.assert_called_once_with(feed[0], 'fake_map')
|
|
self.assertEqual(0, ft_fx.patchers['update'].mock.call_count)
|
|
|
|
def test_delete_disks(self):
|
|
def _mk_img_lu(idx):
|
|
lu = pvm_stg.LU.bld(None, 'img_lu%d' % idx, 123,
|
|
typ=pvm_stg.LUType.IMAGE)
|
|
lu._udid('xxabc123%d' % idx)
|
|
return lu
|
|
|
|
def _mk_dsk_lu(idx, cloned_from_idx):
|
|
lu = pvm_stg.LU.bld(None, 'dsk_lu%d' % idx, 123,
|
|
typ=pvm_stg.LUType.DISK)
|
|
lu._udid('xxabc123%d' % idx)
|
|
lu._cloned_from_udid('yyabc123%d' % cloned_from_idx)
|
|
return lu
|
|
|
|
# We should be ignoring the return value from update - but it still
|
|
# needs to be wrappable
|
|
self.apt.update_by_path.return_value = pvm_stg.SSP.bld(
|
|
self.apt, 'ssp', []).entry
|
|
ssp_stor = self._get_ssp_stor()
|
|
ssp1 = ssp_stor._ssp_wrap
|
|
# Seed the SSP with three clones backed to two images:
|
|
# img_lu1 => dsk_lu3, dsk_lu4
|
|
img_lu1 = _mk_img_lu(1)
|
|
dsk_lu3 = _mk_dsk_lu(3, 1)
|
|
dsk_lu4 = _mk_dsk_lu(4, 1)
|
|
# img_lu2 => dsk_lu5
|
|
img_lu2 = _mk_img_lu(2)
|
|
dsk_lu5 = _mk_dsk_lu(5, 2)
|
|
ssp1.logical_units = [img_lu1, img_lu2, dsk_lu3, dsk_lu4, dsk_lu5]
|
|
# We'll delete dsk_lu3 and dsk_lu5. We expect img_lu2 to vanish too.
|
|
ssp_stor.delete_disks(None, None, [dsk_lu3, dsk_lu5])
|
|
self.assertSetEqual(
|
|
{(lu.name, lu.udid) for lu in (img_lu1, dsk_lu4)},
|
|
set([(lu.name, lu.udid) for lu in ssp1.logical_units]))
|
|
# Update should have been called only once.
|
|
self.assertEqual(1, self.apt.update_by_path.call_count)
|
|
|
|
@mock.patch('nova_powervm.virt.powervm.disk.ssp.SSPDiskAdapter.'
|
|
'vios_uuids')
|
|
@mock.patch('pypowervm.tasks.scsi_mapper.find_maps')
|
|
@mock.patch('pypowervm.tasks.scsi_mapper.remove_maps')
|
|
@mock.patch('pypowervm.tasks.scsi_mapper.build_vscsi_mapping')
|
|
@mock.patch('nova_powervm.virt.powervm.vios.get_active_vioses')
|
|
def test_disconnect_disk(self, mock_active_vioses, mock_build_map,
|
|
mock_remove_maps, mock_find_maps, mock_vio_uuids):
|
|
# vio is a single-entry response. Wrap it and put it in a list
|
|
# to act as the feed for FeedTaskFx and FeedTask.
|
|
feed = [pvm_vios.VIOS.wrap(self.vio)]
|
|
ft_fx = pvm_fx.FeedTaskFx(feed)
|
|
mock_active_vioses.return_value = feed
|
|
self.useFixture(ft_fx)
|
|
|
|
# The mock return values
|
|
mock_build_map.return_value = 'fake_map'
|
|
|
|
# Need the driver to return the actual UUID of the VIOS in the feed,
|
|
# to match the FeedTask.
|
|
ssp = self._get_ssp_stor()
|
|
ssp.vios_uuids = [feed[0].uuid]
|
|
|
|
# Make the LU's to remove
|
|
def mklu(udid):
|
|
lu = pvm_stg.LU.bld(None, 'lu_%s' % udid, 1)
|
|
lu._udid('27%s' % udid)
|
|
return lu
|
|
|
|
lu1 = mklu('abc')
|
|
lu2 = mklu('def')
|
|
|
|
def remove_resp(vios_w, client_lpar_id, match_func=None,
|
|
include_orphans=False):
|
|
return [mock.Mock(backing_storage=lu1),
|
|
mock.Mock(backing_storage=lu2)]
|
|
|
|
mock_remove_maps.side_effect = remove_resp
|
|
mock_find_maps.side_effect = remove_resp
|
|
|
|
# As initialized above, remove_maps returns True to trigger update.
|
|
lu_list = ssp.disconnect_image_disk(mock.Mock(), self.instance,
|
|
stg_ftsk=None)
|
|
self.assertEqual({lu1, lu2}, set(lu_list))
|
|
self.assertEqual(1, mock_remove_maps.call_count)
|
|
self.assertEqual(1, ft_fx.patchers['update'].mock.call_count)
|
|
|
|
def test_shared_stg_calls(self):
|
|
|
|
# Check the good paths
|
|
ssp_stor = self._get_ssp_stor()
|
|
data = ssp_stor.check_instance_shared_storage_local('context', 'inst')
|
|
self.assertTrue(
|
|
ssp_stor.check_instance_shared_storage_remote('context', data))
|
|
ssp_stor.check_instance_shared_storage_cleanup('context', data)
|
|
|
|
# Check bad paths...
|
|
# No data
|
|
self.assertFalse(
|
|
ssp_stor.check_instance_shared_storage_remote('context', None))
|
|
# Unexpected data format
|
|
self.assertFalse(
|
|
ssp_stor.check_instance_shared_storage_remote('context', 'bad'))
|
|
# Good data, but not the same SSP uuid
|
|
not_same = {'ssp_uuid': 'uuid value not the same'}
|
|
self.assertFalse(
|
|
ssp_stor.check_instance_shared_storage_remote('context', not_same))
|
|
|
|
def _bld_mocks_for_instance_disk(self):
|
|
inst = mock.Mock()
|
|
inst.name = 'my-instance-name'
|
|
lpar_wrap = mock.Mock()
|
|
lpar_wrap.id = 4
|
|
# Build mock VIOS Wrappers as the returns from VIOS.wrap.
|
|
# vios1 and vios2 will both have the mapping for client ID 4 and LU
|
|
# named boot_my_instance_name.
|
|
vios1 = pvm_vios.VIOS.wrap(pvmhttp.load_pvm_resp(
|
|
'fake_vios_ssp_npiv.txt', adapter=self.apt).get_response())
|
|
vios1.scsi_mappings[3].backing_storage._name('boot_my_instance_name')
|
|
resp1 = self._bld_resp(entry_or_list=vios1.entry)
|
|
vios2 = copy.deepcopy(vios1)
|
|
# Change name and UUID so we can tell the difference:
|
|
vios2.name = 'vios2'
|
|
vios2.entry.properties['id'] = '7C6475B3-73A5-B9FF-4799-B23B20292951'
|
|
resp2 = self._bld_resp(entry_or_list=vios2.entry)
|
|
# vios3 will not have the mapping
|
|
vios3 = pvm_vios.VIOS.wrap(pvmhttp.load_pvm_resp(
|
|
'fake_vios_ssp_npiv.txt', adapter=self.apt).get_response())
|
|
vios3.name = 'vios3'
|
|
vios3.entry.properties['id'] = 'B9FF4799-B23B-2029-2951-7C6475B373A5'
|
|
resp3 = self._bld_resp(entry_or_list=vios3.entry)
|
|
return inst, lpar_wrap, resp1, resp2, resp3
|
|
|
|
def test_instance_disk_iter(self):
|
|
def assert_read_calls(num):
|
|
self.assertEqual(num, self.apt.read.call_count)
|
|
self.apt.read.assert_has_calls(
|
|
[mock.call(pvm_vios.VIOS.schema_type, root_id=mock.ANY,
|
|
xag=[pvm_vios.VIOS.xags.SCSI_MAPPING])
|
|
for i in range(num)])
|
|
ssp_stor = self._get_ssp_stor()
|
|
inst, lpar_wrap, rsp1, rsp2, rsp3 = self._bld_mocks_for_instance_disk()
|
|
|
|
# Test with two VIOSes, both of which contain the mapping
|
|
self.apt.read.side_effect = [rsp1, rsp2]
|
|
count = 0
|
|
for lu, vios in ssp_stor.instance_disk_iter(inst, lpar_wrap=lpar_wrap):
|
|
count += 1
|
|
self.assertEqual('274d7bb790666211e3bc1a00006cae8b01ac18997ab9bc23'
|
|
'fb24756e9713a93f90', lu.udid)
|
|
self.assertEqual('vios1_181.68' if count == 1 else 'vios2',
|
|
vios.name)
|
|
self.assertEqual(2, count)
|
|
assert_read_calls(2)
|
|
|
|
# Same, but prove that breaking out of the loop early avoids the second
|
|
# Adapter.read call
|
|
self.apt.reset_mock()
|
|
self.apt.read.side_effect = [rsp1, rsp2]
|
|
for lu, vios in ssp_stor.instance_disk_iter(inst, lpar_wrap=lpar_wrap):
|
|
self.assertEqual('274d7bb790666211e3bc1a00006cae8b01ac18997ab9bc23'
|
|
'fb24756e9713a93f90', lu.udid)
|
|
self.assertEqual('vios1_181.68', vios.name)
|
|
break
|
|
self.assertEqual(1, self.apt.read.call_count)
|
|
assert_read_calls(1)
|
|
|
|
# Now the first VIOS doesn't have the mapping, but the second does
|
|
self.apt.reset_mock()
|
|
self.apt.read.side_effect = [rsp3, rsp2]
|
|
count = 0
|
|
for lu, vios in ssp_stor.instance_disk_iter(inst, lpar_wrap=lpar_wrap):
|
|
count += 1
|
|
self.assertEqual('274d7bb790666211e3bc1a00006cae8b01ac18997ab9bc23'
|
|
'fb24756e9713a93f90', lu.udid)
|
|
self.assertEqual('vios2', vios.name)
|
|
self.assertEqual(1, count)
|
|
assert_read_calls(2)
|
|
|
|
# No hits
|
|
self.apt.reset_mock()
|
|
self.apt.read.side_effect = [rsp3, rsp3]
|
|
for lu, vios in ssp_stor.instance_disk_iter(inst, lpar_wrap=lpar_wrap):
|
|
self.fail()
|
|
assert_read_calls(2)
|
|
|
|
@mock.patch('nova_powervm.virt.powervm.vm.get_instance_wrapper')
|
|
@mock.patch('pypowervm.tasks.scsi_mapper.add_vscsi_mapping')
|
|
def test_connect_instance_disk_to_mgmt(self, mock_add, mock_lw):
|
|
ssp_stor = self._get_ssp_stor()
|
|
inst, lpar_wrap, rsp1, rsp2, rsp3 = self._bld_mocks_for_instance_disk()
|
|
mock_lw.return_value = lpar_wrap
|
|
|
|
# Test with two VIOSes, both of which contain the mapping
|
|
self.apt.read.side_effect = [rsp1, rsp2]
|
|
lu, vios = ssp_stor.connect_instance_disk_to_mgmt(inst)
|
|
self.assertEqual('274d7bb790666211e3bc1a00006cae8b01ac18997ab9bc23'
|
|
'fb24756e9713a93f90', lu.udid)
|
|
# Should hit on the first VIOS
|
|
self.assertIs(rsp1.entry, vios.entry)
|
|
self.assertEqual(1, mock_add.call_count)
|
|
mock_add.assert_called_with('67dca605-3923-34da-bd8f-26a378fc817f',
|
|
vios, 'mp_uuid', lu)
|
|
|
|
# Now the first VIOS doesn't have the mapping, but the second does
|
|
mock_add.reset_mock()
|
|
self.apt.read.side_effect = [rsp3, rsp2]
|
|
lu, vios = ssp_stor.connect_instance_disk_to_mgmt(inst)
|
|
self.assertEqual('274d7bb790666211e3bc1a00006cae8b01ac18997ab9bc23'
|
|
'fb24756e9713a93f90', lu.udid)
|
|
# Should hit on the second VIOS
|
|
self.assertIs(rsp2.entry, vios.entry)
|
|
self.assertEqual(1, mock_add.call_count)
|
|
mock_add.assert_called_with('67dca605-3923-34da-bd8f-26a378fc817f',
|
|
vios, 'mp_uuid', lu)
|
|
|
|
# No hits
|
|
mock_add.reset_mock()
|
|
self.apt.read.side_effect = [rsp3, rsp3]
|
|
self.assertRaises(npvmex.InstanceDiskMappingFailed,
|
|
ssp_stor.connect_instance_disk_to_mgmt, inst)
|
|
self.assertEqual(0, mock_add.call_count)
|
|
|
|
# First add_vscsi_mapping call raises
|
|
self.apt.read.side_effect = [rsp1, rsp2]
|
|
mock_add.side_effect = [Exception("mapping failed"), None]
|
|
# Should hit on the second VIOS
|
|
self.assertIs(rsp2.entry, vios.entry)
|
|
|
|
@mock.patch('pypowervm.tasks.scsi_mapper.remove_lu_mapping')
|
|
def test_disconnect_disk_from_mgmt(self, mock_rm_lu_map):
|
|
ssp_stor = self._get_ssp_stor()
|
|
ssp_stor.disconnect_disk_from_mgmt('vios_uuid', 'disk_name')
|
|
mock_rm_lu_map.assert_called_with(ssp_stor.adapter, 'vios_uuid',
|
|
'mp_uuid', disk_names=['disk_name'])
|