Namespace deletion functionality for namespace_subnet driver

This patch extends the namespace_subnet driver to handle namespace
deletion. It ensures the created resources during namespace creation
are removed upon namespace deletion.

Note it does not currently support deleting the extra ports created
by the ports pool feature, so it should not be used if ports pool
feature is enabled. A follow up patch will address this issue

Partially Implements: blueprint network-namespace

Change-Id: I2eed278dafacd5090a902bacfd366f7cdf9edca4
This commit is contained in:
Luis Tomas Bolivar 2018-04-18 11:31:48 +00:00
parent 51c57ec31f
commit c1e8f458d4
8 changed files with 224 additions and 9 deletions

View File

@ -108,3 +108,11 @@ Testing the network per namespace functionality
$ curl 10.0.0.141
demo-5995548848-lmmjc: HELLO! I AM ALIVE!!!
6. And finally, to remove the namespace and all its resources, including
openstack networks, kuryrnet CRD, svc, pods, you just need to do::
$ kubectl delete namespace test
Note it does not currently support deleting the extra ports created by the
ports pool feature, so it should not be used if ports pool feature is enabled.

View File

@ -137,6 +137,14 @@ class PodSubnetsDriver(DriverBase):
"""
raise NotImplementedError()
def delete_namespace_subnet(self, kuryr_net_crd):
"""Delete network resources associated to a namespace.
:param kuryr_net_crd: kuryrnet CRD obj dict that contains Neutron's
network resources associated to a namespace
"""
raise NotImplementedError()
def rollback_network_resources(self, router_id, net_id, subnet_id,
namespace):
"""Rollback created network resources for a namespace.

View File

@ -57,7 +57,7 @@ class NamespacePodSubnetDriver(default_subnet.DefaultPodSubnetDriver):
try:
annotations = ns['metadata']['annotations']
net_crd_url = annotations[constants.K8S_ANNOTATION_NET_CRD]
net_crd_name = annotations[constants.K8S_ANNOTATION_NET_CRD]
except KeyError:
LOG.exception("Namespace missing CRD annotations for selecting "
"the corresponding subnet.")
@ -65,13 +65,40 @@ class NamespacePodSubnetDriver(default_subnet.DefaultPodSubnetDriver):
try:
net_crd = kubernetes.get('%s/kuryrnets/%s' % (
constants.K8S_API_CRD, net_crd_url))
except exceptions.K8sClientException as ex:
constants.K8S_API_CRD, net_crd_name))
except exceptions.K8sClientException:
LOG.exception("Kubernetes Client Exception.")
raise ex
raise
return net_crd['spec']['subnetId']
def delete_namespace_subnet(self, net_crd_name):
router_id = oslo_cfg.CONF.namespace_subnet.pod_router
neutron = clients.get_neutron_client()
kubernetes = clients.get_kubernetes_client()
try:
net_crd = kubernetes.get('%s/kuryrnets/%s' % (
constants.K8S_API_CRD, net_crd_name))
except exceptions.K8sClientException:
LOG.exception("Kubernetes Client Exception.")
raise
subnet_id = net_crd['spec']['subnetId']
net_id = net_crd['spec']['netId']
try:
neutron.remove_interface_router(router_id,
{"subnet_id": subnet_id})
neutron.delete_network(net_id)
except n_exc.NotFound as ex:
LOG.debug("Neutron resource not found: %s", ex)
except n_exc.NeutronClientException:
LOG.exception("Error deleting neutron resources.")
raise
self._del_kuryrnet_crd(net_crd_name)
def create_namespace_network(self, namespace):
neutron = clients.get_neutron_client()
@ -111,12 +138,12 @@ class NamespacePodSubnetDriver(default_subnet.DefaultPodSubnetDriver):
try:
net_crd = self._add_kuryrnet_crd(namespace, neutron_net['id'],
router_id, neutron_subnet['id'])
except exceptions.K8sClientException as ex:
except exceptions.K8sClientException:
LOG.exception("Kuryrnet CRD could not be added. Rolling back "
"network resources created for the namespace.")
self.rollback_network_resources(router_id, neutron_net['id'],
neutron_subnet['id'], namespace)
raise ex
raise
return net_crd
def rollback_network_resources(self, router_id, net_id, subnet_id,
@ -152,8 +179,18 @@ class NamespacePodSubnetDriver(default_subnet.DefaultPodSubnetDriver):
}
try:
kubernetes.post('%s/kuryrnets' % constants.K8S_API_CRD, net_crd)
except exceptions.K8sClientException as ex:
except exceptions.K8sClientException:
LOG.exception("Kubernetes Client Exception creating kuryrnet "
"CRD.")
raise ex
raise
return net_crd
def _del_kuryrnet_crd(self, net_crd_name):
kubernetes = clients.get_kubernetes_client()
try:
kubernetes.delete('%s/kuryrnets/%s' % (constants.K8S_API_CRD,
net_crd_name))
except exceptions.K8sClientException:
LOG.exception("Kubernetes Client Exception deleting kuryrnet "
"CRD.")
raise

