From 34b929e3e2162110436f46233db421887b3edc69 Mon Sep 17 00:00:00 2001 From: Hitomi Koba Date: Mon, 1 Sep 2025 16:35:50 +0900 Subject: [PATCH] Add toggle to recreate Cinder volumes in v1 Heal Extend v1 Heal to accept an optional additionalParams key that toggles recreation of Cinder volumes. Add two [vnf_lcm] options in tacker.conf: - heal_vnfc_block_storage: default when the request omits the key - heal_include_block_storage_key: key name in additionalParams If neither is set, existing behavior is unchanged. Implements: blueprint v1-heal-cinder-volume Change-Id: I29ca4dceac40d3d0fe1fcb1f6bf4c4f1729e883b Signed-off-by: Hitomi Koba --- api-ref/source/v1/parameters_vnflcm.yaml | 25 +++ .../vnflcm/heal-vnf-instance-request.json | 7 +- api-ref/source/v1/vnflcm.inc | 4 +- doc/source/user/etsi_vnf_healing.rst | 115 ++++++++++++- tacker/conf/vnf_lcm.py | 27 +++ .../openstack/test_heal_storage_toggle.py | 157 ++++++++++++++++++ .../vnfm/infra_drivers/openstack/openstack.py | 16 +- 7 files changed, 333 insertions(+), 18 deletions(-) create mode 100644 tacker/tests/unit/vnfm/infra_drivers/openstack/test_heal_storage_toggle.py diff --git a/api-ref/source/v1/parameters_vnflcm.yaml b/api-ref/source/v1/parameters_vnflcm.yaml index 27726540a..a531dbc70 100644 --- a/api-ref/source/v1/parameters_vnflcm.yaml +++ b/api-ref/source/v1/parameters_vnflcm.yaml @@ -702,6 +702,14 @@ grant_id: in: body required: false type: string +heal_additional_params: + description: | + Additional parameters passed by the NFVO as input to the healing process, + specific to the VNF being healed, as declared in the VNFD as part of + "HealVnfOpConfig". + in: body + required: false + type: key value pairs instantiated_vnf_info: description: | Information specific to an instantiated VNF instance. This attribute shall @@ -985,6 +993,23 @@ subscription_id_response: in: body required: true type: string +tacker_extension_heal_include_block_storage: + description: | + ** Admin-configurable parameter name. ** + Controls whether related Cinder block storage is recreated when + healing specified VNFC instance(s). + + The key name is configured by ``[vnf_lcm] heal_include_block_storage_key`` + in ``tacker.conf`` (default: ``tacker_extension_heal_include_block_storage``). + If the key is omitted, the default behavior is controlled by + ``[vnf_lcm] heal_vnfc_block_storage``. + + **Recommendation:** Because both the key name and the default behavior + depend on your deployment settings, it is recommended to confirm them + with your environment administrator. + in: body + required: false + type: boolean termination_type: description: | Indicates whether forceful or graceful termination is requested. diff --git a/api-ref/source/v1/samples/vnflcm/heal-vnf-instance-request.json b/api-ref/source/v1/samples/vnflcm/heal-vnf-instance-request.json index 86e7e45a4..80f262139 100644 --- a/api-ref/source/v1/samples/vnflcm/heal-vnf-instance-request.json +++ b/api-ref/source/v1/samples/vnflcm/heal-vnf-instance-request.json @@ -1,4 +1,7 @@ { "cause": "healing", - "vnfcInstanceId": ["c51c98dc-b918-4681-a9eb-4f32a57c4e08"] -} \ No newline at end of file + "vnfcInstanceId": ["c51c98dc-b918-4681-a9eb-4f32a57c4e08"], + "additionalParams": { + "tacker_extension_heal_include_block_storage": false + } +} diff --git a/api-ref/source/v1/vnflcm.inc b/api-ref/source/v1/vnflcm.inc index 202304ae8..707217cf6 100644 --- a/api-ref/source/v1/vnflcm.inc +++ b/api-ref/source/v1/vnflcm.inc @@ -229,6 +229,8 @@ Request Parameters - vnfInstanceId: vnf_instance_id - cause: cause - vnfcInstanceId: vnfc_resource_info_ids + - additionalParams: heal_additional_params + - tacker_extension_heal_include_block_storage: tacker_extension_heal_include_block_storage Request Example @@ -1368,4 +1370,4 @@ Request Parameters .. rest_parameters:: parameters_vnflcm.yaml - - vnfLcmOpOccId: vnf_lcm_op_occ_id \ No newline at end of file + - vnfLcmOpOccId: vnf_lcm_op_occ_id diff --git a/doc/source/user/etsi_vnf_healing.rst b/doc/source/user/etsi_vnf_healing.rst index 3b016bedb..3e2b40a5f 100644 --- a/doc/source/user/etsi_vnf_healing.rst +++ b/doc/source/user/etsi_vnf_healing.rst @@ -733,8 +733,14 @@ VDU1 information before healing: .. code-block:: console - $ openstack stack resource show HEAT_STACK_ID \ - VDU_NAME -c physical_resource_id -c resource_status + $ openstack stack resource show HEAT_STACK_ID VDU1_SERVER_NAME \ + -c physical_resource_id -c resource_name -c resource_status -c resource_type + + +.. code-block:: console + + $ openstack stack resource show HEAT_STACK_ID VDU1_VOLUME_NAME \ + -c physical_resource_id -c resource_name -c resource_status -c resource_type Result: @@ -744,17 +750,75 @@ Result: +----------------------+--------------------------------------+ | Field | Value | +----------------------+--------------------------------------+ - | physical_resource_id | 6b89f9c9-ebd8-49ca-8e2c-c01838daeb95 | + | physical_resource_id | ed781426-c59d-4c32-8bc3-e26144167220 | + | resource_name | VDU1 | | resource_status | CREATE_COMPLETE | + | resource_type | OS::Nova::Server | +----------------------+--------------------------------------+ +.. code-block:: console + + +----------------------+--------------------------------------+ + | Field | Value | + +----------------------+--------------------------------------+ + | physical_resource_id | 2d4715e6-1e0e-449e-91b5-a6c162adbb39 | + | resource_name | VDU1-VirtualStorage | + | resource_status | CREATE_COMPLETE | + | resource_type | OS::Cinder::Volume | + +----------------------+--------------------------------------+ + +Heal Specified with VNFC Instances can control whether related Cinder +block storage is recreated for the healed VNFC(s). + +This is done via an optional key inside ``additionalParams`` in the Heal +request. The key name is operator-configurable, and when the request +omits the key, a configurable default is applied [Conf-Options_]. + +* **Request key (configurable)**: + the key to read from ``additionalParams`` is configured by + ``[vnf_lcm] heal_include_block_storage_key`` on the VNFM. + By default it is ``tacker_extension_heal_include_block_storage``. +* **Default behavior (configurable)**: + when the request does not include the key, the behavior is decided by + ``[vnf_lcm] heal_vnfc_block_storage``. + +.. warning:: + + The default behavior (when the key is not provided) and the key name + itself depend on ``tacker.conf``. If you are unsure, contact your + environment administrator. + +In the examples below, the parameter name is shown as +``tacker_extension_heal_include_block_storage``. + +*Example: recreate storage (boolean true)* + +.. code-block:: json + + { + "additionalParams": { + "tacker_extension_heal_include_block_storage": true + } + } + +*Example: do not recreate storage (boolean false)* + +.. code-block:: json + + { + "additionalParams": { + "tacker_extension_heal_include_block_storage": false + } + } + + Healing execution of VDU1: .. code-block:: console - $ openstack vnflcm heal VNF_INSTANCE_ID --vnfc-instance VNFC_INSTANCE_ID - + $ openstack vnflcm heal VNF_INSTANCE_ID --vnfc-instance VNFC_INSTANCE_ID \ + --additional-param-file heal-params.json \ Result: @@ -772,8 +836,14 @@ VDU1 information after healing: .. code-block:: console - $ openstack stack resource show HEAT_STACK_ID \ - VDU_NAME -c physical_resource_id -c resource_status + $ openstack stack resource show HEAT_STACK_ID VDU1_SERVER_NAME \ + -c physical_resource_id -c resource_name -c resource_status -c resource_type + + +.. code-block:: console + + $ openstack stack resource show HEAT_STACK_ID VDU1_VOLUME_NAME \ + -c physical_resource_id -c resource_name -c resource_status -c resource_type Result: @@ -783,11 +853,39 @@ Result: +----------------------+--------------------------------------+ | Field | Value | +----------------------+--------------------------------------+ - | physical_resource_id | 6b89f9c9-ebd8-49ca-8e2c-c01838daeb95 | + | physical_resource_id | ed781426-c59d-4c32-8bc3-e26144167220 | + | resource_name | VDU1 | | resource_status | UPDATE_COMPLETE | + | resource_type | OS::Nova::Server | +----------------------+--------------------------------------+ +*recreate storage* + +.. code-block:: console + + +----------------------+--------------------------------------+ + | Field | Value | + +----------------------+--------------------------------------+ + | physical_resource_id | 2d4715e6-1e0e-449e-91b5-a6c162adbb39 | + | resource_name | VDU1-VirtualStorage | + | resource_status | UPDATE_COMPLETE | + | resource_type | OS::Cinder::Volume | + +----------------------+--------------------------------------+ + +*do not recreate storage* + +.. code-block:: console + + +----------------------+--------------------------------------+ + | Field | Value | + +----------------------+--------------------------------------+ + | physical_resource_id | 2d4715e6-1e0e-449e-91b5-a6c162adbb39 | + | resource_name | VDU1-VirtualStorage | + | resource_status | CREATE_COMPLETE | + | resource_type | OS::Cinder::Volume | + +----------------------+--------------------------------------+ + .. note:: 'physical_resource_id' has not changed from the ID before healing. @@ -797,3 +895,4 @@ Result: .. _NFV-SOL002 v2.6.1 : https://www.etsi.org/deliver/etsi_gs/NFV-SOL/001_099/002/02.06.01_60/gs_NFV-SOL002v020601p.pdf .. _Heat API reference : https://docs.openstack.org/api-ref/orchestration/v1/index.html .. _Heat CLI reference : https://docs.openstack.org/python-openstackclient/latest/cli/plugin-commands/heat.html +.. _Conf-Options : https://docs.openstack.org/tacker/latest/configuration/config.html#vnf-lcm diff --git a/tacker/conf/vnf_lcm.py b/tacker/conf/vnf_lcm.py index fb1d43b0f..3474529fb 100644 --- a/tacker/conf/vnf_lcm.py +++ b/tacker/conf/vnf_lcm.py @@ -20,6 +20,33 @@ OPTS = [ 'endpoint_url', default='http://localhost:9890/', help="endpoint_url"), + cfg.StrOpt( + 'heal_include_block_storage_key', + default='tacker_extension_heal_include_block_storage', + help=""" +Name of the boolean key in ``additionalParams`` that toggles including +block storage for a v1 heal request. + +Example payload:: + + { + "additionalParams": { + "tacker_extension_heal_include_block_storage": true + } + } +"""), + cfg.BoolOpt( + 'heal_vnfc_block_storage', + default=True, + help=""" +Default behaviour when a v1 heal request omits the per-request key. +If ``additionalParams[]`` is present, +that value takes precedence over this option. + +In ``tacker.conf`` (``[vnf_lcm]`` section):: + + heal_vnfc_block_storage = false +"""), cfg.IntOpt( 'subscription_num', default=100, diff --git a/tacker/tests/unit/vnfm/infra_drivers/openstack/test_heal_storage_toggle.py b/tacker/tests/unit/vnfm/infra_drivers/openstack/test_heal_storage_toggle.py new file mode 100644 index 000000000..9e51e937f --- /dev/null +++ b/tacker/tests/unit/vnfm/infra_drivers/openstack/test_heal_storage_toggle.py @@ -0,0 +1,157 @@ +# Copyright (C) 2025 KDDI +# All Rights Reserved. +# +# 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 oslo_config import cfg +from oslo_config import fixture as config +from tacker.conf import vnf_lcm as vnf_lcm_conf +from tacker.objects import fields +from tacker.vnfm.infra_drivers.openstack import openstack as os_drv +from types import SimpleNamespace +import unittest +from unittest import mock + + +class TestHealStorageToggleMin(unittest.TestCase): + def setUp(self): + super().setUp() + self.conf_fix = config.Config(cfg.CONF) + self.conf_fix.setUp() + vnf_lcm_conf.register_opts(cfg.CONF) + + self.driver = os_drv.OpenStack() + self.recorded = [] + + hc_patch = mock.patch( + 'tacker.vnfm.infra_drivers.openstack.openstack.hc.HeatClient') + self.addCleanup(hc_patch.stop) + FakeHC = hc_patch.start() + hc = FakeHC.return_value + hc.get.return_value = SimpleNamespace(stack_status='CREATE_COMPLETE') + + def rec_mark_unhealthy(*, stack_id, resource_name, **_): + self.recorded.append(resource_name) + + hc.resource_mark_unhealthy.side_effect = rec_mark_unhealthy + hc.update.return_value = None + + mock.patch( + 'tacker.vnfm.infra_drivers.openstack.openstack.vnflcm_utils.' + 'get_base_nest_hot_dict', + return_value=({}, {}) + ).start() + mock.patch( + 'tacker.vnfm.infra_drivers.openstack.openstack.vnflcm_utils.' + 'get_stack_param', + return_value={} + ).start() + mock.patch( + 'tacker.vnfm.infra_drivers.openstack.openstack.manager.' + 'TackerManager.get_service_plugins', + return_value={'VNFM': SimpleNamespace( + get_vnf=lambda *a, **k: {'vnfd_id': 'vnfd-1'})} + ).start() + mock.patch( + 'tacker.vnfm.infra_drivers.openstack.openstack.objects.' + 'VnfLcmOpOcc.get_by_vnf_instance_id', + return_value=SimpleNamespace( + error_point=fields.ErrorPoint.PRE_VIM_CONTROL) + ).start() + + mock.patch.object( + os_drv.OpenStack, '_get_stack_resources', + return_value={ + 'stack-123': { + 'VDU1': {'physical_resource_id': 'srv-001'}, + 'VDU1-volume': {'physical_resource_id': 'vol-001'}, + 'child_stack': False + } + } + ).start() + + mock.patch( + 'tacker.vnfm.infra_drivers.openstack.openstack.vnflcm_utils.' + 'get_vnfd_dict', + return_value={ + 'flavours': {'flv': {}}, + 'topology_template': {} + } + ).start() + + vnfc = SimpleNamespace( + id='vnfc-1', + vdu_id='VDU1', + compute_resource=SimpleNamespace(resource_id='srv-001'), + storage_resource_ids=['s-1'], + ) + vstorage = SimpleNamespace( + id='s-1', + virtual_storage_desc_id='VDU1-volume', + storage_resource=SimpleNamespace(resource_id='vol-001') + ) + self.vnf_instance = SimpleNamespace( + id='vnf-1', + vnfd_id='vnfd-1', + instantiated_vnf_info=SimpleNamespace( + instance_id='stack-123', + flavour_id='flv', + vnfc_resource_info=[vnfc], + virtual_storage_resource_info=[vstorage], + additional_params=None + ) + ) + from tacker.objects import vim_connection + self.vim_info = vim_connection.VimConnectionInfo( + vim_id='vim-1', + vim_type='openstack', + interface_info={}, + access_info={'region': 'RegionOne'}, + ) + + def _heal(self, *, default=None, req_val=None, custom_key=None): + if default is not None: + self.conf_fix.config(group='vnf_lcm', + heal_vnfc_block_storage=bool(default)) + if custom_key is not None: + self.conf_fix.config(group='vnf_lcm', + heal_include_block_storage_key=custom_key) + key = custom_key or cfg.CONF.vnf_lcm.heal_include_block_storage_key + add_params = {} if req_val is None else {key: req_val} + heal_req = SimpleNamespace(vnfc_instance_id=['vnfc-1'], + cause=None, + additional_params=add_params) + self.recorded.clear() + self.driver.heal_vnf(None, self.vnf_instance, self.vim_info, heal_req) + return set(self.recorded) + + def test_default_true_when_key_omitted(self): + marked = self._heal(default=True, req_val=None) + self.assertEqual({'VDU1', 'VDU1-volume'}, marked) + + def test_default_false_when_key_omitted(self): + marked = self._heal(default=False, req_val=None) + self.assertEqual({'VDU1'}, marked) + + def test_request_overrides_true(self): + marked = self._heal(default=False, req_val=True) + self.assertEqual({'VDU1', 'VDU1-volume'}, marked) + + def test_request_overrides_false(self): + marked = self._heal(default=True, req_val=False) + self.assertEqual({'VDU1'}, marked) + + def test_custom_key(self): + marked = self._heal( + default=False, req_val=True, custom_key='recreate_block') + self.assertEqual({'VDU1', 'VDU1-volume'}, marked) diff --git a/tacker/vnfm/infra_drivers/openstack/openstack.py b/tacker/vnfm/infra_drivers/openstack/openstack.py index b2975ea3f..e53852042 100644 --- a/tacker/vnfm/infra_drivers/openstack/openstack.py +++ b/tacker/vnfm/infra_drivers/openstack/openstack.py @@ -54,7 +54,6 @@ from tacker.vnfm.lcm_user_data.constants import USER_DATA_TIMEOUT from tacker.vnfm.lcm_user_data import utils as user_data_utils from toscaparser import tosca_template - eventlet.monkey_patch(time=True) SCALING_GROUP_RESOURCE = "OS::Heat::AutoScalingGroup" @@ -1495,12 +1494,15 @@ class OpenStack(abstract_driver.VnfAbstractDriver, 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) + if heal_vnf_request.additional_params.get( + CONF.vnf_lcm.heal_include_block_storage_key, + CONF.vnf_lcm.heal_vnfc_block_storage): + # 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