Merge "PowerVM Driver: SEA"

This commit is contained in:
Zuul 2018-01-26 07:07:40 +00:00 committed by Gerrit Code Review
commit bf1f835ce7
2 changed files with 270 additions and 34 deletions

View File

@ -59,8 +59,37 @@ class TestVifFunctions(test.NoDBTestCase):
{'type': 'bad_type'})
mock_driver.assert_not_called()
@mock.patch('oslo_serialization.jsonutils.dumps')
@mock.patch('pypowervm.wrappers.event.Event')
def test_push_vif_event(self, mock_event, mock_dumps):
mock_vif = mock.Mock(mac='MAC', href='HREF')
vif._push_vif_event(self.adpt, 'action', mock_vif, mock.Mock(),
'pvm_sea')
mock_dumps.assert_called_once_with(
{'provider': 'NOVA_PVM_VIF', 'action': 'action', 'mac': 'MAC',
'type': 'pvm_sea'})
mock_event.bld.assert_called_once_with(self.adpt, 'HREF',
mock_dumps.return_value)
mock_event.bld.return_value.create.assert_called_once_with()
mock_dumps.reset_mock()
mock_event.bld.reset_mock()
mock_event.bld.return_value.create.reset_mock()
# Exception reraises
mock_event.bld.return_value.create.side_effect = IndexError
self.assertRaises(IndexError, vif._push_vif_event, self.adpt, 'action',
mock_vif, mock.Mock(), 'pvm_sea')
mock_dumps.assert_called_once_with(
{'provider': 'NOVA_PVM_VIF', 'action': 'action', 'mac': 'MAC',
'type': 'pvm_sea'})
mock_event.bld.assert_called_once_with(self.adpt, 'HREF',
mock_dumps.return_value)
mock_event.bld.return_value.create.assert_called_once_with()
@mock.patch('nova.virt.powervm.vif._push_vif_event')
@mock.patch('nova.virt.powervm.vif._build_vif_driver')
def test_plug(self, mock_bld_drv):
def test_plug(self, mock_bld_drv, mock_event):
"""Test the top-level plug method."""
mock_vif = {'address': 'MAC', 'type': 'pvm_sea'}
@ -71,10 +100,13 @@ class TestVifFunctions(test.NoDBTestCase):
mock_bld_drv.return_value.plug.assert_called_once_with(mock_vif,
new_vif=True)
self.assertEqual(mock_bld_drv.return_value.plug.return_value, vnet)
mock_event.assert_called_once_with(self.adpt, 'plug', vnet, mock.ANY,
'pvm_sea')
# Clean up
mock_bld_drv.reset_mock()
mock_bld_drv.return_value.plug.reset_mock()
mock_event.reset_mock()
# 2) Plug returns None (which it should IRL whenever new_vif=False).
mock_bld_drv.return_value.plug.return_value = None
@ -84,6 +116,7 @@ class TestVifFunctions(test.NoDBTestCase):
mock_bld_drv.return_value.plug.assert_called_once_with(mock_vif,
new_vif=False)
self.assertIsNone(vnet)
mock_event.assert_not_called()
@mock.patch('nova.virt.powervm.vif._build_vif_driver')
def test_plug_raises(self, mock_vif_drv):
@ -98,8 +131,9 @@ class TestVifFunctions(test.NoDBTestCase):
mock_vif_drv.assert_called_once_with('adap', 'inst', mock_vif)
vif_drv.plug.assert_called_once_with(mock_vif, new_vif='new_vif')
@mock.patch('nova.virt.powervm.vif._push_vif_event')
@mock.patch('nova.virt.powervm.vif._build_vif_driver')
def test_unplug(self, mock_bld_drv):
def test_unplug(self, mock_bld_drv, mock_event):
"""Test the top-level unplug method."""
mock_vif = {'address': 'MAC', 'type': 'pvm_sea'}
@ -109,10 +143,12 @@ class TestVifFunctions(test.NoDBTestCase):
mock_bld_drv.assert_called_once_with(self.adpt, 'instance', mock_vif)
mock_bld_drv.return_value.unplug.assert_called_once_with(
mock_vif, cna_w_list=None)
mock_event.assert_called_once_with(self.adpt, 'unplug', 'vnet_w',
mock.ANY, 'pvm_sea')
# Clean up
mock_bld_drv.reset_mock()
mock_bld_drv.return_value.unplug.reset_mock()
mock_event.reset_mock()
# 2) With specified cna_w_list
mock_bld_drv.return_value.unplug.return_value = None
@ -120,6 +156,7 @@ class TestVifFunctions(test.NoDBTestCase):
mock_bld_drv.assert_called_once_with(self.adpt, 'instance', mock_vif)
mock_bld_drv.return_value.unplug.assert_called_once_with(
mock_vif, cna_w_list='cnalist')
mock_event.assert_not_called()
@mock.patch('nova.virt.powervm.vif._build_vif_driver')
def test_unplug_raises(self, mock_vif_drv):
@ -223,3 +260,71 @@ class TestVifOvsDriver(test.NoDBTestCase):
self.assertTrue(t1.delete.called)
self.assertTrue(t2.delete.called)
self.assertTrue(mock_cna.delete.called)
class TestVifSeaDriver(test.NoDBTestCase):
def setUp(self):
super(TestVifSeaDriver, self).setUp()
self.adpt = mock.Mock()
self.inst = mock.Mock()
self.drv = vif.PvmSeaVifDriver(self.adpt, self.inst)
@mock.patch('nova.virt.powervm.vm.get_pvm_uuid')
@mock.patch('pypowervm.tasks.cna.crt_cna')
def test_plug_from_neutron(self, mock_crt_cna, mock_pvm_uuid):
"""Tests that a VIF can be created. Mocks Neutron net"""
# Set up the mocks. Look like Neutron
fake_vif = {'details': {'vlan': 5}, 'network': {'meta': {}},
'address': 'aabbccddeeff'}
def validate_crt(adpt, host_uuid, lpar_uuid, vlan, mac_addr=None):
self.assertIsNone(host_uuid)
self.assertEqual(5, vlan)
self.assertEqual('aabbccddeeff', mac_addr)
return pvm_net.CNA.bld(self.adpt, 5, 'host_uuid',
mac_addr=mac_addr)
mock_crt_cna.side_effect = validate_crt
# Invoke
resp = self.drv.plug(fake_vif)
# Validate (along with validate method above)
self.assertEqual(1, mock_crt_cna.call_count)
self.assertIsNotNone(resp)
self.assertIsInstance(resp, pvm_net.CNA)
def test_plug_existing_vif(self):
"""Tests that a VIF need not be created."""
# Set up the mocks
fake_vif = {'network': {'meta': {'vlan': 5}},
'address': 'aabbccddeeff'}
# Invoke
resp = self.drv.plug(fake_vif, new_vif=False)
self.assertIsNone(resp)
@mock.patch('nova.virt.powervm.vm.get_cnas')
def test_unplug_vifs(self, mock_vm_get):
"""Tests that a delete of the vif can be done."""
# Mock up the CNA response. Two should already exist, the other
# should not.
cnas = [cna('AABBCCDDEEFF'), cna('AABBCCDDEE11'), cna('AABBCCDDEE22')]
mock_vm_get.return_value = cnas
# Run method. The AABBCCDDEE11 won't be unplugged (wasn't invoked
# below) and the last unplug will also just no-op because its not on
# the VM.
self.drv.unplug({'address': 'aa:bb:cc:dd:ee:ff'})
self.drv.unplug({'address': 'aa:bb:cc:dd:ee:22'})
self.drv.unplug({'address': 'aa:bb:cc:dd:ee:33'})
# The delete should have only been called once for each applicable vif.
# The second CNA didn't have a matching mac so it should be skipped.
self.assertEqual(1, cnas[0].delete.call_count)
self.assertEqual(0, cnas[1].delete.call_count)
self.assertEqual(1, cnas[2].delete.call_count)

