Merge "PowerVM Driver: SEA"
This commit is contained in:
commit
bf1f835ce7
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue