diff --git a/tacker/db/nfvo/ns_db.py b/tacker/db/nfvo/ns_db.py index ac17f6253..7517d7d7a 100644 --- a/tacker/db/nfvo/ns_db.py +++ b/tacker/db/nfvo/ns_db.py @@ -133,7 +133,7 @@ class NSPluginDb(network_service.NSPluginBase, db_base.CommonDbMixin): else: raise - def _get_ns_db(self, context, ns_id, current_statuses, new_status): + def _get_ns_db(self, context, ns_id, current_statuses): try: ns_db = ( self._model_query(context, NS). @@ -142,6 +142,9 @@ class NSPluginDb(network_service.NSPluginBase, db_base.CommonDbMixin): with_lockmode('update').one()) except orm_exc.NoResultFound: raise network_service.NSNotFound(ns_id=ns_id) + return ns_db + + def _update_ns_db(self, ns_db, new_status): ns_db.update({'status': new_status}) return ns_db @@ -340,11 +343,17 @@ class NSPluginDb(network_service.NSPluginBase, db_base.CommonDbMixin): return ns_dict # reference implementation. needs to be overrided by subclass - def delete_ns_pre(self, context, ns_id): + def delete_ns_pre(self, context, ns_id, force_delete=False): with context.session.begin(subtransactions=True): ns_db = self._get_ns_db( - context, ns_id, _ACTIVE_UPDATE_ERROR_DEAD, - constants.PENDING_DELETE) + context, ns_id, _ACTIVE_UPDATE_ERROR_DEAD) + if not force_delete: + if (ns_db is not None and ns_db.status in + [constants.PENDING_DELETE, + constants.PENDING_CREATE, + constants.PENDING_UPDATE]): + raise network_service.NSInUse(ns_id=ns_id) + ns_db = self._update_ns_db(ns_db, constants.PENDING_DELETE) deleted_ns_db = self._make_ns_dict(ns_db) self._cos_db_plg.create_event( context, res_id=ns_id, @@ -355,15 +364,21 @@ class NSPluginDb(network_service.NSPluginBase, db_base.CommonDbMixin): return deleted_ns_db def delete_ns_post(self, context, ns_id, mistral_obj, - error_reason, soft_delete=True): + error_reason, soft_delete=True, force_delete=False): ns = self.get_ns(context, ns_id) nsd_id = ns.get('nsd_id') with context.session.begin(subtransactions=True): - query = ( - self._model_query(context, NS). - filter(NS.id == ns_id). - filter(NS.status == constants.PENDING_DELETE)) - if mistral_obj and mistral_obj.state == 'ERROR': + if force_delete: + query = ( + self._model_query(context, NS). + filter(NS.id == ns_id)) + else: + query = ( + self._model_query(context, NS). + filter(NS.id == ns_id). + filter(NS.status == constants.PENDING_DELETE)) + if not force_delete and (mistral_obj + and mistral_obj.state == 'ERROR'): query.update({'status': constants.ERROR}) self._cos_db_plg.create_event( context, res_id=ns_id, @@ -385,9 +400,12 @@ class NSPluginDb(network_service.NSPluginBase, db_base.CommonDbMixin): details="ns Delete Complete") else: query.delete() - template_db = self._get_resource(context, NSD, nsd_id) - if template_db.get('template_source') == 'inline': - self.delete_nsd(context, nsd_id) + try: + template_db = self._get_resource(context, NSD, nsd_id) + if template_db.get('template_source') == 'inline': + self.delete_nsd(context, nsd_id) + except orm_exc.NoResultFound: + pass def get_ns(self, context, ns_id, fields=None): ns_db = self._get_resource(context, NS, ns_id) diff --git a/tacker/extensions/nfvo_plugins/network_service.py b/tacker/extensions/nfvo_plugins/network_service.py index 65321180b..5fb0d4d8a 100644 --- a/tacker/extensions/nfvo_plugins/network_service.py +++ b/tacker/extensions/nfvo_plugins/network_service.py @@ -60,3 +60,7 @@ class NSDNotFound(exceptions.NotFound): class NSNotFound(exceptions.NotFound): message = _('NS %(ns_id)s could not be found') + + +class NSInUse(exceptions.InUse): + message = _('NS %(ns_id)s in use') diff --git a/tacker/nfvo/drivers/workflow/workflow_generator.py b/tacker/nfvo/drivers/workflow/workflow_generator.py index ec23bbc32..c5eb76738 100644 --- a/tacker/nfvo/drivers/workflow/workflow_generator.py +++ b/tacker/nfvo/drivers/workflow/workflow_generator.py @@ -96,6 +96,8 @@ class WorkflowGenerator(workflow_generator.WorkflowGeneratorBase): def _add_delete_vnf_tasks(self, ns, vnffg_ids=None): vnfds = ns['vnfd_details'] + vnf_attr = {'vnf': {'attributes': { + 'force': ns.get('force_delete', False)}}} task_dict = dict() for vnfd_name, vnfd_info in (vnfds).items(): nodes = vnfd_info['instances'] @@ -104,6 +106,7 @@ class WorkflowGenerator(workflow_generator.WorkflowGeneratorBase): task_dict[task] = { 'action': 'tacker.delete_vnf vnf=<% $.vnf_id_{0}' '%>'.format(node), + 'input': {'body': vnf_attr}, } if vnffg_ids and len(vnffg_ids): task_dict[task].update({'join': 'all'}) @@ -271,5 +274,6 @@ class WorkflowGenerator(workflow_generator.WorkflowGeneratorBase): ns_dict['vnffg_details'] = vnffg_ids self.definition[self.wf_identifier]['tasks'].update( self._add_delete_vnffg_task(ns_dict)) + ns_dict['force_delete'] = ns.get('force_delete', False) self.definition[self.wf_identifier]['tasks'].update( self._add_delete_vnf_tasks(ns_dict, vnffg_ids)) diff --git a/tacker/nfvo/nfvo_plugin.py b/tacker/nfvo/nfvo_plugin.py index a924216be..6df88947e 100644 --- a/tacker/nfvo/nfvo_plugin.py +++ b/tacker/nfvo/nfvo_plugin.py @@ -31,6 +31,7 @@ from toscaparser.tosca_template import ToscaTemplate from tacker._i18n import _ from tacker.common import driver_manager +from tacker.common import exceptions from tacker.common import log from tacker.common import utils from tacker import context as t_context @@ -896,15 +897,23 @@ class NfvoPlugin(nfvo_db_plugin.NfvoPluginDb, vnffg_db.VnffgPluginDbMixin, raise cs.ParamYAMLInputMissing() @log.log - def delete_ns(self, context, ns_id): + def delete_ns(self, context, ns_id, ns=None): + # Extract "force_delete" from request's body + force_delete = False + if ns and ns.get('ns', {}).get('attributes', {}).get('force'): + force_delete = ns['ns'].get('attributes').get('force') + if force_delete and not context.is_admin: + LOG.warning("force delete is admin only operation") + raise exceptions.AdminRequired(reason="Admin only operation") ns = super(NfvoPlugin, self).get_ns(context, ns_id) LOG.debug("Deleting ns: %s", ns) vim_res = self.vim_client.get_vim(context, ns['vim_id']) - super(NfvoPlugin, self).delete_ns_pre(context, ns_id) + super(NfvoPlugin, self).delete_ns_pre(context, ns_id, force_delete) driver_type = vim_res['vim_type'] workflow = None try: if ns['vnf_ids']: + ns['force_delete'] = force_delete workflow = self._vim_drivers.invoke( driver_type, 'prepare_and_create_workflow', @@ -969,11 +978,12 @@ class NfvoPlugin(nfvo_db_plugin.NfvoPluginDb, vnffg_db.VnffgPluginDbMixin, workflow_id=workflow['id'], auth_dict=self.get_auth_dict(context)) super(NfvoPlugin, self).delete_ns_post(context, ns_id, exec_obj, - error_reason) + error_reason, + force_delete=force_delete) if workflow: self.spawn_n(_delete_ns_wait, ns['id'], mistral_execution.id) else: super(NfvoPlugin, self).delete_ns_post( - context, ns_id, None, None) + context, ns_id, None, None, force_delete=force_delete) return ns['id'] diff --git a/tacker/tests/unit/nfvo/drivers/workflow/test_workflow_generator.py b/tacker/tests/unit/nfvo/drivers/workflow/test_workflow_generator.py index df49b00aa..74c188b01 100644 --- a/tacker/tests/unit/nfvo/drivers/workflow/test_workflow_generator.py +++ b/tacker/tests/unit/nfvo/drivers/workflow/test_workflow_generator.py @@ -174,9 +174,13 @@ def get_dummy_create_workflow(): 'on-success': [{'delete_vnf_VNF1': '<% $.status_VNF1=' '"ERROR" %>'}]}, 'delete_vnf_VNF1': { - 'action': 'tacker.delete_vnf vnf=<% $.vnf_id_VNF1%>'}, + 'action': 'tacker.delete_vnf vnf=<% $.vnf_id_VNF1%>', + 'input': {'body': {'vnf': {'attributes': { + 'force': False}}}}}, 'delete_vnf_VNF2': { - 'action': 'tacker.delete_vnf vnf=<% $.vnf_id_VNF2%>'}}, + 'action': 'tacker.delete_vnf vnf=<% $.vnf_id_VNF2%>', + 'input': {'body': {'vnf': {'attributes': { + 'force': False}}}}}}, 'type': 'direct', 'output': { 'status_VNF1': '<% $.status_VNF1 %>', 'status_VNF2': '<% $.status_VNF2 %>', @@ -268,9 +272,13 @@ def get_dummy_create_vnffg_ns_workflow(): '<% task(create_ns_VNF2).result.vnf.id %>'}, 'on-success': ['wait_vnf_active_VNF2']}, 'delete_vnf_VNF1': { - 'action': 'tacker.delete_vnf vnf=<% $.vnf_id_VNF1%>'}, + 'action': 'tacker.delete_vnf vnf=<% $.vnf_id_VNF1%>', + 'input': {'body': {'vnf': {'attributes': { + 'force': False}}}}}, 'delete_vnf_VNF2': { - 'action': 'tacker.delete_vnf vnf=<% $.vnf_id_VNF2%>'}}, + 'action': 'tacker.delete_vnf vnf=<% $.vnf_id_VNF2%>', + 'input': {'body': {'vnf': {'attributes': { + 'force': False}}}}}}, 'type': 'direct', 'output': { 'status_VNF1': '<% $.status_VNF1 %>', @@ -301,7 +309,9 @@ def get_dummy_delete_workflow(): 'input': ['vnf_id_VNF1'], 'tasks': { 'delete_vnf_VNF1': { - 'action': 'tacker.delete_vnf vnf=<% $.vnf_id_VNF1%>'}}, + 'action': 'tacker.delete_vnf vnf=<% $.vnf_id_VNF1%>', + 'input': {'body': {'vnf': {'attributes': { + 'force': False}}}}}}, 'type': 'direct'}} @@ -312,7 +322,9 @@ def get_dummy_delete_vnffg_ns_workflow(): 'tasks': { 'delete_vnf_VNF1': { 'join': 'all', - 'action': 'tacker.delete_vnf vnf=<% $.vnf_id_VNF1%>'}, + 'action': 'tacker.delete_vnf vnf=<% $.vnf_id_VNF1%>', + 'input': {'body': {'vnf': {'attributes': { + 'force': False}}}}}, 'delete_vnffg_VNFFG1': { 'action': 'tacker.delete_vnffg vnffg=' '<% $.VNFFG1 %>', diff --git a/tacker/tests/unit/nfvo/test_nfvo_plugin.py b/tacker/tests/unit/nfvo/test_nfvo_plugin.py index bf23766d1..b382e5b85 100644 --- a/tacker/tests/unit/nfvo/test_nfvo_plugin.py +++ b/tacker/tests/unit/nfvo/test_nfvo_plugin.py @@ -21,6 +21,7 @@ from oslo_utils import uuidutils from mock import patch +from tacker.common import exceptions from tacker import context from tacker.db.common_services import common_services_db_plugin from tacker.db.nfvo import nfvo_db @@ -1043,13 +1044,13 @@ class TestNfvoPlugin(db_base.SqlTestCase): session.flush() return nsd_template - def _insert_dummy_ns(self): + def _insert_dummy_ns(self, status='ACTIVE'): session = self.context.session ns = ns_db.NS( id='ba6bf017-f6f7-45f1-a280-57b073bf78ea', name='dummy_ns', tenant_id='ad7ebc56538745a08ef7c5e97f8bd437', - status='ACTIVE', + status=status, nsd_id='eb094833-995e-49f0-a047-dfb56aaf7c4e', vim_id='6261579e-d6f3-49ad-8bc3-a9cb974778ff', description='dummy_ns_description', @@ -1269,4 +1270,57 @@ class TestNfvoPlugin(db_base.SqlTestCase): self.nfvo_plugin.delete_ns(self.context, DUMMY_NS_2) mock_delete_ns_post.assert_called_with( - self.context, DUMMY_NS_2, None, None) + self.context, DUMMY_NS_2, None, None, force_delete=False) + + @mock.patch.object(nfvo_plugin.NfvoPlugin, 'get_auth_dict') + @mock.patch.object(vim_client.VimClient, 'get_vim') + @mock.patch.object(nfvo_plugin.NfvoPlugin, '_get_by_name') + def test_delete_ns_force(self, mock_get_by_name, + mock_get_vim, mock_auth_dict): + self._insert_dummy_vim() + self._insert_dummy_ns_template() + self._insert_dummy_ns(status='PENDING_DELETE') + mock_auth_dict.return_value = { + 'auth_url': 'http://127.0.0.1', + 'token': 'DummyToken', + 'project_domain_name': 'dummy_domain', + 'project_name': 'dummy_project' + } + nsattr = {'ns': {'attributes': {'force': True}}} + + with patch.object(TackerManager, 'get_service_plugins') as \ + mock_plugins: + mock_plugins.return_value = {'VNFM': FakeVNFMPlugin()} + mock_get_by_name.return_value = get_by_name() + result = self.nfvo_plugin.delete_ns(self.context, + 'ba6bf017-f6f7-45f1-a280-57b073bf78ea', ns=nsattr) + self.assertIsNotNone(result) + + @mock.patch.object(nfvo_plugin.NfvoPlugin, 'get_auth_dict') + @mock.patch.object(vim_client.VimClient, 'get_vim') + @mock.patch.object(nfvo_plugin.NfvoPlugin, '_get_by_name') + def test_delete_ns_force_non_admin(self, mock_get_by_name, + mock_get_vim, mock_auth_dict): + self._insert_dummy_vim() + self._insert_dummy_ns_template() + self._insert_dummy_ns(status='PENDING_DELETE') + mock_auth_dict.return_value = { + 'auth_url': 'http://127.0.0.1', + 'token': 'DummyToken', + 'project_domain_name': 'dummy_domain', + 'project_name': 'dummy_project' + } + nsattr = {'ns': {'attributes': {'force': True}}} + + with patch.object(TackerManager, 'get_service_plugins') as \ + mock_plugins: + mock_plugins.return_value = {'VNFM': FakeVNFMPlugin()} + mock_get_by_name.return_value = get_by_name() + non_admin_context = context.Context(user_id=None, + tenant_id=None, + is_admin=False) + self.assertRaises(exceptions.AdminRequired, + self.nfvo_plugin.delete_ns, + non_admin_context, + 'ba6bf017-f6f7-45f1-a280-57b073bf78ea', + ns=nsattr)