View File

@ -14,12 +14,17 @@
# License for the specific language governing permissions and limitations
# under the License.
import abc
from oslo_config import cfg
from oslo_log import log
from oslo_serialization import jsonutils
from oslo_utils import excutils
from oslo_utils import importutils
from pypowervm import exceptions as pvm_ex
from pypowervm.tasks import cna as pvm_cna
from pypowervm.tasks import partition as pvm_par
from pypowervm.wrappers import event as pvm_evt
import six
from nova import exception
@ -30,8 +35,14 @@ LOG = log.getLogger(__name__)
NOVALINK_VSWITCH = 'NovaLinkVEABridge'
# Provider tag for custom events from this module
EVENT_PROVIDER_ID = 'NOVA_PVM_VIF'
VIF_TYPE_PVM_SEA = 'pvm_sea'
VIF_TYPE_PVM_OVS = 'ovs'
VIF_MAPPING = {VIF_TYPE_PVM_OVS:
VIF_MAPPING = {VIF_TYPE_PVM_SEA:
'nova.virt.powervm.vif.PvmSeaVifDriver',
VIF_TYPE_PVM_OVS:
'nova.virt.powervm.vif.PvmOvsVifDriver'}
CONF = cfg.CONF
@ -39,6 +50,7 @@ CONF = cfg.CONF
def _build_vif_driver(adapter, instance, vif):
"""Returns the appropriate VIF Driver for the given VIF.
:param adapter: The pypowervm adapter API interface.
:param instance: The nova instance.
:param vif: The virtual interface.
@ -60,6 +72,33 @@ def _build_vif_driver(adapter, instance, vif):
raise exception.VirtualInterfacePlugException()
def _push_vif_event(adapter, action, vif_w, instance, vif_type):
"""Push a custom event to the REST server for a vif action (plug/unplug).
This event prompts the neutron agent to mark the port up or down. It is
consumed by custom neutron agents (e.g. Shared Ethernet Adapter)
:param adapter: The pypowervm adapter.
:param action: The action taken on the vif - either 'plug' or 'unplug'
:param vif_w: The pypowervm wrapper of the affected vif (CNA, VNIC, etc.)
:param instance: The nova instance for the event
:param vif_type: The type of event source (pvm_sea, ovs, bridge,
pvm_sriov etc)
"""
data = vif_w.href
detail = jsonutils.dumps(dict(provider=EVENT_PROVIDER_ID, action=action,
mac=vif_w.mac, type=vif_type))
event = pvm_evt.Event.bld(adapter, data, detail)
try:
event = event.create()
LOG.debug('Pushed custom event for consumption by neutron agent: %s',
str(event), instance=instance)
except Exception:
with excutils.save_and_reraise_exception(logger=LOG):
LOG.exception('Custom VIF event push failed. %s', str(event),
instance=instance)
def plug(adapter, instance, vif, new_vif=True):
"""Plugs a virtual interface (network) into a VM.
@ -76,13 +115,19 @@ def plug(adapter, instance, vif, new_vif=True):
vif_drv = _build_vif_driver(adapter, instance, vif)
try:
return vif_drv.plug(vif, new_vif=new_vif)
vnet_w = vif_drv.plug(vif, new_vif=new_vif)
except pvm_ex.HttpError:
LOG.exception('VIF plug failed for instance.', instance=instance)
raise exception.VirtualInterfacePlugException()
# Other exceptions are (hopefully) custom VirtualInterfacePlugException
# generated lower in the call stack.
# Push a custom event if we really plugged the vif
if vnet_w is not None:
_push_vif_event(adapter, 'plug', vnet_w, instance, vif['type'])
return vnet_w
def unplug(adapter, instance, vif, cna_w_list=None):
"""Unplugs a virtual interface (network) from a VM.
@ -96,18 +141,26 @@ def unplug(adapter, instance, vif, cna_w_list=None):
"""
vif_drv = _build_vif_driver(adapter, instance, vif)
try:
vif_drv.unplug(vif, cna_w_list=cna_w_list)
vnet_w = vif_drv.unplug(vif, cna_w_list=cna_w_list)
except pvm_ex.HttpError as he:
LOG.exception('VIF unplug failed for instance', instance=instance)
raise exception.VirtualInterfaceUnplugException(reason=he.args[0])
# Push a custom event if we successfully unplugged the vif.
if vnet_w:
_push_vif_event(adapter, 'unplug', vnet_w, instance, vif['type'])
class PvmOvsVifDriver(object):
"""The Open vSwitch VIF driver for PowerVM."""
@six.add_metaclass(abc.ABCMeta)
class PvmVifDriver(object):
"""Represents an abstract class for a PowerVM Vif Driver.
A VIF Driver understands a given virtual interface type (network). It
understands how to plug and unplug a given VIF for a virtual machine.
"""
def __init__(self, adapter, instance):
"""Initializes a VIF Driver.
:param adapter: The pypowervm adapter API interface.
:param instance: The nova instance that the vif action will be run
against.
@ -115,6 +168,74 @@ class PvmOvsVifDriver(object):
self.adapter = adapter
self.instance = instance
@abc.abstractmethod
def plug(self, vif, new_vif=True):
"""Plugs a virtual interface (network) into a VM.
:param vif: The virtual interface to plug into the instance.
:param new_vif: (Optional, Default: True) If set, indicates that it is
a brand new VIF. If False, it indicates that the VIF
is already on the client but should be treated on the
bridge.
:return: The new vif that was created. Only returned if new_vif is
set to True. Otherwise None is expected.
"""
pass
def unplug(self, vif, cna_w_list=None):
"""Unplugs a virtual interface (network) from a VM.
:param vif: The virtual interface to plug into the instance.
:param cna_w_list: (Optional, Default: None) The list of Client Network
Adapters from pypowervm. Providing this input
allows for an improvement in operation speed.
:return cna_w: The deleted Client Network Adapter or None if the CNA
is not found.
"""
# This is a default implementation that most implementations will
# require.
# Need to find the adapters if they were not provided
if not cna_w_list:
cna_w_list = vm.get_cnas(self.adapter, self.instance)
cna_w = self._find_cna_for_vif(cna_w_list, vif)
if not cna_w:
LOG.warning('Unable to unplug VIF with mac %(mac)s. The VIF was '
'not found on the instance.',
{'mac': vif['address']}, instance=self.instance)
return None
LOG.info('Deleting VIF with mac %(mac)s.',
{'mac': vif['address']}, instance=self.instance)
try:
cna_w.delete()
except Exception as e:
LOG.exception('Unable to unplug VIF with mac %(mac)s.',
{'mac': vif['address']}, instance=self.instance)
raise exception.VirtualInterfaceUnplugException(
reason=six.text_type(e))
return cna_w
@staticmethod
def _find_cna_for_vif(cna_w_list, vif):
"""Finds the PowerVM CNA for a given Nova VIF.
:param cna_w_list: The list of Client Network Adapter wrappers from
pypowervm.
:param vif: The Nova Virtual Interface (virtual network interface).
:return: The CNA that corresponds to the VIF. None if one is not
part of the cna_w_list.
"""
for cna_w in cna_w_list:
if vm.norm_mac(cna_w.mac) == vif['address']:
return cna_w
return None
class PvmOvsVifDriver(PvmVifDriver):
"""The Open vSwitch VIF driver for PowerVM."""
def plug(self, vif, new_vif=True):
"""Plugs a virtual interface (network) into a VM.
@ -191,8 +312,8 @@ class PvmOvsVifDriver(object):
def unplug(self, vif, cna_w_list=None):
"""Unplugs a virtual interface (network) from a VM.
This will remove the adapter from the Open vSwitch and delete the
trunk adapters.
Extends the base implementation, but before calling it will remove
the adapter from the Open vSwitch and delete the trunk.
:param vif: The virtual interface to plug into the instance.
:param cna_w_list: (Optional, Default: None) The list of Client Network
@ -219,29 +340,39 @@ class PvmOvsVifDriver(object):
for trunk in trunks:
trunk.delete()
# Now delete the client CNA
LOG.info('Deleting VIF with mac %s for instance.', vif['address'],
instance=self.instance)
try:
cna_w.delete()
except Exception as e:
LOG.error('Unable to unplug VIF with mac %s for instance.',
vif['address'], instance=self.instance)
raise exception.VirtualInterfaceUnplugException(
reason=six.text_type(e))
return cna_w
# Delete the client CNA
return super(PvmOvsVifDriver, self).unplug(vif, cna_w_list=cna_w_list)
@staticmethod
def _find_cna_for_vif(cna_w_list, vif):
"""Finds the PowerVM CNA for a given Nova VIF.
:param cna_w_list: The list of Client Network Adapter wrappers from
pypowervm.
:param vif: The Nova Virtual Interface (virtual network interface).
:return: The CNA that corresponds to the VIF. None if one is not
part of the cna_w_list.
class PvmSeaVifDriver(PvmVifDriver):
"""The PowerVM Shared Ethernet Adapter VIF Driver."""
def plug(self, vif, new_vif=True):
"""Plugs a virtual interface (network) into a VM.
This method simply creates the client network adapter into the VM.
:param vif: The virtual interface to plug into the instance.
:param new_vif: (Optional, Default: True) If set, indicates that it is
a brand new VIF. If False, it indicates that the VIF
is already on the client but should be treated on the
bridge.
:return: The new vif that was created. Only returned if new_vif is
set to True. Otherwise None is expected.
"""
for cna_w in cna_w_list:
if vm.norm_mac(cna_w.mac) == vif['address']:
return cna_w
return None
# Do nothing if not a new VIF
if not new_vif:
return None
lpar_uuid = vm.get_pvm_uuid(self.instance)
# CNA's require a VLAN. The networking-powervm neutron agent puts this
# in the vif details.
vlan = int(vif['details']['vlan'])
LOG.debug("Creating SEA-based VIF with VLAN %s", str(vlan),
instance=self.instance)
cna_w = pvm_cna.crt_cna(self.adapter, None, lpar_uuid, vlan,
mac_addr=vif['address'])
return cna_w