From b4f357f2b3a6c064c0fafd9e6529b37dfce05075 Mon Sep 17 00:00:00 2001 From: tpatil Date: Thu, 5 Dec 2019 07:47:47 +0000 Subject: [PATCH] Add heal vnf instance API Implemented heal vnf instance API. * POST /vnflcm/v1/vnf_instances/{vnf_instance_id}/heal} Co-authored-By: Ajay Parja Co-authored-By: Nitin Uikey Co-authored-By: Shubham Potale Change-Id: I38bdac64a8a3eb3f8880085b5a77db97e5a1a8f2 Blueprint: support-etsi-nfv-specs --- tacker/api/schemas/vnf_lcm.py | 16 + tacker/api/vnflcm/v1/controller.py | 34 +- tacker/api/vnflcm/v1/router.py | 7 + tacker/common/exceptions.py | 4 + tacker/conductor/conductor_server.py | 3 + tacker/conductor/conductorrpc/vnf_lcm_rpc.py | 11 + tacker/objects/heal_vnf_request.py | 18 +- tacker/objects/instantiate_vnf_req.py | 202 ++++++++++++ tacker/policies/vnf_lcm.py | 11 + .../unit/conductor/test_conductor_server.py | 15 + .../unit/objects/test_instantiate_vnf_req.py | 282 ++++++++++++++++ tacker/tests/unit/vnflcm/test_controller.py | 137 ++++++++ .../tests/unit/vnflcm/test_vnflcm_driver.py | 270 ++++++++++++++- .../openstack/test_openstack_driver.py | 312 +++++++++++++++++- tacker/vnflcm/vnflcm_driver.py | 122 ++++++- tacker/vnfm/infra_drivers/abstract_driver.py | 31 ++ .../kubernetes/kubernetes_driver.py | 11 + tacker/vnfm/infra_drivers/noop.py | 11 + .../vnfm/infra_drivers/openstack/openstack.py | 157 +++++++++ 19 files changed, 1641 insertions(+), 13 deletions(-) create mode 100644 tacker/tests/unit/objects/test_instantiate_vnf_req.py diff --git a/tacker/api/schemas/vnf_lcm.py b/tacker/api/schemas/vnf_lcm.py index 9356737c1..40ab08863 100644 --- a/tacker/api/schemas/vnf_lcm.py +++ b/tacker/api/schemas/vnf_lcm.py @@ -210,3 +210,19 @@ terminate = { 'required': ['terminationType'], 'additionalProperties': False, } + +heal = { + 'type': 'object', + 'properties': { + 'cause': {'type': 'string', 'maxLength': 255}, + 'vnfcInstanceId': { + 'type': 'array', + "items": { + "type": "string", + 'format': 'uuid' + } + } + + }, + 'additionalProperties': False, +} diff --git a/tacker/api/vnflcm/v1/controller.py b/tacker/api/vnflcm/v1/controller.py index 4648b5040..fe9bf5437 100644 --- a/tacker/api/vnflcm/v1/controller.py +++ b/tacker/api/vnflcm/v1/controller.py @@ -269,8 +269,40 @@ class VnfLcmController(wsgi.Controller): vnf_instance = self._get_vnf_instance(context, id) self._terminate(context, vnf_instance, body) + @check_vnf_state(action="heal", + instantiation_state=[fields.VnfInstanceState.INSTANTIATED], + task_state=[None]) + def _heal(self, context, vnf_instance, request_body): + req_body = utils.convert_camelcase_to_snakecase(request_body) + heal_vnf_request = objects.HealVnfRequest(context=context, **req_body) + inst_vnf_info = vnf_instance.instantiated_vnf_info + vnfc_resource_info_ids = [vnfc_resource_info.id for + vnfc_resource_info in inst_vnf_info.vnfc_resource_info] + + for vnfc_id in heal_vnf_request.vnfc_instance_id: + # check if vnfc_id exists in vnfc_resource_info + if vnfc_id not in vnfc_resource_info_ids: + msg = _("Vnfc id %(vnfc_id)s not present in vnf instance " + "%(id)s") + raise webob.exc.HTTPBadRequest( + explanation=msg % {"vnfc_id": vnfc_id, + "id": vnf_instance.id}) + + vnf_instance.task_state = fields.VnfInstanceTaskState.HEALING + vnf_instance.save() + + self.rpc_api.heal(context, vnf_instance, heal_vnf_request) + + @wsgi.response(http_client.ACCEPTED) + @wsgi.expected_errors((http_client.BAD_REQUEST, http_client.FORBIDDEN, + http_client.NOT_FOUND, http_client.CONFLICT)) + @validation.schema(vnf_lcm.heal) def heal(self, request, id, body): - raise webob.exc.HTTPNotImplemented() + context = request.environ['tacker.context'] + context.can(vnf_lcm_policies.VNFLCM % 'heal') + + vnf_instance = self._get_vnf_instance(context, id) + self._heal(context, vnf_instance, body) def create_resource(): diff --git a/tacker/api/vnflcm/v1/router.py b/tacker/api/vnflcm/v1/router.py index 9cd9d04d7..9e77fd9f0 100644 --- a/tacker/api/vnflcm/v1/router.py +++ b/tacker/api/vnflcm/v1/router.py @@ -70,6 +70,13 @@ class VnflcmAPIRouter(wsgi.Router): "/vnf_instances/{id}/instantiate", methods, controller, default_resource) + # Allowed methods on + # /vnflcm/v1/vnf_instances/{vnfInstanceId}/heal resource + methods = {"POST": "heal"} + self._setup_route(mapper, + "/vnf_instances/{id}/heal", + methods, controller, default_resource) + # Allowed methods on # /vnflcm/v1/vnf_instances/{vnfInstanceId}/terminate resource methods = {"POST": "terminate"} diff --git a/tacker/common/exceptions.py b/tacker/common/exceptions.py index bcd1b5d79..1e2c0cdf0 100644 --- a/tacker/common/exceptions.py +++ b/tacker/common/exceptions.py @@ -266,6 +266,10 @@ class VnfPreInstantiationFailed(TackerException): "%(error)s") +class VnfHealFailed(TackerException): + message = _("Heal Vnf failed for vnf %(id)s, error: %(error)s") + + class OrphanedObjectError(TackerException): msg_fmt = _('Cannot call %(method)s on orphaned %(objtype)s object') diff --git a/tacker/conductor/conductor_server.py b/tacker/conductor/conductor_server.py index ce1aabb4a..a322a19cb 100644 --- a/tacker/conductor/conductor_server.py +++ b/tacker/conductor/conductor_server.py @@ -438,6 +438,9 @@ class Conductor(manager.Manager): LOG.error("Failed to update usage_state of vnf package %s", vnf_package.id) + def heal(self, context, vnf_instance, heal_vnf_request): + self.vnflcm_driver.heal_vnf(context, vnf_instance, heal_vnf_request) + def init(args, **kwargs): CONF(args=args, project='tacker', diff --git a/tacker/conductor/conductorrpc/vnf_lcm_rpc.py b/tacker/conductor/conductorrpc/vnf_lcm_rpc.py index 1a934555d..0ec085531 100644 --- a/tacker/conductor/conductorrpc/vnf_lcm_rpc.py +++ b/tacker/conductor/conductorrpc/vnf_lcm_rpc.py @@ -49,3 +49,14 @@ class VNFLcmRPCAPI(object): return rpc_method(context, 'terminate', vnf_instance=vnf_instance, terminate_vnf_req=terminate_vnf_req) + + def heal(self, context, vnf_instance, heal_vnf_request, cast=True): + serializer = objects_base.TackerObjectSerializer() + + client = rpc.get_client(self.target, version_cap=None, + serializer=serializer) + cctxt = client.prepare() + rpc_method = cctxt.cast if cast else cctxt.call + return rpc_method(context, 'heal', + vnf_instance=vnf_instance, + heal_vnf_request=heal_vnf_request) diff --git a/tacker/objects/heal_vnf_request.py b/tacker/objects/heal_vnf_request.py index f00ae987c..fb0b064bd 100644 --- a/tacker/objects/heal_vnf_request.py +++ b/tacker/objects/heal_vnf_request.py @@ -13,6 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. +from oslo_utils import versionutils + from tacker.objects import base from tacker.objects import fields @@ -33,10 +35,20 @@ class HealVnfAdditionalParams(base.TackerObject): class HealVnfRequest(base.TackerObject): # Version 1.0: Initial version - VERSION = '1.0' + # Version 1.1: Added vnf_instance_id + VERSION = '1.1' fields = { - 'cause': fields.StringField(), + 'vnfc_instance_id': fields.ListOfStringsField(nullable=True, + default=[]), + 'cause': fields.StringField(nullable=True, default=None), 'additional_params': fields.ListOfObjectsField( - 'HealVnfAdditionalParams') + 'HealVnfAdditionalParams', default=[]) } + + def obj_make_compatible(self, primitive, target_version): + super(HealVnfRequest, self).obj_make_compatible(primitive, + target_version) + target_version = versionutils.convert_version_to_tuple(target_version) + if target_version < (1, 1) and 'vnfc_instance_id' in primitive: + del primitive['vnfc_instance_id'] diff --git a/tacker/objects/instantiate_vnf_req.py b/tacker/objects/instantiate_vnf_req.py index 76f5d08d7..4f1858a5a 100644 --- a/tacker/objects/instantiate_vnf_req.py +++ b/tacker/objects/instantiate_vnf_req.py @@ -22,6 +22,176 @@ from tacker.objects import fields LOG = logging.getLogger(__name__) +def _get_vim_connection_info(vnf_instance): + vim_connection_infos = [] + + for vim_conn_info in vnf_instance.vim_connection_info: + new_vim_conn_info = vim_conn_info.obj_clone() + vim_connection_infos.append(new_vim_conn_info) + + return vim_connection_infos + + +def _get_ext_managed_virtual_links(vnf_instantiated_info): + ext_managed_virtual_links = [] + + for ext_mng_vl_info in \ + vnf_instantiated_info.ext_managed_virtual_link_info: + network_resource = ext_mng_vl_info.network_resource + + ext_mng_vl_data = objects.ExtManagedVirtualLinkData( + id=ext_mng_vl_info.id, + vnf_virtual_link_desc_id=ext_mng_vl_info.vnf_virtual_link_desc_id, + resource_id=network_resource.resource_id) + + ext_managed_virtual_links.append(ext_mng_vl_data) + + return ext_managed_virtual_links + + +def _get_cp_instance_id(ext_virtual_link_info_id, vnf_instantiated_info): + cp_instances = [] + + for vnf_vl_res_info in \ + vnf_instantiated_info.vnf_virtual_link_resource_info: + if ext_virtual_link_info_id == \ + vnf_vl_res_info.vnf_virtual_link_desc_id: + for vnf_link_port in vnf_vl_res_info.vnf_link_ports: + cp_instances.append(vnf_link_port.cp_instance_id) + + return cp_instances + + +def _get_cp_data_from_vnfc_resource_info(cp_instance_list, + vnf_instantiated_info): + vnfc_cp_infos = [] + + for vnfc_resource_info in vnf_instantiated_info.vnfc_resource_info: + for vnfc_cp_info in vnfc_resource_info.vnfc_cp_info: + if vnfc_cp_info.id in cp_instance_list: + vnfc_cp_infos.append(vnfc_cp_info) + + return vnfc_cp_infos + + +def _get_link_ports(vnfc_cp_info_list, vnf_instantiated_info): + ext_link_ports = [] + + for vnfc_cp_info in vnfc_cp_info_list: + for ext_vl_info in vnf_instantiated_info.ext_virtual_link_info: + for ext_vl_port in ext_vl_info.ext_link_ports: + if ext_vl_port.id != vnfc_cp_info.vnf_ext_cp_id: + continue + + resource_handle = ext_vl_port.resource_handle.obj_clone() + + ext_link_port_data = objects.ExtLinkPortData( + id=ext_vl_port.id, + resource_handle=resource_handle) + ext_link_ports.append(ext_link_port_data) + break + + return ext_link_ports + + +def _get_cp_protocol_data_list(ext_cp_info): + cp_protocol_data_list = [] + + def _get_ip_addresses(ip_addresses): + ip_addresses = [] + for ip_address in ip_addresses: + # TODO(nitin-uikey): How to determine num_dynamic_addresses + # back from InstantiatedVnfInfo->IpAddress. + ip_address_data = IpAddress( + type=ip_address.type, + subnet_id=ip_address.subnet_id, + fixed_addresses=ip_address.addresses) + + ip_addresses.append(ip_address_data) + + return ip_addresses + + for cp_protocol_info in ext_cp_info.cp_protocol_info: + if cp_protocol_info.ip_over_ethernet: + ip_addresses = _get_ip_addresses(cp_protocol_info. + ip_over_ethernet.ip_addresses) + + ip_over_ethernet_data = objects.IpOverEthernetAddressData( + mac_address=cp_protocol_info.ip_over_ethernet.mac_address, + ip_addresses=ip_addresses) + else: + ip_over_ethernet_data = None + + cp_protocol_data = objects.CpProtocolData( + layer_protocol=cp_protocol_info.layer_protocol, + ip_over_ethernet=ip_over_ethernet_data) + + cp_protocol_data_list.append(cp_protocol_data) + + return cp_protocol_data_list + + +def _get_cp_ids(vnfc_cp_info_list, vnf_instantiated_info): + + ext_cps = [] + for vnfc_cp_info in vnfc_cp_info_list: + for ext_cp_info in vnf_instantiated_info.ext_cp_info: + if ext_cp_info.cpd_id != vnfc_cp_info.cpd_id: + continue + + ext_cp_configs = [] + vnf_ext_cp_data = objects.VnfExtCpData() + vnf_ext_cp_data.cpd_id = ext_cp_info.cpd_id + + cp_protocol_data_list = _get_cp_protocol_data_list(ext_cp_info) + # TODO(nitin-uikey) set cp_instance_id + # and prepare 1-N objects of VnfExtCpConfig + vnf_ext_cp_config = objects.VnfExtCpConfig( + link_port_id=vnfc_cp_info.vnf_ext_cp_id, + cp_protocol_data=cp_protocol_data_list) + + ext_cp_configs.append(vnf_ext_cp_config) + + vnf_ext_cp_data = objects.VnfExtCpData( + cpd_id=ext_cp_info.cpd_id, + cp_config=ext_cp_configs) + ext_cps.append(vnf_ext_cp_data) + break + + return ext_cps + + +def _get_ext_virtual_link_data(vnf_instantiated_info): + + ext_virtual_links = [] + + for ext_vl_info in \ + vnf_instantiated_info.ext_virtual_link_info: + resource_handle = ext_vl_info.resource_handle + ext_vl_data = objects.ExtVirtualLinkData( + id=ext_vl_info.id, + resource_id=resource_handle.resource_id) + + # call vnf virtual link resource info + cp_instances = _get_cp_instance_id(ext_vl_info.id, + vnf_instantiated_info) + + # get cp info from vnfcresources + vnfc_cp_infos = _get_cp_data_from_vnfc_resource_info(cp_instances, + vnf_instantiated_info) + + ext_vl_data.ext_link_ports = _get_link_ports(vnfc_cp_infos, + vnf_instantiated_info) + + # assign the data to extcp info and link port + ext_vl_data.ext_cps = _get_cp_ids(vnfc_cp_infos, + vnf_instantiated_info) + + ext_virtual_links.append(ext_vl_data) + + return ext_virtual_links + + @base.TackerObjectRegistry.register class InstantiateVnfRequest(base.TackerObject): # Version 1.0: Initial version @@ -87,6 +257,38 @@ class InstantiateVnfRequest(base.TackerObject): ext_virtual_links=ext_virtual_links, additional_params=additional_params) + @classmethod + def from_vnf_instance(cls, vnf_instance): + + vnf_instantiated_info = vnf_instance.instantiated_vnf_info + + # Vim connection info + vim_connection_info = _get_vim_connection_info(vnf_instance) + + # Flavour id + flavour_id = vnf_instantiated_info.flavour_id + + # Instantiation level + instantiation_level_id = vnf_instantiated_info.instantiation_level_id + + # Externally managed virtual links + ext_managed_virtual_links = _get_ext_managed_virtual_links( + vnf_instantiated_info) + + # External virtual links + ext_virtual_links = _get_ext_virtual_link_data(vnf_instantiated_info) + + additional_params = vnf_instantiated_info.additional_params + + instantiate_vnf_request = cls(flavour_id=flavour_id, + instantiation_level_id=instantiation_level_id, + ext_managed_virtual_links=ext_managed_virtual_links, + vim_connection_info=vim_connection_info, + ext_virtual_links=ext_virtual_links, + additional_params=additional_params) + + return instantiate_vnf_request + @base.TackerObjectRegistry.register class ExtManagedVirtualLinkData(base.TackerObject): diff --git a/tacker/policies/vnf_lcm.py b/tacker/policies/vnf_lcm.py index caec50577..4ccb26c43 100644 --- a/tacker/policies/vnf_lcm.py +++ b/tacker/policies/vnf_lcm.py @@ -65,6 +65,17 @@ rules = [ 'path': '/vnflcm/v1/vnf_instances/{vnfInstanceId}/terminate' } ] + ), + policy.DocumentedRuleDefault( + name=VNFLCM % 'heal', + check_str=base.RULE_ADMIN_OR_OWNER, + description="Heal a VNF instance.", + operations=[ + { + 'method': 'POST', + 'path': '/vnflcm/v1/vnf_instances/{vnfInstanceId}/heal' + } + ] ) ] diff --git a/tacker/tests/unit/conductor/test_conductor_server.py b/tacker/tests/unit/conductor/test_conductor_server.py index 5e050b36e..8a36ec67c 100644 --- a/tacker/tests/unit/conductor/test_conductor_server.py +++ b/tacker/tests/unit/conductor/test_conductor_server.py @@ -342,6 +342,21 @@ class TestConductor(SqlTestCase): mock_log.error.assert_called_once_with(expected_msg, vnf_package_vnfd.package_uuid) + def test_heal_vnf_instance(self): + vnf_package_vnfd = self._create_and_upload_vnf_package() + vnf_instance_data = fake_obj.get_vnf_instance_data( + vnf_package_vnfd.vnfd_id) + vnf_instance = objects.VnfInstance(context=self.context, + **vnf_instance_data) + vnf_instance.create() + vnf_instance.instantiation_state = \ + fields.VnfInstanceState.INSTANTIATED + vnf_instance.save() + heal_vnf_req = objects.HealVnfRequest(cause="healing request") + self.conductor.heal(self.context, vnf_instance, heal_vnf_req) + self.vnflcm_driver.heal_vnf.assert_called_once_with( + self.context, mock.ANY, heal_vnf_req) + @mock.patch.object(os, 'remove') @mock.patch.object(shutil, 'rmtree') @mock.patch.object(os.path, 'exists') diff --git a/tacker/tests/unit/objects/test_instantiate_vnf_req.py b/tacker/tests/unit/objects/test_instantiate_vnf_req.py new file mode 100644 index 000000000..2c4d65efa --- /dev/null +++ b/tacker/tests/unit/objects/test_instantiate_vnf_req.py @@ -0,0 +1,282 @@ +# Copyright (c) 2020 NTT DATA +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from tacker import context +from tacker import objects +from tacker.objects import fields +from tacker.tests.unit import base +from tacker.tests import uuidsentinel + + +def get_instantiated_info_dict_for_ext_links_and_flavour_id(): + return {"instantiated_vnf_info": { + "additional_params": {'key1': 'value1'}, + "vnfc_resource_info": [{ + "vdu_id": "VDU1", + "vnfc_cp_info": [{ + "cp_protocol_info": [{ + "ip_over_ethernet": { + "mac_address": "fa:16:3e:0d:6f:71"}, + "layer_protocol": "IP_OVER_ETHERNET"}], + "vnf_ext_cp_id": None, + "cpd_id": "CP1", + "vnf_link_port_id": "a41417d8-5cba-4256-aa4a-c68b79bb2dc2", + "id": "350ab5d7-7cf0-4d24-aa2f-ec7e51149d56"}, { + "cp_protocol_info": [{ + "layer_protocol": "IP_OVER_ETHERNET"}], + "vnf_ext_cp_id": "413f4e46-21cf-41b1-be0f-de8d23f76cfe", + "cpd_id": "CP2", + "vnf_link_port_id": "b5f67448-87ee-4250-ad8b-514bc693f6c4", + "id": "2cab54a3-8ac5-4338-b586-b12e4ff1a89f"}, { + "vnf_link_port_id": "124f1923-9de1-4729-8d4e-bc0613066dc4", + "id": "b3341e7b-20bb-4ae5-8be9-b00cf79a9395", + "vnf_ext_cp_id": None, + "cpd_id": "CP3"}], + "compute_resource": { + "vim_level_resource_type": "OS::Nova::Server", + "resource_id": "c83ac219-376a-4016-b8b2-50c3707f71c4"}, + "id": "0688be82-05a6-4b00-b869-d4719baf4f42", + "storage_resource_ids": [], + "metadata": {}}], + "ext_virtual_link_info": [{ + "resource_handle": { + "vim_level_resource_type": None, + "resource_id": "7ec01a11-e584-404a-88bd-39a56b63e29c"}, + "id": "net0"}, { + "resource_handle": { + "vim_level_resource_type": None, + "resource_id": "dc67ee99-e963-44e2-a152-f0fb492eae76"}, + "id": "external_network", + "ext_link_ports": [{ + "cp_instance_id": "f47a9e33-b31a-4290-828a-c7569c52bd0e", + "resource_handle": { + "vim_level_resource_type": "LINKPORT", + "resource_id": "67f7e772-0d31-4087-bf4c-2576fadcbdb7"}, + "id": "413f4e46-21cf-41b1-be0f-de8d23f76cfe"}]}], + "vnf_virtual_link_resource_info": [{ + "vnf_link_ports": [{ + "cp_instance_id": "b3341e7b-20bb-4ae5-8be9-b00cf79a9395", + "resource_handle": { + "vim_level_resource_type": "OS::Neutron::Port", + "resource_id": "87b567a0-783a-4c06-b0d8-cdafd40c51e3"}, + "id": "124f1923-9de1-4729-8d4e-bc0613066dc4"}], + "network_resource": { + "vim_level_resource_type": "OS::Neutron::Net", + "resource_id": "413f4e46-21cf-41b1-be0f-de8d23f76cfd"}, + "vnf_virtual_link_desc_id": "VL3", + "id": "84ffa547-1164-4e60-863d-c7dac770f824"}, { + "vnf_link_ports": [{ + "cp_instance_id": "2cab54a3-8ac5-4338-b586-b12e4ff1a89f", + "resource_handle": { + "vim_level_resource_type": "OS::Neutron::Port", + "resource_id": "413f4e46-21cf-41b1-be0f-de8d23f76cfe"}, + "id": "b5f67448-87ee-4250-ad8b-514bc693f6c4"}], + "network_resource": { + "vim_level_resource_type": "OS::Neutron::Net", + "resource_id": "dc67ee99-e963-44e2-a152-f0fb492eae76"}, + "vnf_virtual_link_desc_id": "external_network", + "id": "1050cce6-27ca-40df-b66e-160113fc4826"}, { + "vnf_link_ports": [{ + "cp_instance_id": "350ab5d7-7cf0-4d24-aa2f-ec7e51149d56", + "resource_handle": { + "vim_level_resource_type": "OS::Neutron::Port", + "resource_id": "2bca5538-edd6-4590-ab1a-552ae7d0f81b"}, + "id": "a41417d8-5cba-4256-aa4a-c68b79bb2dc2"}], + "network_resource": { + "vim_level_resource_type": "OS::Neutron::Net", + "resource_id": "7ec01a11-e584-404a-88bd-39a56b63e29c"}, + "vnf_virtual_link_desc_id": "net0", + "id": "5ad98d4d-4b81-4b1b-a88b-1f64b66e151b"}], + "ext_managed_virtual_link_info": [{ + "network_resource": { + "vim_level_resource_type": "OS::Neutron::Net", + "resource_id": "413f4e46-21cf-41b1-be0f-de8d23f76cfd"}, + "id": "net1", + "vnf_link_ports": [{ + "cp_instance_id": "b3341e7b-20bb-4ae5-8be9-b00cf79a9395", + "resource_handle": { + "vim_level_resource_type": "OS::Neutron::Port", + "resource_id": "87b567a0-783a-4c06-b0d8-cdafd40c51e3"}, + "id": "124f1923-9de1-4729-8d4e-bc0613066dc4"}], + "vnf_virtual_link_desc_id": "VL3"}], + "flavour_id": "simple", + "vnf_state": "STARTED", + "ext_cp_info": [{ + "cp_protocol_info": [{ + "ip_over_ethernet": { + "mac_address": "fa:16:3e:0d:6f:71"}, + "layer_protocol": "IP_OVER_ETHERNET"}], + "cpd_id": "CP1", + "id": "19f0aa71-9376-43dc-8e13-5e76e4bdc8bf", + "ext_link_port_id": None}, { + "cp_protocol_info": [{ + "layer_protocol": "IP_OVER_ETHERNET"}], + "cpd_id": "CP2", + "id": "f47a9e33-b31a-4290-828a-c7569c52bd0e", + "ext_link_port_id": None}] + } + } + + +class InstantiateVnfRequestTestCase(base.TestCase): + + def setUp(self): + super(InstantiateVnfRequestTestCase, self).setUp() + self.context = context.get_admin_context() + + def _create_vim_connection_info(self, **kwargs): + vim_connection_info = objects.VimConnectionInfo(**kwargs) + return [vim_connection_info] + + def _create_vnf_instance(self, vim_connection_info=None): + vnf_instance = objects.VnfInstance( + context=self.context, + vnf_instance_name="Sample vnf instance name", + vnf_instance_description="Sample vnf instance description", + vnfd_id=uuidsentinel.vnfd_id, + instantiation_state=fields.VnfInstanceState.INSTANTIATED, + vnf_provider='Company', + vnf_product_name='Sample VNF', + vnf_software_version='1.0', + vnfd_version='1.0', + tenant_id=uuidsentinel.tenant_id) + + if vim_connection_info: + vnf_instance.vim_connection_info = vim_connection_info + + return vnf_instance + + def test_from_vnf_instance_with_flavour(self): + """Map flavour id""" + + inst_vnf_request = objects.InstantiateVnfRequest(flavour_id="simple") + + instantiated_vnf_info = objects.InstantiatedVnfInfo( + flavour_id="simple") + + vnf_instance = self._create_vnf_instance() + vnf_instance.instantiated_vnf_info = instantiated_vnf_info + + inst_vnf_request_actual = objects.InstantiateVnfRequest.\ + from_vnf_instance(vnf_instance) + + self.compare_obj(inst_vnf_request, inst_vnf_request_actual) + + def test_from_vnf_instance_with_flavour_and_instantiation_level(self): + """Map flavour id and instantiation level""" + + inst_vnf_request = objects.InstantiateVnfRequest(flavour_id="simple", + instantiation_level_id="instantiation_level_1") + + instantiated_vnf_info = objects.InstantiatedVnfInfo( + flavour_id="simple", + instantiation_level_id="instantiation_level_1") + + vnf_instance = self._create_vnf_instance() + vnf_instance.instantiated_vnf_info = instantiated_vnf_info + + inst_vnf_request_actual = objects.InstantiateVnfRequest.\ + from_vnf_instance(vnf_instance) + + self.compare_obj(inst_vnf_request, inst_vnf_request_actual) + + def test_from_vnf_instance_with_ext_vl_and_ext_managed_vl(self): + """Map external and internal network information. + + Map following information: + a) ext_virtual_link_info + b) ext_managed_virtual_link_info + """ + + ext_vl_info = [{ + "ext_cps": [{ + "cp_config": [{ + "cp_protocol_data": [{ + "ip_over_ethernet": { + "mac_address": "fa:16:3e:0d:6f:71"}, + "layer_protocol": "IP_OVER_ETHERNET"}]}], + "cpd_id": "CP1"}], + "id": "net0", + "resource_id": "7ec01a11-e584-404a-88bd-39a56b63e29c"}, { + "ext_cps": [{ + "cp_config": [{ + "cp_protocol_data": [{ + "layer_protocol": "IP_OVER_ETHERNET"}], + "link_port_id": "413f4e46-21cf-41b1-be0f-de8d23f76cfe"}], + "cpd_id": "CP2"}], + "ext_link_ports": [{ + "id": "413f4e46-21cf-41b1-be0f-de8d23f76cfe", + "resource_handle": { + "resource_id": "67f7e772-0d31-4087-bf4c-2576fadcbdb7", + "vim_level_resource_type": "LINKPORT"}}], + "id": "external_network", + "resource_id": "dc67ee99-e963-44e2-a152-f0fb492eae76"}] + + ext_mg_vl = {"id": "net1", + "resource_id": "413f4e46-21cf-41b1-be0f-de8d23f76cfd", + "vnf_virtual_link_desc_id": "VL3"} + + vim_connection_info = [{ + "id": "6b0ff598-60d6-49b4-a907-a1111de52d92", + "vim_id": uuidsentinel.vim_id, + "vim_type": "ETSINFV.OPENSTACK_KEYSTONE.v_2", + "access_info": {}}] + + vnf_info_data = {'ext_managed_virtual_links': [ext_mg_vl], + 'ext_virtual_links': ext_vl_info, + 'vim_connection_info': vim_connection_info, + 'flavour_id': 'simple', + 'additional_params': {'key1': 'value1'}} + + inst_vnf_request = objects.InstantiateVnfRequest.obj_from_primitive( + vnf_info_data, self.context) + + response = get_instantiated_info_dict_for_ext_links_and_flavour_id() + + instantiated_vnf_info_dict = response.get('instantiated_vnf_info') + + instantiated_vnf_info = objects.InstantiatedVnfInfo.obj_from_primitive( + instantiated_vnf_info_dict, self.context) + + vnf_instance = self._create_vnf_instance( + self._create_vim_connection_info(**vim_connection_info[0])) + vnf_instance.instantiated_vnf_info = instantiated_vnf_info + + inst_vnf_req_actual = objects.InstantiateVnfRequest.from_vnf_instance( + vnf_instance) + + self.assertEqual(inst_vnf_request.flavour_id, + inst_vnf_req_actual.flavour_id) + + self.assertEqual(inst_vnf_request.instantiation_level_id, + inst_vnf_req_actual.instantiation_level_id) + + self.assertEqual(inst_vnf_request.additional_params, + inst_vnf_req_actual.additional_params) + + for expected, actual in zip(inst_vnf_request.ext_managed_virtual_links, + inst_vnf_req_actual.ext_managed_virtual_links): + self.compare_obj(expected, actual) + + for expected, actual in zip(inst_vnf_request.ext_virtual_links, + inst_vnf_req_actual.ext_virtual_links): + self.assertEqual(expected.id, actual.id) + self.assertEqual(expected.resource_id, actual.resource_id) + + for expected, actual in zip(inst_vnf_request.vim_connection_info, + inst_vnf_req_actual.vim_connection_info): + self.assertEqual(expected.id, actual.id) + self.assertEqual(expected.vim_id, actual.vim_id) + self.assertEqual(expected.vim_type, actual.vim_type) diff --git a/tacker/tests/unit/vnflcm/test_controller.py b/tacker/tests/unit/vnflcm/test_controller.py index 9f170c0f6..d38785160 100644 --- a/tacker/tests/unit/vnflcm/test_controller.py +++ b/tacker/tests/unit/vnflcm/test_controller.py @@ -835,3 +835,140 @@ class TestController(base.TestCase): "terminate while the vnf instance is in this state.") self.assertEqual(expected_msg % uuidsentinel.vnf_instance_id, resp.json['conflictingRequest']['message']) + + @mock.patch.object(objects.VnfInstance, "get_by_id") + @mock.patch.object(objects.VnfInstance, "save") + @mock.patch.object(VNFLcmRPCAPI, "heal") + @ddt.data({'cause': 'healing'}, {}) + def test_heal(self, body, mock_rpc_heal, mock_save, + mock_vnf_by_id): + vnf_instance_obj = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED) + mock_vnf_by_id.return_value = vnf_instance_obj + + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/heal' % uuidsentinel.vnf_instance_id) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + + resp = req.get_response(self.app) + + self.assertEqual(http_client.ACCEPTED, resp.status_code) + mock_rpc_heal.assert_called_once() + + def test_heal_cause_max_length_exceeded(self): + body = {'cause': 'A' * 256} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/heal' % uuidsentinel.vnf_instance_id) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + + resp = req.get_response(self.app) + self.assertEqual(http_client.BAD_REQUEST, resp.status_code) + + @mock.patch.object(objects.VnfInstance, "get_by_id") + def test_heal_incorrect_instantiated_state(self, mock_vnf_by_id): + vnf_instance_obj = fakes.return_vnf_instance( + fields.VnfInstanceState.NOT_INSTANTIATED) + mock_vnf_by_id.return_value = vnf_instance_obj + + body = {} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/heal' % uuidsentinel.vnf_instance_id) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + + resp = req.get_response(self.app) + self.assertEqual(http_client.CONFLICT, resp.status_code) + expected_msg = ("Vnf instance %s in instantiation_state " + "NOT_INSTANTIATED. Cannot heal while the vnf instance " + "is in this state.") + self.assertEqual(expected_msg % uuidsentinel.vnf_instance_id, + resp.json['conflictingRequest']['message']) + + @mock.patch.object(objects.VnfInstance, "get_by_id") + def test_heal_incorrect_task_state(self, mock_vnf_by_id): + vnf_instance_obj = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED, + task_state=fields.VnfInstanceTaskState.HEALING) + mock_vnf_by_id.return_value = vnf_instance_obj + + body = {} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/heal' % uuidsentinel.vnf_instance_id) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + + resp = req.get_response(self.app) + self.assertEqual(http_client.CONFLICT, resp.status_code) + expected_msg = ("Vnf instance %s in task_state " + "HEALING. Cannot heal while the vnf instance " + "is in this state.") + self.assertEqual(expected_msg % uuidsentinel.vnf_instance_id, + resp.json['conflictingRequest']['message']) + + @mock.patch.object(objects.VnfInstance, "get_by_id") + def test_heal_with_invalid_vnfc_id(self, mock_vnf_by_id): + vnf_instance_obj = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED) + mock_vnf_by_id.return_value = vnf_instance_obj + + body = {'vnfcInstanceId': [uuidsentinel.vnfc_instance_id]} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/heal' % uuidsentinel.vnf_instance_id) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + + resp = req.get_response(self.app) + self.assertEqual(http_client.BAD_REQUEST, resp.status_code) + expected_msg = "Vnfc id %s not present in vnf instance %s" + self.assertEqual(expected_msg % (uuidsentinel.vnfc_instance_id, + uuidsentinel.vnf_instance_id), resp.json['badRequest']['message']) + + @ddt.data('HEAD', 'PUT', 'DELETE', 'PATCH', 'GET') + def test_heal_invalid_http_method(self, method): + body = {} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/heal' % uuidsentinel.vnf_instance_id) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = method + + resp = req.get_response(self.app) + + self.assertEqual(http_client.METHOD_NOT_ALLOWED, resp.status_code) + + @ddt.data({'attribute': 'cause', 'value': 123, + 'expected_type': 'string'}, + {'attribute': 'cause', 'value': True, + 'expected_type': 'string'}, + {'attribute': 'vnfcInstanceId', 'value': 123, + 'expected_type': 'array'}, + {'attribute': 'vnfcInstanceId', 'value': True, + 'expected_type': 'array'}, + ) + @ddt.unpack + def test_heal_with_invalid_request_body( + self, attribute, value, expected_type): + body = {} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/29c770a3-02bc-4dfc-b4be-eb173ac00567/heal') + body.update({attribute: value}) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + exception = self.assertRaises( + exceptions.ValidationError, self.controller.heal, + req, body=body) + expected_message = \ + ("Invalid input for field/attribute {attribute}. Value: {value}. " + "{value} is not of type '{expected_type}'". + format(value=value, attribute=attribute, + expected_type=expected_type)) + + self.assertEqual(expected_message, exception.msg) diff --git a/tacker/tests/unit/vnflcm/test_vnflcm_driver.py b/tacker/tests/unit/vnflcm/test_vnflcm_driver.py index 12d0b3ab5..2e527156a 100644 --- a/tacker/tests/unit/vnflcm/test_vnflcm_driver.py +++ b/tacker/tests/unit/vnflcm/test_vnflcm_driver.py @@ -59,7 +59,7 @@ class FakeDriverManager(mock.Mock): if self.fail_method_name and \ self.fail_method_name == 'create_wait': raise InfraDriverException("create_wait failed") - if 'post_vnf_instantiation' in args: + elif 'post_vnf_instantiation' in args: pass if 'delete' in args: if self.fail_method_name and \ @@ -73,6 +73,18 @@ class FakeDriverManager(mock.Mock): if self.fail_method_name and \ self.fail_method_name == 'delete_vnf_resource': raise InfraDriverException("delete_vnf_resource failed") + elif 'heal_vnf' in args: + if self.fail_method_name and \ + self.fail_method_name == 'heal_vnf': + raise InfraDriverException("heal_vnf failed") + elif 'heal_vnf_wait' in args: + if self.fail_method_name and \ + self.fail_method_name == 'heal_vnf_wait': + raise InfraDriverException("heal_vnf_wait failed") + elif 'post_heal_vnf' in args: + if self.fail_method_name and \ + self.fail_method_name == 'post_heal_vnf': + raise InfraDriverException("post_heal_vnf failed") class FakeVimClient(mock.Mock): @@ -439,3 +451,259 @@ class TestVnflcmDriver(db_base.SqlTestCase): self.assertEqual("delete_vnf_resource failed", str(error)) self.assertEqual(2, mock_vnf_instance_save.call_count) self.assertEqual(3, self._vnf_manager.invoke.call_count) + + @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') + @mock.patch.object(vim_client.VimClient, "get_vim") + @mock.patch.object(objects.VnfResource, "create") + @mock.patch.object(objects.VnfResource, "destroy") + @mock.patch.object(objects.VnfResourceList, "get_by_vnf_instance_id") + @mock.patch.object(objects.VnfInstance, "save") + @mock.patch('tacker.vnflcm.vnflcm_driver.LOG') + def test_heal_vnf_without_vnfc_instance(self, mock_log, mock_save, + mock_vnf_resource_list, mock_resource_destroy, + mock_resource_create, mock_vim, mock_vnf_package_vnfd): + vnf_package_vnfd = fakes.return_vnf_package_vnfd() + vnf_package_id = vnf_package_vnfd.package_uuid + mock_vnf_package_vnfd.return_value = vnf_package_vnfd + + fake_csar = os.path.join(self.temp_dir, vnf_package_id) + cfg.CONF.set_override('vnf_package_csar_path', self.temp_dir, + group='vnf_package') + base_path = os.path.dirname(os.path.abspath(__file__)) + sample_vnf_package_zip = os.path.join( + base_path, "../../etc/samples/sample_vnf_package_csar.zip") + extracted_zip_path = fake_csar + zipfile.ZipFile(sample_vnf_package_zip, 'r').extractall( + extracted_zip_path) + + mock_vnf_resource_list.return_value = [fakes.return_vnf_resource()] + # Heal as per SOL003 i.e. without vnfcInstanceId + heal_vnf_req = objects.HealVnfRequest() + + vim_obj = {'vim_id': uuidsentinel.vim_id, + 'vim_name': 'fake_vim', + 'vim_type': 'openstack', + 'vim_auth': { + 'auth_url': 'http://localhost/identity', + 'password': 'test_pw', + 'username': 'test_user', + 'project_name': 'test_project'}} + + mock_vim.return_value = vim_obj + + vnf_instance = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED) + + vnf_instance.instantiated_vnf_info.instance_id =\ + uuidsentinel.instance_id + self._mock_vnf_manager() + driver = vnflcm_driver.VnfLcmDriver() + driver.heal_vnf(self.context, vnf_instance, heal_vnf_req) + self.assertEqual(1, mock_save.call_count) + # vnf resource software images will be deleted during + # deleting vnf instance. + self.assertEqual(1, mock_resource_destroy.call_count) + # Vnf resource software images will be created during + # instantiation. + self.assertEqual(1, mock_resource_create.call_count) + # Invoke will be called 7 times, 3 for deleting the vnf + # resources and 4 during instantiation. + self.assertEqual(7, self._vnf_manager.invoke.call_count) + expected_msg = ("Request received for healing vnf '%s' " + "is completed successfully") + mock_log.info.assert_called_with(expected_msg, + vnf_instance.id) + + shutil.rmtree(fake_csar) + + @mock.patch.object(objects.VnfInstance, "save") + @mock.patch('tacker.vnflcm.vnflcm_driver.LOG') + def test_heal_vnf_without_vnfc_instance_infra_delete_fail(self, mock_log, + mock_save): + # Heal as per SOL003 i.e. without vnfcInstanceId + heal_vnf_req = objects.HealVnfRequest() + + vnf_instance = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED) + + vnf_instance.instantiated_vnf_info.instance_id =\ + uuidsentinel.instance_id + self._mock_vnf_manager(fail_method_name='delete') + driver = vnflcm_driver.VnfLcmDriver() + self.assertRaises(exceptions.VnfHealFailed, + driver.heal_vnf, self.context, vnf_instance, heal_vnf_req) + self.assertEqual(1, mock_save.call_count) + self.assertEqual(1, self._vnf_manager.invoke.call_count) + self.assertEqual(fields.VnfInstanceTaskState.ERROR, + vnf_instance.task_state) + expected_msg = ('Failed to delete vnf resources for vnf instance %s ' + 'before respawning. The vnf is in inconsistent ' + 'state. Error: delete failed') + mock_log.error.assert_called_with(expected_msg % vnf_instance.id) + + @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') + @mock.patch.object(vim_client.VimClient, "get_vim") + @mock.patch.object(objects.VnfResource, "create") + @mock.patch.object(objects.VnfResource, "destroy") + @mock.patch.object(objects.VnfResourceList, "get_by_vnf_instance_id") + @mock.patch.object(objects.VnfInstance, "save") + @mock.patch('tacker.vnflcm.vnflcm_driver.LOG') + def test_heal_vnf_without_vnfc_instance_infra_instantiate_vnf_fail(self, + mock_log, mock_save, mock_vnf_resource_list, + mock_resource_destroy, mock_resource_create, mock_vim, + mock_vnf_package_vnfd): + vnf_package_vnfd = fakes.return_vnf_package_vnfd() + vnf_package_id = vnf_package_vnfd.package_uuid + mock_vnf_package_vnfd.return_value = vnf_package_vnfd + + fake_csar = os.path.join(self.temp_dir, vnf_package_id) + cfg.CONF.set_override('vnf_package_csar_path', self.temp_dir, + group='vnf_package') + base_path = os.path.dirname(os.path.abspath(__file__)) + sample_vnf_package_zip = os.path.join( + base_path, "../../etc/samples/sample_vnf_package_csar.zip") + extracted_zip_path = fake_csar + zipfile.ZipFile(sample_vnf_package_zip, 'r').extractall( + extracted_zip_path) + + mock_vnf_resource_list.return_value = [fakes.return_vnf_resource()] + # Heal as per SOL003 i.e. without vnfcInstanceId + heal_vnf_req = objects.HealVnfRequest() + + vnf_instance = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED) + + vnf_instance.instantiated_vnf_info.instance_id =\ + uuidsentinel.instance_id + self._mock_vnf_manager(fail_method_name='instantiate_vnf') + driver = vnflcm_driver.VnfLcmDriver() + self.assertRaises(exceptions.VnfHealFailed, + driver.heal_vnf, self.context, vnf_instance, heal_vnf_req) + self.assertEqual(1, mock_save.call_count) + # vnf resource software images will be deleted during + # deleting vnf instance. + self.assertEqual(1, mock_resource_destroy.call_count) + # Vnf resource software images will be created during + # instantiation. + self.assertEqual(1, mock_resource_create.call_count) + + self.assertEqual(5, self._vnf_manager.invoke.call_count) + self.assertEqual(fields.VnfInstanceTaskState.ERROR, + vnf_instance.task_state) + expected_msg = ('Failed to instantiate vnf instance %s ' + 'after termination. The vnf is in inconsistent ' + 'state. Error: Vnf instantiation failed for vnf %s, ' + 'error: instantiate_vnf failed') + mock_log.error.assert_called_with(expected_msg % (vnf_instance.id, + vnf_instance.id)) + + @mock.patch.object(objects.VnfInstance, "save") + @mock.patch('tacker.vnflcm.vnflcm_driver.LOG') + def test_heal_vnf_with_vnfc_instance(self, mock_log, mock_save): + heal_vnf_req = objects.HealVnfRequest(vnfc_instance_id=[ + uuidsentinel.vnfc_instance_id_1]) + + vnf_instance = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED, + task_state=fields.VnfInstanceTaskState.HEALING) + + self._mock_vnf_manager() + driver = vnflcm_driver.VnfLcmDriver() + driver.heal_vnf(self.context, vnf_instance, heal_vnf_req) + self.assertEqual(1, mock_save.call_count) + self.assertEqual(3, self._vnf_manager.invoke.call_count) + + self.assertEqual(None, vnf_instance.task_state) + expected_msg = ("Request received for healing vnf '%s' " + "is completed successfully") + mock_log.info.assert_called_with(expected_msg, + vnf_instance.id) + + @mock.patch.object(objects.VnfInstance, "save") + @mock.patch('tacker.vnflcm.vnflcm_driver.LOG') + def test_heal_vnf_with_infra_heal_vnf_fail(self, mock_log, mock_save): + heal_vnf_req = objects.HealVnfRequest(vnfc_instance_id=[ + uuidsentinel.vnfc_instance_id_1]) + + vnf_instance = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED, + task_state=fields.VnfInstanceTaskState.HEALING) + + self._mock_vnf_manager(fail_method_name='heal_vnf') + driver = vnflcm_driver.VnfLcmDriver() + self.assertRaises(exceptions.VnfHealFailed, + driver.heal_vnf, self.context, vnf_instance, + heal_vnf_req) + self.assertEqual(1, mock_save.call_count) + self.assertEqual(1, self._vnf_manager.invoke.call_count) + + self.assertEqual(fields.VnfInstanceTaskState.ERROR, + vnf_instance.task_state) + expected_msg = ("Failed to heal vnf %(id)s in infra driver. " + "Error: %(error)s") + mock_log.error.assert_called_with(expected_msg, + {'id': vnf_instance.id, 'error': 'heal_vnf failed'}) + + @mock.patch.object(objects.VnfInstance, "save") + @mock.patch('tacker.vnflcm.vnflcm_driver.LOG') + def test_heal_vnf_with_infra_heal_vnf_wait_fail(self, mock_log, + mock_save): + heal_vnf_req = objects.HealVnfRequest(vnfc_instance_id=[ + uuidsentinel.vnfc_instance_id_1]) + + vnf_instance = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED, + task_state=fields.VnfInstanceTaskState.HEALING) + + vnf_instance.instantiated_vnf_info.instance_id =\ + uuidsentinel.instance_id + self._mock_vnf_manager(fail_method_name='heal_vnf_wait') + driver = vnflcm_driver.VnfLcmDriver() + # It won't raise any exception if infra driver raises + # heal_vnf_wait because there is a possibility the vnfc + # resources could go into inconsistent state so it would + # proceed further and call post_heal_vnf with a hope + # it will work and vnflcm can update the vnfc resources + # properly and hence the _vnf_manager.invoke.call_count + # should be 3 instead of 2. + driver.heal_vnf(self.context, vnf_instance, heal_vnf_req) + self.assertEqual(1, mock_save.call_count) + self.assertEqual(3, self._vnf_manager.invoke.call_count) + + self.assertEqual(None, vnf_instance.task_state) + expected_msg = ('Failed to update vnf %(id)s resources for ' + 'instance%(instance)s. Error: %(error)s') + mock_log.error.assert_called_with(expected_msg, + {'id': vnf_instance.id, + 'instance': vnf_instance.instantiated_vnf_info.instance_id, + 'error': 'heal_vnf_wait failed'}) + + @mock.patch.object(objects.VnfInstance, "save") + @mock.patch('tacker.vnflcm.vnflcm_driver.LOG') + def test_heal_vnf_with_infra_post_heal_vnf_fail(self, mock_log, + mock_save): + heal_vnf_req = objects.HealVnfRequest(vnfc_instance_id=[ + uuidsentinel.vnfc_instance_id_1]) + + vnf_instance = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED, + task_state=fields.VnfInstanceTaskState.HEALING) + + vnf_instance.instantiated_vnf_info.instance_id =\ + uuidsentinel.instance_id + self._mock_vnf_manager(fail_method_name='post_heal_vnf') + driver = vnflcm_driver.VnfLcmDriver() + self.assertRaises(exceptions.VnfHealFailed, + driver.heal_vnf, self.context, vnf_instance, heal_vnf_req) + self.assertEqual(1, mock_save.call_count) + self.assertEqual(3, self._vnf_manager.invoke.call_count) + + self.assertEqual(fields.VnfInstanceTaskState.ERROR, + vnf_instance.task_state) + expected_msg = ('Failed to store updated resources information for ' + 'instance %(instance)s for vnf %(id)s. ' + 'Error: %(error)s') + mock_log.error.assert_called_with(expected_msg, + {'instance': vnf_instance.instantiated_vnf_info.instance_id, + 'id': vnf_instance.id, + 'error': 'post_heal_vnf failed'}) diff --git a/tacker/tests/unit/vnfm/infra_drivers/openstack/test_openstack_driver.py b/tacker/tests/unit/vnfm/infra_drivers/openstack/test_openstack_driver.py index 06a6e73da..c541a1c32 100644 --- a/tacker/tests/unit/vnfm/infra_drivers/openstack/test_openstack_driver.py +++ b/tacker/tests/unit/vnfm/infra_drivers/openstack/test_openstack_driver.py @@ -22,6 +22,7 @@ import tempfile from tacker.common import exceptions from tacker import context from tacker.extensions import vnfm +from tacker import objects from tacker.tests.common import helpers from tacker.tests.unit import base from tacker.tests.unit.db import utils @@ -77,6 +78,39 @@ class TestOpenStack(base.FixturedTestCase): self.requests_mock.register_uri('GET', url, json=json, headers=self.json_headers) + def _response_in_stack_get(self, id, stack_status='CREATE_COMPLETE'): + # response for heat_client's stack_get() + url = self.heat_url + '/stacks/' + id + + json = {'stack': fd_utils.get_dummy_stack(status=stack_status)} + self.requests_mock.register_uri('GET', url, json=json, + headers=self.json_headers) + + def _response_in_stack_update(self, id): + # response for heat_client's stack_patch() + url = self.heat_url + '/stacks/' + id + + self.requests_mock.register_uri('PATCH', url, + headers=self.json_headers) + + def _response_resource_mark_unhealthy(self, id, resources, + raise_exception=False): + # response for heat_client's heatclient.resource_mark_unhealthy + if not resources: + return + + class MyException(Exception): + pass + + for resource in resources: + url = os.path.join(self.heat_url, 'stacks', id, + 'myStack/60f83b5e/resources', resource['resource_name']) + if raise_exception: + self.requests_mock.register_uri('PATCH', url, + exc=MyException("Any stuff")) + else: + self.requests_mock.register_uri('PATCH', url) + def test_create_wait(self): self._response_in_wait_until_stack_ready(["CREATE_IN_PROGRESS", "CREATE_COMPLETE"]) @@ -235,10 +269,16 @@ class TestOpenStack(base.FixturedTestCase): region_name=None) self.assertEqual(dummy_event['id'], event_id) - def _response_in_resource_get_list(self): + def _response_in_resource_get_list(self, stack_id=None, + resources=None): # response for heat_client's resource_get_list() - url = self.heat_url + '/stacks/' + self.stack_id + '/resources' - json = {'resources': [fd_utils.get_dummy_resource()]} + + if stack_id: + url = self.heat_url + '/stacks/' + stack_id + '/resources' + else: + url = self.heat_url + '/stacks/' + self.stack_id + '/resources' + resources = resources or [fd_utils.get_dummy_resource()] + json = {'resources': resources} self.requests_mock.register_uri('GET', url, json=json, headers=self.json_headers) @@ -763,3 +803,269 @@ class TestOpenStack(base.FixturedTestCase): vnf_instance.instantiated_vnf_info. ext_managed_virtual_link_info[0].vnf_link_ports[0]. resource_handle.resource_id) + + def test_heal_vnf_instance(self): + v_s_resource_info = fd_utils.get_virtual_storage_resource_info( + desc_id="storage1") + + storage_resource_ids = [v_s_resource_info.id] + vnfc_resource_info = fd_utils.get_vnfc_resource_info(vdu_id="VDU_VNF", + storage_resource_ids=storage_resource_ids) + + inst_vnf_info = fd_utils.get_vnf_instantiated_info( + virtual_storage_resource_info=[v_s_resource_info], + vnfc_resource_info=[vnfc_resource_info]) + + vnf_instance = fd_utils.get_vnf_instance_object( + instantiated_vnf_info=inst_vnf_info) + + vim_connection_info = fd_utils.get_vim_connection_info_object() + + heal_vnf_request = objects.HealVnfRequest( + vnfc_instance_id=[vnfc_resource_info.id], + cause="healing request") + + # Mock various heat APIs that will be called by heatclient + # during the process of heal_vnf. + resources = [{ + 'resource_name': vnfc_resource_info.vdu_id, + 'resource_type': vnfc_resource_info.compute_resource. + vim_level_resource_type, + 'physical_resource_id': vnfc_resource_info.compute_resource. + resource_id}, { + 'resource_name': v_s_resource_info.virtual_storage_desc_id, + 'resource_type': v_s_resource_info.storage_resource. + vim_level_resource_type, + 'physical_resource_id': v_s_resource_info.storage_resource. + resource_id}] + + self._response_in_stack_get(inst_vnf_info.instance_id) + self._response_in_resource_get_list(inst_vnf_info.instance_id, + resources=resources) + self._responses_in_stack_list(inst_vnf_info.instance_id, + resources=resources) + self._response_in_stack_update(inst_vnf_info.instance_id) + self._response_resource_mark_unhealthy(inst_vnf_info.instance_id, + resources=resources) + + self.openstack.heal_vnf( + self.context, vnf_instance, vim_connection_info, heal_vnf_request) + + history = self.requests_mock.request_history + patch_req = [req.url for req in history if req.method == 'PATCH'] + # Total 3 times patch should be called, 2 for marking resources + # as unhealthy, and 1 for updating stack + self.assertEqual(3, len(patch_req)) + + def test_heal_vnf_instance_resource_mark_unhealthy_error(self): + vnfc_resource_info = fd_utils.get_vnfc_resource_info(vdu_id="VDU_VNF") + + inst_vnf_info = fd_utils.get_vnf_instantiated_info( + vnfc_resource_info=[vnfc_resource_info]) + + vnf_instance = fd_utils.get_vnf_instance_object( + instantiated_vnf_info=inst_vnf_info) + + vim_connection_info = fd_utils.get_vim_connection_info_object() + + heal_vnf_request = objects.HealVnfRequest( + vnfc_instance_id=[vnfc_resource_info.id], + cause="healing request") + + # Mock various heat APIs that will be called by heatclient + # during the process of heal_vnf. + resources = [{ + 'resource_name': vnfc_resource_info.vdu_id, + 'resource_type': vnfc_resource_info.compute_resource. + vim_level_resource_type, + 'physical_resource_id': vnfc_resource_info.compute_resource. + resource_id}] + + self._response_in_stack_get(inst_vnf_info.instance_id) + self._response_in_resource_get_list(inst_vnf_info.instance_id, + resources=resources) + self._responses_in_stack_list(inst_vnf_info.instance_id, + resources=resources) + self._response_resource_mark_unhealthy(inst_vnf_info.instance_id, + resources=resources, raise_exception=True) + + result = self.assertRaises(exceptions.VnfHealFailed, + self.openstack.heal_vnf, self.context, vnf_instance, + vim_connection_info, heal_vnf_request) + + expected_msg = ("Failed to mark stack '%(id)s' resource as unhealthy " + "for resource '%(resource_name)s'") % { + 'id': inst_vnf_info.instance_id, + 'resource_name': resources[0]['resource_name']} + self.assertIn(expected_msg, str(result)) + + history = self.requests_mock.request_history + patch_req = [req.url for req in history if req.method == 'PATCH'] + # only one time patch method be called for marking vdu resource + # as unhealthy + self.assertEqual(1, len(patch_req)) + + def test_heal_vnf_instance_incorrect_stack_status(self): + inst_vnf_info = fd_utils.get_vnf_instantiated_info() + + vnf_instance = fd_utils.get_vnf_instance_object( + instantiated_vnf_info=inst_vnf_info) + + vim_connection_info = fd_utils.get_vim_connection_info_object() + + heal_vnf_request = objects.HealVnfRequest( + vnfc_instance_id=[uuidsentinel.vnfc_resource_id], + cause="healing request") + + # Mock various heat APIs that will be called by heatclient + # during the process of heal_vnf. + self._response_in_stack_get(inst_vnf_info.instance_id, + stack_status='UPDATE_IN_PROGRESS') + + result = self.assertRaises(exceptions.VnfHealFailed, + self.openstack.heal_vnf, self.context, vnf_instance, + vim_connection_info, heal_vnf_request) + + expected_msg = ("Healing of vnf instance %s is possible only when " + "stack %s status is CREATE_COMPLETE,UPDATE_COMPLETE, " + "current stack status is UPDATE_IN_PROGRESS") + self.assertIn(expected_msg % (vnf_instance.id, + inst_vnf_info.instance_id), str(result)) + + def test_heal_vnf_wait(self): + inst_vnf_info = fd_utils.get_vnf_instantiated_info() + + vnf_instance = fd_utils.get_vnf_instance_object( + instantiated_vnf_info=inst_vnf_info) + + vim_connection_info = fd_utils.get_vim_connection_info_object() + + # Mock various heat APIs that will be called by heatclient + # during the process of heal_vnf_wait. + self._response_in_wait_until_stack_ready(["UPDATE_IN_PROGRESS", + "UPDATE_COMPLETE"]) + + stack = self.openstack.heal_vnf_wait( + self.context, vnf_instance, vim_connection_info) + self.assertEqual('UPDATE_COMPLETE', stack.stack_status) + + def test_heal_vnf_wait_fail(self): + inst_vnf_info = fd_utils.get_vnf_instantiated_info() + + vnf_instance = fd_utils.get_vnf_instance_object( + instantiated_vnf_info=inst_vnf_info) + + vim_connection_info = fd_utils.get_vim_connection_info_object() + + # Mock various heat APIs that will be called by heatclient + # during the process of heal_vnf_wait. + self._response_in_wait_until_stack_ready(["UPDATE_IN_PROGRESS"]) + self.openstack.STACK_RETRIES = 1 + result = self.assertRaises(vnfm.VNFHealWaitFailed, + self.openstack.heal_vnf_wait, self.context, vnf_instance, + vim_connection_info) + + expected_msg = ("VNF Heal action is not completed within 10 seconds " + "on stack %s") % inst_vnf_info.instance_id + self.assertIn(expected_msg, str(result)) + + def test_post_heal_vnf(self): + v_s_resource_info = fd_utils.get_virtual_storage_resource_info( + desc_id="storage1") + + storage_resource_ids = [v_s_resource_info.id] + vnfc_resource_info = fd_utils.get_vnfc_resource_info(vdu_id="VDU_VNF", + storage_resource_ids=storage_resource_ids) + + inst_vnf_info = fd_utils.get_vnf_instantiated_info( + virtual_storage_resource_info=[v_s_resource_info], + vnfc_resource_info=[vnfc_resource_info]) + + vnfc_resource_info.metadata['stack_id'] = inst_vnf_info.instance_id + vnf_instance = fd_utils.get_vnf_instance_object( + instantiated_vnf_info=inst_vnf_info) + + vim_connection_info = fd_utils.get_vim_connection_info_object() + + heal_vnf_request = objects.HealVnfRequest( + vnfc_instance_id=[vnfc_resource_info.id], + cause="healing request") + + # Change the physical_resource_id of both the resources, so + # that we can check it's updated in vnf instance after + # post_heal_vnf call. + resources = [{ + 'resource_name': vnfc_resource_info.vdu_id, + 'resource_type': vnfc_resource_info.compute_resource. + vim_level_resource_type, + 'physical_resource_id': uuidsentinel.compute_resource_id_new}, { + 'resource_name': v_s_resource_info.virtual_storage_desc_id, + 'resource_type': v_s_resource_info.storage_resource. + vim_level_resource_type, + 'physical_resource_id': uuidsentinel.storage_resource_id_new}] + + # Mock various heat APIs that will be called by heatclient + # during the process of heal_vnf. + self._responses_in_stack_list(inst_vnf_info.instance_id, + resources=resources) + + v_s_resource_info_old = v_s_resource_info.obj_clone() + vnfc_resource_info_old = vnfc_resource_info.obj_clone() + + self.openstack.post_heal_vnf(self.context, vnf_instance, + vim_connection_info, heal_vnf_request) + + vnfc_resource_info_new = vnf_instance.instantiated_vnf_info.\ + vnfc_resource_info[0] + v_s_resource_info_new = vnf_instance.instantiated_vnf_info.\ + virtual_storage_resource_info[0] + + # Compare the resource_id, it should be updated with the new ones. + self.assertNotEqual(vnfc_resource_info_old.compute_resource. + resource_id, vnfc_resource_info_new.compute_resource.resource_id) + self.assertEqual(uuidsentinel.compute_resource_id_new, + vnfc_resource_info_new.compute_resource.resource_id) + + self.assertNotEqual(v_s_resource_info_old.storage_resource.resource_id, + v_s_resource_info_new.storage_resource.resource_id) + self.assertEqual(uuidsentinel.storage_resource_id_new, + v_s_resource_info_new.storage_resource.resource_id) + + def test_post_heal_vnf_fail(self): + vnfc_resource_info = fd_utils.get_vnfc_resource_info() + + inst_vnf_info = fd_utils.get_vnf_instantiated_info( + vnfc_resource_info=[vnfc_resource_info]) + + vnfc_resource_info.metadata['stack_id'] = uuidsentinel.stack_id + vnf_instance = fd_utils.get_vnf_instance_object( + instantiated_vnf_info=inst_vnf_info) + + vim_connection_info = fd_utils.get_vim_connection_info_object() + + heal_vnf_request = objects.HealVnfRequest( + vnfc_instance_id=[vnfc_resource_info.id], + cause="healing request") + + # Change the physical_resource_id of both the resources, so + # that we can check it's updated in vnf instance after + # post_heal_vnf call. + resources = [{ + 'resource_name': vnfc_resource_info.vdu_id, + 'resource_type': vnfc_resource_info.compute_resource. + vim_level_resource_type, + 'physical_resource_id': uuidsentinel.compute_resource_id_new}] + + # Mock various heat APIs that will be called by heatclient + # during the process of heal_vnf. + self._responses_in_stack_list(inst_vnf_info.instance_id, + resources=resources) + + result = self.assertRaises(exceptions.VnfHealFailed, + self.openstack.post_heal_vnf, self.context, vnf_instance, + vim_connection_info, heal_vnf_request) + + expected_msg = ("Heal Vnf failed for vnf %s, error: Failed to find " + "stack_id %s") % (vnf_instance.id, + uuidsentinel.stack_id) + self.assertEqual(expected_msg, str(result)) diff --git a/tacker/vnflcm/vnflcm_driver.py b/tacker/vnflcm/vnflcm_driver.py index 2e0df6e8f..95c2a9ff3 100644 --- a/tacker/vnflcm/vnflcm_driver.py +++ b/tacker/vnflcm/vnflcm_driver.py @@ -264,8 +264,8 @@ class VnfLcmDriver(abstract_driver.VnfInstanceAbstractDriver): encodeutils.exception_to_unicode(exp)) def _delete_vnf_instance_resources(self, context, vnf_instance, - vim_connection_info, - terminate_vnf_req=None): + vim_connection_info, terminate_vnf_req=None, + update_instantiated_state=True): if vnf_instance.instantiated_vnf_info and \ vnf_instance.instantiated_vnf_info.instance_id: @@ -285,9 +285,10 @@ class VnfLcmDriver(abstract_driver.VnfInstanceAbstractDriver): 'delete', plugin=self, context=context, vnf_id=instance_id, auth_attr=access_info) - vnf_instance.instantiation_state = \ - fields.VnfInstanceState.NOT_INSTANTIATED - vnf_instance.save() + if update_instantiated_state: + vnf_instance.instantiation_state = \ + fields.VnfInstanceState.NOT_INSTANTIATED + vnf_instance.save() self._vnf_manager.invoke(vim_connection_info.vim_type, 'delete_wait', plugin=self, context=context, @@ -304,3 +305,114 @@ class VnfLcmDriver(abstract_driver.VnfInstanceAbstractDriver): vnf_resource=vnf_resource) vnf_resource.destroy(context) + + def _heal_vnf(self, context, vnf_instance, vim_connection_info, + heal_vnf_request): + inst_vnf_info = vnf_instance.instantiated_vnf_info + try: + self._vnf_manager.invoke( + vim_connection_info.vim_type, 'heal_vnf', + context=context, vnf_instance=vnf_instance, + vim_connection_info=vim_connection_info, + heal_vnf_request=heal_vnf_request) + except Exception as exp: + with excutils.save_and_reraise_exception() as exc_ctxt: + exc_ctxt.reraise = False + LOG.error("Failed to heal vnf %(id)s in infra driver. " + "Error: %(error)s", {"id": vnf_instance.id, "error": + encodeutils.exception_to_unicode(exp)}) + raise exceptions.VnfHealFailed(id=vnf_instance.id, + error=encodeutils.exception_to_unicode(exp)) + + try: + self._vnf_manager.invoke( + vim_connection_info.vim_type, 'heal_vnf_wait', + context=context, vnf_instance=vnf_instance, + vim_connection_info=vim_connection_info) + except Exception as exp: + LOG.error("Failed to update vnf %(id)s resources for instance" + "%(instance)s. Error: %(error)s", + {'id': vnf_instance.id, 'instance': + inst_vnf_info.instance_id, 'error': + encodeutils.exception_to_unicode(exp)}) + + try: + self._vnf_manager.invoke( + vim_connection_info.vim_type, 'post_heal_vnf', + context=context, vnf_instance=vnf_instance, + vim_connection_info=vim_connection_info, + heal_vnf_request=heal_vnf_request) + self._vnf_instance_update(context, vnf_instance, task_state=None) + except Exception as exp: + with excutils.save_and_reraise_exception() as exc_ctxt: + exc_ctxt.reraise = False + LOG.error("Failed to store updated resources information for " + "instance %(instance)s for vnf %(id)s. " + "Error: %(error)s", + {'id': vnf_instance.id, 'instance': + inst_vnf_info.instance_id, 'error': + encodeutils.exception_to_unicode(exp)}) + raise exceptions.VnfHealFailed(id=vnf_instance.id, + error=encodeutils.exception_to_unicode(exp)) + + def _respawn_vnf(self, context, vnf_instance, vim_connection_info, + heal_vnf_request): + try: + self._delete_vnf_instance_resources(context, vnf_instance, + vim_connection_info, update_instantiated_state=False) + except Exception as exc: + with excutils.save_and_reraise_exception() as exc_ctxt: + exc_ctxt.reraise = False + err_msg = ("Failed to delete vnf resources for vnf instance " + "%(id)s before respawning. The vnf is in " + "inconsistent state. Error: %(error)s") + LOG.error(err_msg % {"id": vnf_instance.id, + "error": six.text_type(exc)}) + raise exceptions.VnfHealFailed(id=vnf_instance.id, + error=encodeutils.exception_to_unicode(exc)) + + # InstantiateVnfRequest is not stored in the db as it's mapped + # to InstantiatedVnfInfo version object. Convert InstantiatedVnfInfo + # version object to InstantiateVnfRequest so that vnf can be + # instantiated. + + instantiate_vnf_request = objects.InstantiateVnfRequest.\ + from_vnf_instance(vnf_instance) + + try: + self._instantiate_vnf(context, vnf_instance, vim_connection_info, + instantiate_vnf_request) + except Exception as exc: + with excutils.save_and_reraise_exception() as exc_ctxt: + exc_ctxt.reraise = False + err_msg = ("Failed to instantiate vnf instance " + "%(id)s after termination. The vnf is in " + "inconsistent state. Error: %(error)s") + LOG.error(err_msg % {"id": vnf_instance.id, + "error": six.text_type(exc)}) + raise exceptions.VnfHealFailed(id=vnf_instance.id, + error=encodeutils.exception_to_unicode(exc)) + + self._vnf_instance_update(context, vnf_instance, + instantiation_state=fields.VnfInstanceState.INSTANTIATED, + task_state=None) + + @log.log + @revert_to_error_task_state + def heal_vnf(self, context, vnf_instance, heal_vnf_request): + LOG.info("Request received for healing vnf '%s'", vnf_instance.id) + vim_info = vnflcm_utils._get_vim(context, + vnf_instance.vim_connection_info) + + vim_connection_info = objects.VimConnectionInfo.obj_from_primitive( + vim_info, context) + + if not heal_vnf_request.vnfc_instance_id: + self._respawn_vnf(context, vnf_instance, vim_connection_info, + heal_vnf_request) + else: + self._heal_vnf(context, vnf_instance, vim_connection_info, + heal_vnf_request) + + LOG.info("Request received for healing vnf '%s' is completed " + "successfully", vnf_instance.id) diff --git a/tacker/vnfm/infra_drivers/abstract_driver.py b/tacker/vnfm/infra_drivers/abstract_driver.py index 8c1daed9b..f22dc101f 100644 --- a/tacker/vnfm/infra_drivers/abstract_driver.py +++ b/tacker/vnfm/infra_drivers/abstract_driver.py @@ -102,3 +102,34 @@ class VnfAbstractDriver(extensions.PluginInterface): def post_vnf_instantiation(self, context, vnf_instance, vim_connection_info): pass + + @abc.abstractmethod + def heal_vnf(self, context, vnf_instance, vim_connection_info, + heal_vnf_request): + """Heal vnf + + :param context: A RequestContext + :param vnf_instance: tacker.objects.VnfInstance to be healed + :vim_info: Credentials to initialize Vim connection + :heal_vnf_request: tacker.objects.HealVnfRequest object containing + parameters passed in the heal request + """ + pass + + @abc.abstractmethod + def heal_vnf_wait(self, context, vnf_instance, vim_connection_info): + """Check vnf is healed successfully""" + pass + + @abc.abstractmethod + def post_heal_vnf(self, context, vnf_instance, vim_connection_info, + heal_vnf_request): + """Update resource_id for each vnfc resources + + :param context: A RequestContext + :param vnf_instance: tacker.objects.VnfInstance to be healed + :vim_info: Credentials to initialize Vim connection + :heal_vnf_request: tacker.objects.HealVnfRequest object containing + parameters passed in the heal request + """ + pass diff --git a/tacker/vnfm/infra_drivers/kubernetes/kubernetes_driver.py b/tacker/vnfm/infra_drivers/kubernetes/kubernetes_driver.py index 9f0d0a93b..80cba146c 100644 --- a/tacker/vnfm/infra_drivers/kubernetes/kubernetes_driver.py +++ b/tacker/vnfm/infra_drivers/kubernetes/kubernetes_driver.py @@ -567,3 +567,14 @@ class Kubernetes(abstract_driver.VnfAbstractDriver, def post_vnf_instantiation(self, context, vnf_instance, vim_connection_info): raise NotImplementedError() + + def heal_vnf(self, context, vnf_instance, vim_connection_info, + heal_vnf_request): + raise NotImplementedError() + + def heal_vnf_wait(self, context, vnf_instance, vim_connection_info): + raise NotImplementedError() + + def post_heal_vnf(self, context, vnf_instance, vim_connection_info, + heal_vnf_request): + raise NotImplementedError() diff --git a/tacker/vnfm/infra_drivers/noop.py b/tacker/vnfm/infra_drivers/noop.py index 10afccf37..7874f26bc 100644 --- a/tacker/vnfm/infra_drivers/noop.py +++ b/tacker/vnfm/infra_drivers/noop.py @@ -93,3 +93,14 @@ class VnfNoop(abstract_driver.VnfAbstractDriver): def post_vnf_instantiation(self, context, vnf_instance, vim_connection_info): pass + + def heal_vnf(self, context, vnf_instance, vim_connection_info, + heal_vnf_request): + pass + + def heal_vnf_wait(self, context, vnf_instance, vim_connection_info): + pass + + def post_heal_vnf(self, context, vnf_instance, vim_connection_info, + heal_vnf_request): + pass diff --git a/tacker/vnfm/infra_drivers/openstack/openstack.py b/tacker/vnfm/infra_drivers/openstack/openstack.py index d106c550a..c325618ae 100644 --- a/tacker/vnfm/infra_drivers/openstack/openstack.py +++ b/tacker/vnfm/infra_drivers/openstack/openstack.py @@ -796,3 +796,160 @@ class OpenStack(abstract_driver.VnfAbstractDriver, resource_details[id].update({'child_stack': child_stack}) return resource_details + + def _get_vnfc_resources_from_heal_request(self, inst_vnf_info, + heal_vnf_request): + if not heal_vnf_request.vnfc_instance_id: + # include all vnfc resources + return [resource for resource in inst_vnf_info.vnfc_resource_info] + + vnfc_resources = [] + for vnfc_resource in inst_vnf_info.vnfc_resource_info: + if vnfc_resource.id in heal_vnf_request.vnfc_instance_id: + vnfc_resources.append(vnfc_resource) + return vnfc_resources + + @log.log + def heal_vnf(self, context, vnf_instance, vim_connection_info, + heal_vnf_request): + access_info = vim_connection_info.access_info + region_name = access_info.get('region') + inst_vnf_info = vnf_instance.instantiated_vnf_info + heatclient = hc.HeatClient(access_info, region_name=region_name) + + def _get_storage_resources(vnfc_resource): + # Prepare list of storage resources to be marked unhealthy + for storage_id in vnfc_resource.storage_resource_ids: + for storage_res_info in inst_vnf_info. \ + virtual_storage_resource_info: + if storage_res_info.id == storage_id: + yield storage_res_info.virtual_storage_desc_id, \ + storage_res_info.storage_resource.resource_id + + def _get_vdu_resources(vnfc_resources): + # Prepare list of vdu resources to be marked unhealthy + vdu_resources = [] + for vnfc_resource in vnfc_resources: + resource_details = {"resource_name": vnfc_resource.vdu_id, + "physical_resource_id": + vnfc_resource.compute_resource.resource_id} + vdu_resources.append(resource_details) + + # Get storage resources + for resource_name, resource_id in \ + _get_storage_resources(vnfc_resource): + resource_details = {"resource_name": resource_name, + "physical_resource_id": resource_id} + vdu_resources.append(resource_details) + + return vdu_resources + + def _prepare_stack_resources_for_updation(vdu_resources, + stack_resources): + for resource in vdu_resources: + for stack_uuid, resources in stack_resources.items(): + res_details = resources.get(resource['resource_name']) + if res_details and res_details['physical_resource_id'] == \ + resource['physical_resource_id']: + yield stack_uuid, resource['resource_name'] + + def _resource_mark_unhealthy(): + vnfc_resources = self._get_vnfc_resources_from_heal_request( + inst_vnf_info, heal_vnf_request) + + vdu_resources = _get_vdu_resources(vnfc_resources) + stack_resources = self._get_stack_resources( + inst_vnf_info.instance_id, heatclient) + + cause = heal_vnf_request.cause or "Healing" + for stack_uuid, resource_name in \ + _prepare_stack_resources_for_updation( + vdu_resources, stack_resources): + try: + LOG.info("Marking resource %(resource)s as unhealthy for " + "stack %(stack)s for vnf instance %(id)s", + {"resource": resource_name, + "stack": stack_uuid, + "id": vnf_instance.id}) + + heatclient.resource_mark_unhealthy( + stack_id=stack_uuid, + resource_name=resource_name, mark_unhealthy=True, + resource_status_reason=cause) + except Exception as exp: + msg = ("Failed to mark stack '%(stack_id)s' resource as " + "unhealthy for resource '%(resource)s', " + "Error: %(error)s") + raise exceptions.VnfHealFailed(id=vnf_instance.id, + error=msg % {"stack_id": inst_vnf_info.instance_id, + "resource": resource_name, + "error": str(exp)}) + + def _get_stack_status(): + stack_statuses = ["CREATE_COMPLETE", "UPDATE_COMPLETE"] + stack = heatclient.get(inst_vnf_info.instance_id) + if stack.stack_status not in stack_statuses: + error = ("Healing of vnf instance %(id)s is possible only " + "when stack %(stack_id)s status is %(statuses)s, " + "current stack status is %(status)s") + raise exceptions.VnfHealFailed(id=vnf_instance.id, + error=error % {"id": vnf_instance.id, + "stack_id": inst_vnf_info.instance_id, + "statuses": ",".join(stack_statuses), + "status": stack.stack_status}) + + _get_stack_status() + _resource_mark_unhealthy() + + LOG.info("Updating stack %(stack)s for vnf instance %(id)s", + {"stack": inst_vnf_info.instance_id, "id": vnf_instance.id}) + + heatclient.update(stack_id=inst_vnf_info.instance_id, existing=True) + + @log.log + def heal_vnf_wait(self, context, vnf_instance, vim_connection_info): + """Check vnf is healed successfully""" + + access_info = vim_connection_info.access_info + region_name = access_info.get('region') + inst_vnf_info = vnf_instance.instantiated_vnf_info + stack = self._wait_until_stack_ready(inst_vnf_info.instance_id, + access_info, infra_cnst.STACK_UPDATE_IN_PROGRESS, + infra_cnst.STACK_UPDATE_COMPLETE, vnfm.VNFHealWaitFailed, + region_name=region_name) + return stack + + def post_heal_vnf(self, context, vnf_instance, vim_connection_info, + heal_vnf_request): + """Update resource_id for each vnfc resources + + :param context: A RequestContext + :param vnf_instance: tacker.objects.VnfInstance to be healed + :vim_info: Credentials to initialize Vim connection + :heal_vnf_request: tacker.objects.HealVnfRequest object containing + parameters passed in the heal request + """ + access_info = vim_connection_info.access_info + region_name = access_info.get('region') + + heatclient = hc.HeatClient(access_info, region_name) + inst_vnf_info = vnf_instance.instantiated_vnf_info + stack_resources = self._get_stack_resources(inst_vnf_info.instance_id, + heatclient) + + vnfc_resources = self._get_vnfc_resources_from_heal_request( + inst_vnf_info, heal_vnf_request) + for vnfc_res_info in vnfc_resources: + stack_id = vnfc_res_info.metadata.get("stack_id") + resources = stack_resources.get(stack_id) + if not resources: + # NOTE(tpatil): This could happen when heat child stacks + # and the stack_id stored in metadata of vnfc_res_info are + # not in sync. There is no point in syncing inconsistent + # resources information so exit with an error, + error = "Failed to find stack_id %s" % stack_id + raise exceptions.VnfHealFailed(id=vnf_instance.id, + error=error) + + self._update_vnfc_resource_info(vnf_instance, vnfc_res_info, + {stack_id: resources}, update_network_resource=False)