View File

@ -52,7 +52,13 @@ class NamespaceHandler(k8s_base.ResourceEventHandler):
ns_name)
def on_deleted(self, namespace):
pass
LOG.debug("Deleting namespace: %s", namespace)
net_crd = self._get_net_crd(namespace)
if not net_crd:
LOG.warning("There is no CRD annotated at the namespace %s",
namespace)
return
self._drv_subnets.delete_namespace_subnet(net_crd)
def _get_net_crd(self, namespace):
try:

View File

@ -117,6 +117,19 @@ class K8sClient(object):
return response.json()
raise exc.K8sClientException(response)
def delete(self, path):
LOG.debug("Delete %(path)s", {'path': path})
url = self._base_url + path
header = {'Content-Type': 'application/json'}
if self.token:
header.update({'Authorization': 'Bearer %s' % self.token})
response = requests.delete(url, cert=self.cert,
verify=self.verify_server, headers=header)
if response.ok:
return response.json()
raise exc.K8sClientException(response)
def annotate(self, path, annotations, resource_version=None):
"""Pushes a resource annotation to the K8s API resource

View File

@ -172,6 +172,103 @@ class TestNamespacePodSubnetDriver(test_base.TestCase):
m_driver, namespace)
kubernetes.get.assert_called()
def test_delete_namespace_subnet(self):
cls = subnet_drv.NamespacePodSubnetDriver
m_driver = mock.MagicMock(spec=cls)
net_crd_name = 'test'
net_id = mock.sentinel.net_id
subnet_id = mock.sentinel.subnet_id
crd = {
'spec': {
'subnetId': subnet_id,
'netId': net_id
}
}
neutron = self.useFixture(k_fix.MockNeutronClient()).client
kubernetes = self.useFixture(k_fix.MockK8sClient()).client
kubernetes.get.return_value = crd
cls.delete_namespace_subnet(m_driver, net_crd_name)
kubernetes.get.assert_called_once()
m_driver._del_kuryrnet_crd.assert_called_once_with(net_crd_name)
neutron.remove_interface_router.assert_called_once()
neutron.delete_network.assert_called_once_with(net_id)
def test_delete_namespace_subnet_k8s_get_exception(self):
cls = subnet_drv.NamespacePodSubnetDriver
m_driver = mock.MagicMock(spec=cls)
net_crd_name = 'test'
neutron = self.useFixture(k_fix.MockNeutronClient()).client
kubernetes = self.useFixture(k_fix.MockK8sClient()).client
kubernetes.get.side_effect = k_exc.K8sClientException
self.assertRaises(k_exc.K8sClientException,
cls.delete_namespace_subnet, m_driver, net_crd_name)
kubernetes.get.assert_called_once()
m_driver._del_kuryrnet_crd.assert_not_called()
neutron.remove_interface_router.assert_not_called()
neutron.delete_network.assert_not_called()
def test_delete_namespace_subnet_k8s_del_crd_exception(self):
cls = subnet_drv.NamespacePodSubnetDriver
m_driver = mock.MagicMock(spec=cls)
net_crd_name = 'test'
net_id = mock.sentinel.net_id
subnet_id = mock.sentinel.subnet_id
crd = {
'spec': {
'subnetId': subnet_id,
'netId': net_id
}
}
neutron = self.useFixture(k_fix.MockNeutronClient()).client
kubernetes = self.useFixture(k_fix.MockK8sClient()).client
kubernetes.get.return_value = crd
m_driver._del_kuryrnet_crd.side_effect = k_exc.K8sClientException
self.assertRaises(k_exc.K8sClientException,
cls.delete_namespace_subnet, m_driver, net_crd_name)
kubernetes.get.assert_called_once()
m_driver._del_kuryrnet_crd.assert_called_once_with(net_crd_name)
neutron.remove_interface_router.assert_called_once()
neutron.delete_network.assert_called_once_with(net_id)
def test_delete_namespace_subnet_neutron_exception(self):
cls = subnet_drv.NamespacePodSubnetDriver
m_driver = mock.MagicMock(spec=cls)
net_crd_name = 'test'
net_id = mock.sentinel.net_id
subnet_id = mock.sentinel.subnet_id
crd = {
'spec': {
'subnetId': subnet_id,
'netId': net_id
}
}
neutron = self.useFixture(k_fix.MockNeutronClient()).client
kubernetes = self.useFixture(k_fix.MockK8sClient()).client
kubernetes.get.return_value = crd
neutron.delete_network.side_effect = n_exc.NeutronClientException
self.assertRaises(n_exc.NeutronClientException,
cls.delete_namespace_subnet, m_driver, net_crd_name)
kubernetes.get.assert_called_once()
neutron.remove_interface_router.assert_called_once()
neutron.delete_network.assert_called_once_with(net_id)
m_driver._del_kuryrnet_crd.assert_not_called()
def test_create_namespace_network(self):
cls = subnet_drv.NamespacePodSubnetDriver
m_driver = mock.MagicMock(spec=cls)

View File

@ -57,6 +57,8 @@ class TestNamespaceHandler(test_base.TestCase):
self._create_namespace_network = (
self._handler._drv_subnets.create_namespace_network)
self._delete_namespace_subnet = (
self._handler._drv_subnets.delete_namespace_subnet)
self._get_net_crd = self._handler._get_net_crd
self._set_net_crd = self._handler._set_net_crd
self._rollback_network_resources = (
@ -169,3 +171,21 @@ class TestNamespaceHandler(test_base.TestCase):
self._namespace_name)
self._set_net_crd.assert_called_once_with(self._namespace, net_crd)
self._rollback_network_resources.assert_called_once()
def test_on_deleted(self):
net_crd = self._get_crd()
self._get_net_crd.return_value = net_crd
namespace.NamespaceHandler.on_deleted(self._handler, self._namespace)
self._get_net_crd.assert_called_once_with(self._namespace)
self._delete_namespace_subnet.assert_called_once_with(net_crd)
def test_on_deleted_missing_crd_annotation(self):
self._get_net_crd.return_value = None
namespace.NamespaceHandler.on_deleted(self._handler, self._namespace)
self._get_net_crd.assert_called_once_with(self._namespace)
self._delete_namespace_subnet.assert_not_called()

View File

@ -368,3 +368,29 @@ class TestK8sClient(test_base.TestCase):
self.assertRaises(exc.K8sClientException,
self.client.post, path, body)
@mock.patch('requests.delete')
def test_delete(self, m_delete):
path = '/test'
ret = {'test': 'value'}
m_resp = mock.MagicMock()
m_resp.ok = True
m_resp.json.return_value = ret
m_delete.return_value = m_resp
self.assertEqual(ret, self.client.delete(path))
m_delete.assert_called_once_with(self.base_url + path,
headers=mock.ANY, cert=(None, None),
verify=False)
@mock.patch('requests.delete')
def test_delete_exception(self, m_delete):
path = '/test'
m_resp = mock.MagicMock()
m_resp.ok = False
m_delete.return_value = m_resp
self.assertRaises(exc.K8sClientException,
self.client.delete, path)