Merge "Remove vifs upon teardown"
This commit is contained in:
commit
084c9161f1
@ -12,8 +12,12 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
from ironic.common import exception
|
from ironic.common import exception
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_node_vif_ids(task):
|
def get_node_vif_ids(task):
|
||||||
"""Get all VIF ids for a node.
|
"""Get all VIF ids for a node.
|
||||||
@ -108,3 +112,34 @@ def get_physnets_by_portgroup_id(task, portgroup_id, exclude_port=None):
|
|||||||
raise exception.PortgroupPhysnetInconsistent(
|
raise exception.PortgroupPhysnetInconsistent(
|
||||||
portgroup=portgroup.uuid, physical_networks=", ".join(pg_physnets))
|
portgroup=portgroup.uuid, physical_networks=", ".join(pg_physnets))
|
||||||
return pg_physnets
|
return pg_physnets
|
||||||
|
|
||||||
|
|
||||||
|
def remove_vifs_from_node(task):
|
||||||
|
"""Remove all vif attachment records from a node.
|
||||||
|
|
||||||
|
:param task: a TaskManager instance.
|
||||||
|
"""
|
||||||
|
vifs = task.driver.network.vif_list(task)
|
||||||
|
for vif_entry in vifs:
|
||||||
|
vif = vif_entry.get('id')
|
||||||
|
if not vif:
|
||||||
|
LOG.warning('Incorrect vif entry for %(node)s lacks an ID field, '
|
||||||
|
'and is thus unsupported. Found: %(found)s.',
|
||||||
|
{'node': task.node.uuid,
|
||||||
|
'found': vif_entry})
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
task.driver.network.vif_detach(task, vif)
|
||||||
|
except exception.VifNotAttached as e:
|
||||||
|
LOG.warning('While removing records of VIF attachments from node '
|
||||||
|
'%(node)s, we recieved indication that %(vif)s is '
|
||||||
|
'no longer attached. There should not happen under '
|
||||||
|
'normal circumstances.',
|
||||||
|
{'node': task.node.uuid,
|
||||||
|
'vif': vif})
|
||||||
|
|
||||||
|
except exception.NetworkError as e:
|
||||||
|
LOG.error('An error has been encountered while removing a '
|
||||||
|
'VIF record for %(node)s. Error: %(error)s',
|
||||||
|
{'node': task.node.uuid,
|
||||||
|
'error': e})
|
||||||
|
@ -62,6 +62,7 @@ from ironic.common import exception
|
|||||||
from ironic.common.glance_service import service_utils as glance_utils
|
from ironic.common.glance_service import service_utils as glance_utils
|
||||||
from ironic.common.i18n import _
|
from ironic.common.i18n import _
|
||||||
from ironic.common import images
|
from ironic.common import images
|
||||||
|
from ironic.common import network
|
||||||
from ironic.common import states
|
from ironic.common import states
|
||||||
from ironic.common import swift
|
from ironic.common import swift
|
||||||
from ironic.conductor import base_manager
|
from ironic.conductor import base_manager
|
||||||
@ -929,6 +930,7 @@ class ConductorManager(base_manager.BaseConductorManager):
|
|||||||
driver_internal_info = node.driver_internal_info
|
driver_internal_info = node.driver_internal_info
|
||||||
driver_internal_info.pop('instance', None)
|
driver_internal_info.pop('instance', None)
|
||||||
node.driver_internal_info = driver_internal_info
|
node.driver_internal_info = driver_internal_info
|
||||||
|
network.remove_vifs_from_node(task)
|
||||||
node.save()
|
node.save()
|
||||||
|
|
||||||
# Begin cleaning
|
# Begin cleaning
|
||||||
|
@ -570,6 +570,8 @@ class NeutronVIFPortIDMixin(VIFPortIDMixin):
|
|||||||
|
|
||||||
self._clear_vif_from_port_like_obj(port_like_obj)
|
self._clear_vif_from_port_like_obj(port_like_obj)
|
||||||
|
|
||||||
# NOTE(vsaienko) allow to unplug VIFs from ACTIVE instance.
|
# NOTE(vsaienko): allow to unplug VIFs from ACTIVE instance.
|
||||||
if task.node.provision_state == states.ACTIVE:
|
# NOTE(TheJulia): Also ensure that we delete the vif when in
|
||||||
|
# DELETING state.
|
||||||
|
if task.node.provision_state in [states.ACTIVE, states.DELETING]:
|
||||||
neutron.unbind_neutron_port(vif_id, context=task.context)
|
neutron.unbind_neutron_port(vif_id, context=task.context)
|
||||||
|
@ -13,11 +13,15 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import mock
|
||||||
from oslo_utils import uuidutils
|
from oslo_utils import uuidutils
|
||||||
|
|
||||||
from ironic.common import exception
|
from ironic.common import exception
|
||||||
from ironic.common import network
|
from ironic.common import network
|
||||||
|
from ironic.common import neutron as neutron_common
|
||||||
|
from ironic.common import states
|
||||||
from ironic.conductor import task_manager
|
from ironic.conductor import task_manager
|
||||||
|
from ironic.drivers.modules.network import common as driver_common
|
||||||
from ironic.tests.unit.conductor import mgr_utils
|
from ironic.tests.unit.conductor import mgr_utils
|
||||||
from ironic.tests.unit.db import base as db_base
|
from ironic.tests.unit.db import base as db_base
|
||||||
from ironic.tests.unit.db import utils as db_utils
|
from ironic.tests.unit.db import utils as db_utils
|
||||||
@ -156,6 +160,47 @@ class TestNetwork(db_base.DbTestCase):
|
|||||||
def test_get_node_vif_ids_during_rescuing(self):
|
def test_get_node_vif_ids_during_rescuing(self):
|
||||||
self._test_get_node_vif_ids_multitenancy('rescuing_vif_port_id')
|
self._test_get_node_vif_ids_multitenancy('rescuing_vif_port_id')
|
||||||
|
|
||||||
|
def test_remove_vifs_from_node(self):
|
||||||
|
db_utils.create_test_port(
|
||||||
|
node_id=self.node.id, address='aa:bb:cc:dd:ee:ff',
|
||||||
|
internal_info={driver_common.TENANT_VIF_KEY: 'test-vif-A'})
|
||||||
|
db_utils.create_test_portgroup(
|
||||||
|
node_id=self.node.id, address='dd:ee:ff:aa:bb:cc',
|
||||||
|
internal_info={driver_common.TENANT_VIF_KEY: 'test-vif-B'})
|
||||||
|
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||||
|
network.remove_vifs_from_node(task)
|
||||||
|
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||||
|
result = network.get_node_vif_ids(task)
|
||||||
|
self.assertEqual({}, result['ports'])
|
||||||
|
self.assertEqual({}, result['portgroups'])
|
||||||
|
|
||||||
|
|
||||||
|
class TestRemoveVifsTestCase(db_base.DbTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestRemoveVifsTestCase, self).setUp()
|
||||||
|
self.node = object_utils.create_test_node(
|
||||||
|
self.context,
|
||||||
|
network_interface='flat',
|
||||||
|
provision_state=states.DELETING)
|
||||||
|
|
||||||
|
@mock.patch.object(neutron_common, 'unbind_neutron_port')
|
||||||
|
def test_remove_vifs_from_node_failure(self, mock_unbind):
|
||||||
|
db_utils.create_test_port(
|
||||||
|
node_id=self.node.id, address='aa:bb:cc:dd:ee:ff',
|
||||||
|
internal_info={driver_common.TENANT_VIF_KEY: 'test-vif-A'})
|
||||||
|
db_utils.create_test_portgroup(
|
||||||
|
node_id=self.node.id, address='dd:ee:ff:aa:bb:cc',
|
||||||
|
internal_info={driver_common.TENANT_VIF_KEY: 'test-vif-B'})
|
||||||
|
mock_unbind.side_effect = [exception.NetworkError, None]
|
||||||
|
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||||
|
network.remove_vifs_from_node(task)
|
||||||
|
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||||
|
result = network.get_node_vif_ids(task)
|
||||||
|
self.assertEqual({}, result['ports'])
|
||||||
|
self.assertEqual({}, result['portgroups'])
|
||||||
|
self.assertEqual(2, mock_unbind.call_count)
|
||||||
|
|
||||||
|
|
||||||
class GetPortgroupByIdTestCase(db_base.DbTestCase):
|
class GetPortgroupByIdTestCase(db_base.DbTestCase):
|
||||||
|
|
||||||
|
@ -1791,9 +1791,13 @@ class DoNodeDeployTearDownTestCase(mgr_utils.ServiceSetUpMixin,
|
|||||||
self.assertEqual({}, node.instance_info)
|
self.assertEqual({}, node.instance_info)
|
||||||
mock_tear_down.assert_called_once_with(mock.ANY)
|
mock_tear_down.assert_called_once_with(mock.ANY)
|
||||||
|
|
||||||
|
# TODO(TheJulia): Since we're functionally bound to neutron support
|
||||||
|
# by default, the fake drivers still invoke neutron.
|
||||||
|
@mock.patch('ironic.common.neutron.unbind_neutron_port')
|
||||||
@mock.patch('ironic.conductor.manager.ConductorManager._do_node_clean')
|
@mock.patch('ironic.conductor.manager.ConductorManager._do_node_clean')
|
||||||
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.tear_down')
|
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.tear_down')
|
||||||
def test__do_node_tear_down_ok(self, mock_tear_down, mock_clean):
|
def test__do_node_tear_down_ok(self, mock_tear_down, mock_clean,
|
||||||
|
mock_unbind):
|
||||||
# test when driver.deploy.tear_down succeeds
|
# test when driver.deploy.tear_down succeeds
|
||||||
node = obj_utils.create_test_node(
|
node = obj_utils.create_test_node(
|
||||||
self.context, driver='fake', provision_state=states.DELETING,
|
self.context, driver='fake', provision_state=states.DELETING,
|
||||||
@ -1802,11 +1806,15 @@ class DoNodeDeployTearDownTestCase(mgr_utils.ServiceSetUpMixin,
|
|||||||
instance_info={'foo': 'bar'},
|
instance_info={'foo': 'bar'},
|
||||||
driver_internal_info={'is_whole_disk_image': False,
|
driver_internal_info={'is_whole_disk_image': False,
|
||||||
'instance': {'ephemeral_gb': 10}})
|
'instance': {'ephemeral_gb': 10}})
|
||||||
|
port = obj_utils.create_test_port(
|
||||||
|
self.context, node_id=node.id,
|
||||||
|
internal_info={'tenant_vif_port_id': 'foo'})
|
||||||
|
|
||||||
task = task_manager.TaskManager(self.context, node.uuid)
|
task = task_manager.TaskManager(self.context, node.uuid)
|
||||||
self._start_service()
|
self._start_service()
|
||||||
self.service._do_node_tear_down(task, node.provision_state)
|
self.service._do_node_tear_down(task, node.provision_state)
|
||||||
node.refresh()
|
node.refresh()
|
||||||
|
port.refresh()
|
||||||
# Node will be moved to AVAILABLE after cleaning, not tested here
|
# Node will be moved to AVAILABLE after cleaning, not tested here
|
||||||
self.assertEqual(states.CLEANING, node.provision_state)
|
self.assertEqual(states.CLEANING, node.provision_state)
|
||||||
self.assertEqual(states.AVAILABLE, node.target_provision_state)
|
self.assertEqual(states.AVAILABLE, node.target_provision_state)
|
||||||
@ -1816,6 +1824,8 @@ class DoNodeDeployTearDownTestCase(mgr_utils.ServiceSetUpMixin,
|
|||||||
self.assertNotIn('instance', node.driver_internal_info)
|
self.assertNotIn('instance', node.driver_internal_info)
|
||||||
mock_tear_down.assert_called_once_with(mock.ANY)
|
mock_tear_down.assert_called_once_with(mock.ANY)
|
||||||
mock_clean.assert_called_once_with(mock.ANY)
|
mock_clean.assert_called_once_with(mock.ANY)
|
||||||
|
self.assertEqual({}, port.internal_info)
|
||||||
|
mock_unbind.assert_called_once_with('foo', context=mock.ANY)
|
||||||
|
|
||||||
@mock.patch('ironic.drivers.modules.fake.FakeRescue.clean_up')
|
@mock.patch('ironic.drivers.modules.fake.FakeRescue.clean_up')
|
||||||
@mock.patch('ironic.conductor.manager.ConductorManager._do_node_clean')
|
@mock.patch('ironic.conductor.manager.ConductorManager._do_node_clean')
|
||||||
|
@ -931,6 +931,20 @@ class TestNeutronVifPortIDMixin(db_base.DbTestCase):
|
|||||||
mock_get.assert_called_once_with(task, 'fake_vif_id')
|
mock_get.assert_called_once_with(task, 'fake_vif_id')
|
||||||
mock_clear.assert_called_once_with(self.port)
|
mock_clear.assert_called_once_with(self.port)
|
||||||
|
|
||||||
|
@mock.patch.object(common.VIFPortIDMixin, '_clear_vif_from_port_like_obj')
|
||||||
|
@mock.patch.object(neutron_common, 'unbind_neutron_port', autospec=True)
|
||||||
|
@mock.patch.object(common.VIFPortIDMixin, '_get_port_like_obj_by_vif_id')
|
||||||
|
def test_vif_detach_deleting_node(self, mock_get, mock_unp, mock_clear):
|
||||||
|
self.node.provision_state = states.DELETING
|
||||||
|
self.node.save()
|
||||||
|
mock_get.return_value = self.port
|
||||||
|
with task_manager.acquire(self.context, self.node.id) as task:
|
||||||
|
self.interface.vif_detach(task, 'fake_vif_id')
|
||||||
|
mock_unp.assert_called_once_with('fake_vif_id',
|
||||||
|
context=task.context)
|
||||||
|
mock_get.assert_called_once_with(task, 'fake_vif_id')
|
||||||
|
mock_clear.assert_called_once_with(self.port)
|
||||||
|
|
||||||
@mock.patch.object(common.VIFPortIDMixin, '_clear_vif_from_port_like_obj')
|
@mock.patch.object(common.VIFPortIDMixin, '_clear_vif_from_port_like_obj')
|
||||||
@mock.patch.object(neutron_common, 'unbind_neutron_port', autospec=True)
|
@mock.patch.object(neutron_common, 'unbind_neutron_port', autospec=True)
|
||||||
@mock.patch.object(common.VIFPortIDMixin, '_get_port_like_obj_by_vif_id')
|
@mock.patch.object(common.VIFPortIDMixin, '_get_port_like_obj_by_vif_id')
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
upgrade:
|
||||||
|
- |
|
||||||
|
The behavior for retention of VIF interface attachments has changed.
|
||||||
|
|
||||||
|
If your use of the BareMetal service is reliant upon the behavior of
|
||||||
|
the VIFs being retained, which was introduced as a behavior change
|
||||||
|
during the Ocata cycle, then you must update your tooling to explicitly
|
||||||
|
re-add the the VIF attachments prior to deployment.
|
||||||
|
fixes:
|
||||||
|
- |
|
||||||
|
Removes all records of VIF attachments upon the teardown of a deployed
|
||||||
|
node. This is in order to resolve issues related to where it is
|
||||||
|
operationally impossible in some circumstances to remove a VIF
|
||||||
|
attachment while a node is being undeployed as the Compute service
|
||||||
|
will only attempt to remove the VIF for five minutes.
|
||||||
|
|
||||||
|
See `bug 1743652 <https://bugs.launchpad.net/ironic/+bug/1743652>`_ for more details.
|
Loading…
x
Reference in New Issue
Block a user