From: Alexis Romero Date: Wed, 13 Apr 2022 16:03:30 +0000 (+0200) Subject: Feature 10906: Support for Anti-Affinity groups X-Git-Tag: v11.0.3~9 X-Git-Url: https://osm.etsi.org/gitweb/?a=commitdiff_plain;h=7399321457dedb7ad2c40280cc5ff6f367d9b2ca;p=osm%2FRO.git Feature 10906: Support for Anti-Affinity groups Change-Id: Id14d9a02d4d66f678affb3e80a9169fc57eb452d Signed-off-by: Alexis Romero --- diff --git a/NG-RO/osm_ng_ro/ns.py b/NG-RO/osm_ng_ro/ns.py index 08ee7c44..e8326bf3 100644 --- a/NG-RO/osm_ng_ro/ns.py +++ b/NG-RO/osm_ng_ro/ns.py @@ -693,6 +693,21 @@ class Ns(object): return extra_dict + def _process_affinity_group_params(target_affinity_group, vim_info, target_record_id): + extra_dict = {} + + affinity_group_data = { + "name": target_affinity_group["name"], + "type": target_affinity_group["type"], + "scope": target_affinity_group["scope"], + } + + extra_dict["params"] = { + "affinity_group_data": affinity_group_data, + } + + return extra_dict + def _ip_profile_2_ro(ip_profile): if not ip_profile: return None @@ -908,6 +923,19 @@ class Ns(object): == "persistent-storage:persistent-storage" ] + affinity_group_list = [] + + if target_vdu.get("affinity-or-anti-affinity-group-id"): + affinity_group = {} + for affinity_group_id in target_vdu["affinity-or-anti-affinity-group-id"]: + affinity_group_text = ( + ns_preffix + ":affinity-or-anti-affinity-group." + affinity_group_id + ) + + extra_dict["depends_on"].append(affinity_group_text) + affinity_group["affinity_group_id"] = "TASK-" + affinity_group_text + affinity_group_list.append(affinity_group) + extra_dict["params"] = { "name": "{}-{}-{}-{}".format( indata["name"][:16], @@ -919,6 +947,7 @@ class Ns(object): "start": True, "image_id": "TASK-" + image_text, "flavor_id": "TASK-" + flavor_text, + "affinity_group_list": affinity_group_list, "net_list": net_list, "cloud_config": cloud_config or None, "disk_list": disk_list, @@ -1156,6 +1185,17 @@ class Ns(object): process_params=_process_flavor_params, ) + step = "process NS Affinity Groups" + _process_items( + target_list=indata.get("affinity-or-anti-affinity-group") or [], + existing_list=db_nsr.get("affinity-or-anti-affinity-group") or [], + db_record="nsrs:{}:affinity-or-anti-affinity-group".format(nsr_id), + db_update=db_nsr_update, + db_path="affinity-or-anti-affinity-group", + item="affinity-or-anti-affinity-group", + process_params=_process_affinity_group_params, + ) + # VNF.vld for vnfr_id, vnfr in db_vnfrs.items(): # vnfr_id need to be set as global variable for among others nested method _process_vdu_params diff --git a/NG-RO/osm_ng_ro/ns_thread.py b/NG-RO/osm_ng_ro/ns_thread.py index 4dac9d97..106830de 100644 --- a/NG-RO/osm_ng_ro/ns_thread.py +++ b/NG-RO/osm_ng_ro/ns_thread.py @@ -370,6 +370,23 @@ class VimInteractionVdu(VimInteractionBase): if params_copy["flavor_id"].startswith("TASK-"): params_copy["flavor_id"] = task_depends[params_copy["flavor_id"]] + affinity_group_list = params_copy["affinity_group_list"] + for affinity_group in affinity_group_list: + # change task_id into affinity_group_id + if "affinity_group_id" in affinity_group and affinity_group[ + "affinity_group_id" + ].startswith("TASK-"): + affinity_group_id = task_depends[ + affinity_group["affinity_group_id"] + ] + + if not affinity_group_id: + raise NsWorkerException( + "found for {}".format(affinity_group["affinity_group_id"]) + ) + + affinity_group["affinity_group_id"] = affinity_group_id + vim_vm_id, created_items = target_vim.new_vminstance(**params_copy) interfaces = [iface["vim_id"] for iface in params_copy["net_list"]] @@ -745,6 +762,93 @@ class VimInteractionFlavor(VimInteractionBase): return "FAILED", ro_vim_item_update +class VimInteractionAffinityGroup(VimInteractionBase): + def delete(self, ro_task, task_index): + task = ro_task["tasks"][task_index] + task_id = task["task_id"] + affinity_group_vim_id = ro_task["vim_info"]["vim_id"] + ro_vim_item_update_ok = { + "vim_status": "DELETED", + "created": False, + "vim_details": "DELETED", + "vim_id": None, + } + + try: + if affinity_group_vim_id: + target_vim = self.my_vims[ro_task["target_id"]] + target_vim.delete_affinity_group(affinity_group_vim_id) + except vimconn.VimConnNotFoundException: + ro_vim_item_update_ok["vim_details"] = "already deleted" + except vimconn.VimConnException as e: + self.logger.error( + "ro_task={} vim={} del-affinity-or-anti-affinity-group={}: {}".format( + ro_task["_id"], ro_task["target_id"], affinity_group_vim_id, e + ) + ) + ro_vim_item_update = { + "vim_status": "VIM_ERROR", + "vim_details": "Error while deleting: {}".format(e), + } + + return "FAILED", ro_vim_item_update + + self.logger.debug( + "task={} {} del-affinity-or-anti-affinity-group={} {}".format( + task_id, + ro_task["target_id"], + affinity_group_vim_id, + ro_vim_item_update_ok.get("vim_details", ""), + ) + ) + + return "DONE", ro_vim_item_update_ok + + def new(self, ro_task, task_index, task_depends): + task = ro_task["tasks"][task_index] + task_id = task["task_id"] + created = False + created_items = {} + target_vim = self.my_vims[ro_task["target_id"]] + + try: + affinity_group_vim_id = None + + if task.get("params"): + affinity_group_data = task["params"]["affinity_group_data"] + affinity_group_vim_id = target_vim.new_affinity_group( + affinity_group_data + ) + created = True + + ro_vim_item_update = { + "vim_id": affinity_group_vim_id, + "vim_status": "DONE", + "created": created, + "created_items": created_items, + "vim_details": None, + } + self.logger.debug( + "task={} {} new-affinity-or-anti-affinity-group={} created={}".format( + task_id, ro_task["target_id"], affinity_group_vim_id, created + ) + ) + + return "DONE", ro_vim_item_update + except (vimconn.VimConnException, NsWorkerException) as e: + self.logger.error( + "task={} vim={} new-affinity-or-anti-affinity-group:" + " {}".format(task_id, ro_task["target_id"], e) + ) + ro_vim_item_update = { + "vim_status": "VIM_ERROR", + "created": created, + "vim_details": str(e), + } + + return "FAILED", ro_vim_item_update + + class VimInteractionSdnNet(VimInteractionBase): @staticmethod def _match_pci(port_pci, mapping): @@ -1180,6 +1284,9 @@ class NsWorker(threading.Thread): "sdn_net": VimInteractionSdnNet( self.db, self.my_vims, self.db_vims, self.logger ), + "affinity-or-anti-affinity-group": VimInteractionAffinityGroup( + self.db, self.my_vims, self.db_vims, self.logger + ), } self.time_last_task_processed = None # lists of tasks to delete because nsrs or vnfrs has been deleted from db diff --git a/NG-RO/osm_ng_ro/tests/test_ns_thread.py b/NG-RO/osm_ng_ro/tests/test_ns_thread.py new file mode 100644 index 00000000..9f163a84 --- /dev/null +++ b/NG-RO/osm_ng_ro/tests/test_ns_thread.py @@ -0,0 +1,228 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# 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. +####################################################################################### + +import logging +import unittest +from unittest.mock import MagicMock, patch + +from osm_ng_ro.ns_thread import VimInteractionAffinityGroup + + +class TestVimInteractionAffinityGroup(unittest.TestCase): + def setUp(self): + module_name = "osm_ro_plugin" + self.target_vim = MagicMock(name=f"{module_name}.vimconn.VimConnector") + self.task_depends = None + + patches = [patch(f"{module_name}.vimconn.VimConnector", self.target_vim)] + + # Enabling mocks and add cleanups + for mock in patches: + mock.start() + self.addCleanup(mock.stop) + + def test__new_affinity_group_ok(self): + """ + create affinity group with attributes set in params + """ + db = "test_db" + logger = "test_logger" + my_vims = "test-vim" + db_vims = { + 0: { + "config": {}, + }, + } + + instance = VimInteractionAffinityGroup(db, logger, my_vims, db_vims) + with patch.object(instance, "my_vims", [self.target_vim]), patch.object( + instance, "logger", logging + ), patch.object(instance, "db_vims", db_vims): + ro_task = { + "target_id": 0, + "tasks": { + "task_index_1": { + "target_id": 0, + "action_id": "123456", + "nsr_id": "654321", + "task_id": "123456:1", + "status": "SCHEDULED", + "action": "CREATE", + "item": "test_item", + "target_record": "test_target_record", + "target_record_id": "test_target_record_id", + # values coming from extra_dict + "params": { + "affinity_group_data": { + "name": "affinity_group_1", + "type": "affinity", + "scope": "nfvi-node", + } + }, + "find_params": {}, + "depends_on": "test_depends_on", + }, + }, + } + + task_index = "task_index_1" + self.target_vim.new_affinity_group.return_value = ( + "sample_affinity_group_id_1" + ) + result = instance.new(ro_task, task_index, self.task_depends) + self.assertEqual(result[0], "DONE") + self.assertEqual(result[1].get("vim_id"), "sample_affinity_group_id_1") + self.assertEqual(result[1].get("created"), True) + self.assertEqual(result[1].get("vim_status"), "DONE") + + def test__new_affinity_group_failed(self): + """ + create affinity group with no attributes set in params + """ + db = "test_db" + logger = "test_logger" + my_vims = "test-vim" + db_vims = { + 0: { + "config": {}, + }, + } + + instance = VimInteractionAffinityGroup(db, logger, my_vims, db_vims) + with patch.object(instance, "my_vims", [self.target_vim]), patch.object( + instance, "logger", logging + ), patch.object(instance, "db_vims", db_vims): + ro_task = { + "target_id": 0, + "tasks": { + "task_index_2": { + "target_id": 0, + "action_id": "123456", + "nsr_id": "654321", + "task_id": "123456:1", + "status": "SCHEDULED", + "action": "CREATE", + "item": "test_item", + "target_record": "test_target_record", + "target_record_id": "test_target_record_id", + # values coming from extra_dict + "params": {}, + "find_params": {}, + "depends_on": "test_depends_on", + }, + }, + } + + task_index = "task_index_2" + self.target_vim.new_affinity_group.return_value = ( + "sample_affinity_group_id_1" + ) + result = instance.new(ro_task, task_index, self.task_depends) + self.assertEqual(result[0], "DONE") + self.assertEqual(result[1].get("vim_id"), None) + self.assertEqual(result[1].get("created"), False) + self.assertEqual(result[1].get("vim_status"), "DONE") + + def test__delete_affinity_group_ok(self): + """ + delete affinity group with a proper vim_id + """ + db = "test_db" + logger = "test_logger" + my_vims = "test-vim" + db_vims = { + 0: { + "config": {}, + }, + } + + instance = VimInteractionAffinityGroup(db, logger, my_vims, db_vims) + with patch.object(instance, "my_vims", [self.target_vim]), patch.object( + instance, "logger", logging + ), patch.object(instance, "db_vims", db_vims): + ro_task = { + "target_id": 0, + "tasks": { + "task_index_3": { + "target_id": 0, + "task_id": "123456:1", + }, + }, + "vim_info": { + "created": False, + "created_items": None, + "vim_id": "sample_affinity_group_id_3", + "vim_name": "sample_affinity_group_id_3", + "vim_status": None, + "vim_details": "some-details", + "refresh_at": None, + }, + } + + task_index = "task_index_3" + self.target_vim.delete_affinity_group.return_value = ( + "sample_affinity_group_id_3" + ) + result = instance.delete(ro_task, task_index) + self.assertEqual(result[0], "DONE") + self.assertEqual(result[1].get("vim_details"), "DELETED") + self.assertEqual(result[1].get("created"), False) + self.assertEqual(result[1].get("vim_status"), "DELETED") + + def test__delete_affinity_group_failed(self): + """ + delete affinity group with missing vim_id + """ + db = "test_db" + logger = "test_logger" + my_vims = "test-vim" + db_vims = { + 0: { + "config": {}, + }, + } + + instance = VimInteractionAffinityGroup(db, logger, my_vims, db_vims) + with patch.object(instance, "my_vims", [self.target_vim]), patch.object( + instance, "logger", logging + ), patch.object(instance, "db_vims", db_vims): + ro_task = { + "target_id": 0, + "tasks": { + "task_index_4": { + "target_id": 0, + "task_id": "123456:1", + }, + }, + "vim_info": { + "created": False, + "created_items": None, + "vim_id": None, + "vim_name": None, + "vim_status": None, + "vim_details": "some-details", + "refresh_at": None, + }, + } + + task_index = "task_index_4" + self.target_vim.delete_affinity_group.return_value = "" + result = instance.delete(ro_task, task_index) + self.assertEqual(result[0], "DONE") + self.assertEqual(result[1].get("vim_details"), "DELETED") + self.assertEqual(result[1].get("created"), False) + self.assertEqual(result[1].get("vim_status"), "DELETED") diff --git a/NG-RO/osm_ng_ro/validation.py b/NG-RO/osm_ng_ro/validation.py index 91ea6137..ca8cbc2b 100644 --- a/NG-RO/osm_ng_ro/validation.py +++ b/NG-RO/osm_ng_ro/validation.py @@ -107,6 +107,7 @@ deploy_schema = { "vld": deploy_item_list, }, }, + "affinity-or-anti-affinity-group": deploy_item_list, }, "additionalProperties": False, } diff --git a/RO-VIM-aws/osm_rovim_aws/vimconn_aws.py b/RO-VIM-aws/osm_rovim_aws/vimconn_aws.py index 77adecd8..59b4a50b 100644 --- a/RO-VIM-aws/osm_rovim_aws/vimconn_aws.py +++ b/RO-VIM-aws/osm_rovim_aws/vimconn_aws.py @@ -767,6 +767,7 @@ class vimconnector(vimconn.VimConnector): start, image_id, flavor_id, + affinity_group_list, net_list, cloud_config=None, disk_list=None, diff --git a/RO-VIM-azure/osm_rovim_azure/vimconn_azure.py b/RO-VIM-azure/osm_rovim_azure/vimconn_azure.py index 967eb3dd..d4ef9535 100755 --- a/RO-VIM-azure/osm_rovim_azure/vimconn_azure.py +++ b/RO-VIM-azure/osm_rovim_azure/vimconn_azure.py @@ -865,6 +865,7 @@ class vimconnector(vimconn.VimConnector): start, image_id, flavor_id, + affinity_group_list, net_list, cloud_config=None, disk_list=None, diff --git a/RO-VIM-gcp/osm_rovim_gcp/vimconn_gcp.py b/RO-VIM-gcp/osm_rovim_gcp/vimconn_gcp.py index f8c8145f..0a4ead08 100644 --- a/RO-VIM-gcp/osm_rovim_gcp/vimconn_gcp.py +++ b/RO-VIM-gcp/osm_rovim_gcp/vimconn_gcp.py @@ -864,6 +864,7 @@ class vimconnector(vimconn.VimConnector): start, image_id=None, # :(image|image-family): flavor_id=None, + affinity_group_list=None, net_list=None, cloud_config=None, disk_list=None, diff --git a/RO-VIM-openstack/osm_rovim_openstack/vimconn_openstack.py b/RO-VIM-openstack/osm_rovim_openstack/vimconn_openstack.py index 1083e391..a15a53cf 100644 --- a/RO-VIM-openstack/osm_rovim_openstack/vimconn_openstack.py +++ b/RO-VIM-openstack/osm_rovim_openstack/vimconn_openstack.py @@ -1681,6 +1681,7 @@ class vimconnector(vimconn.VimConnector): start, image_id, flavor_id, + affinity_group_list, net_list, cloud_config=None, disk_list=None, @@ -1690,7 +1691,9 @@ class vimconnector(vimconn.VimConnector): """Adds a VM instance to VIM Params: start: indicates if VM must start or boot in pause mode. Ignored - image_id,flavor_id: iamge and flavor uuid + image_id,flavor_id: image and flavor uuid + affinity_group_list: list of affinity groups, each one is a dictionary. + Ignore if empty. net_list: list of interfaces, each one is a dictionary with: name: net_id: network uuid to connect @@ -1915,10 +1918,19 @@ class vimconnector(vimconn.VimConnector): availability_zone_index, availability_zone_list ) + # Manage affinity groups/server groups + server_group_id = None + scheduller_hints = {} + + if affinity_group_list: + # Only first id on the list will be used. Openstack restriction + server_group_id = affinity_group_list[0]["affinity_group_id"] + scheduller_hints["group"] = server_group_id + self.logger.debug( "nova.servers.create({}, {}, {}, nics={}, security_groups={}, " "availability_zone={}, key_name={}, userdata={}, config_drive={}, " - "block_device_mapping={})".format( + "block_device_mapping={}, server_group={})".format( name, image_id, flavor_id, @@ -1929,6 +1941,7 @@ class vimconnector(vimconn.VimConnector): userdata, config_drive, block_device_mapping, + server_group_id, ) ) server = self.nova.servers.create( @@ -1943,6 +1956,7 @@ class vimconnector(vimconn.VimConnector): userdata=userdata, config_drive=config_drive, block_device_mapping=block_device_mapping, + scheduler_hints=scheduller_hints, ) # , description=description) vm_start_time = time.time() @@ -3444,3 +3458,60 @@ class vimconnector(vimconn.VimConnector): classification_dict[classification_id] = classification return classification_dict + + def new_affinity_group(self, affinity_group_data): + """Adds a server group to VIM + affinity_group_data contains a dictionary with information, keys: + name: name in VIM for the server group + type: affinity or anti-affinity + scope: Only nfvi-node allowed + Returns the server group identifier""" + self.logger.debug("Adding Server Group '%s'", str(affinity_group_data)) + + try: + name = affinity_group_data["name"] + policy = affinity_group_data["type"] + + self._reload_connection() + new_server_group = self.nova.server_groups.create(name, policy) + + return new_server_group.id + except ( + ksExceptions.ClientException, + nvExceptions.ClientException, + ConnectionError, + KeyError, + ) as e: + self._format_exception(e) + + def get_affinity_group(self, affinity_group_id): + """Obtain server group details from the VIM. Returns the server group detais as a dict""" + self.logger.debug("Getting flavor '%s'", affinity_group_id) + try: + self._reload_connection() + server_group = self.nova.server_groups.find(id=affinity_group_id) + + return server_group.to_dict() + except ( + nvExceptions.NotFound, + nvExceptions.ClientException, + ksExceptions.ClientException, + ConnectionError, + ) as e: + self._format_exception(e) + + def delete_affinity_group(self, affinity_group_id): + """Deletes a server group from the VIM. Returns the old affinity_group_id""" + self.logger.debug("Getting server group '%s'", affinity_group_id) + try: + self._reload_connection() + self.nova.server_groups.delete(affinity_group_id) + + return affinity_group_id + except ( + nvExceptions.NotFound, + ksExceptions.ClientException, + nvExceptions.ClientException, + ConnectionError, + ) as e: + self._format_exception(e) diff --git a/RO-VIM-openvim/osm_rovim_openvim/vimconn_openvim.py b/RO-VIM-openvim/osm_rovim_openvim/vimconn_openvim.py index 79a5c7ba..e88cecc2 100644 --- a/RO-VIM-openvim/osm_rovim_openvim/vimconn_openvim.py +++ b/RO-VIM-openvim/osm_rovim_openvim/vimconn_openvim.py @@ -953,6 +953,7 @@ class vimconnector(vimconn.VimConnector): start, image_id, flavor_id, + affinity_group_list, net_list, cloud_config=None, disk_list=None, diff --git a/RO-VIM-vmware/osm_rovim_vmware/vimconn_vmware.py b/RO-VIM-vmware/osm_rovim_vmware/vimconn_vmware.py index 6756ef45..0a220d6b 100644 --- a/RO-VIM-vmware/osm_rovim_vmware/vimconn_vmware.py +++ b/RO-VIM-vmware/osm_rovim_vmware/vimconn_vmware.py @@ -1907,6 +1907,7 @@ class vimconnector(vimconn.VimConnector): start=False, image_id=None, flavor_id=None, + affinity_group_list=[], net_list=[], cloud_config=None, disk_list=None, diff --git a/RO-plugin/osm_ro_plugin/vim_dummy.py b/RO-plugin/osm_ro_plugin/vim_dummy.py index f003ba9f..6c59607a 100644 --- a/RO-plugin/osm_ro_plugin/vim_dummy.py +++ b/RO-plugin/osm_ro_plugin/vim_dummy.py @@ -348,6 +348,7 @@ class VimDummyConnector(vimconn.VimConnector): start, image_id, flavor_id, + affinity_group_list, net_list, cloud_config=None, disk_list=None, diff --git a/RO-plugin/osm_ro_plugin/vimconn.py b/RO-plugin/osm_ro_plugin/vimconn.py index b4e936c1..0b780132 100644 --- a/RO-plugin/osm_ro_plugin/vimconn.py +++ b/RO-plugin/osm_ro_plugin/vimconn.py @@ -528,6 +528,29 @@ class VimConnector: """ raise VimConnNotImplemented("Should have implemented this") + def get_affinity_group(self, affinity_group_id): + """Obtain affinity or anti affinity group details from the VIM + Returns the flavor dict details {'id':<>, 'name':<>, other vim specific } + Raises an exception upon error or if not found + """ + raise VimConnNotImplemented("Should have implemented this") + + def new_affinity_group(self, affinity_group_data): + """Adds an affinity or anti affinity group to VIM + affinity_group_data contains a dictionary with information, keys: + name: name in VIM for the affinity or anti-affinity group + type: affinity or anti-affinity + scope: Only nfvi-node allowed + Returns the affinity or anti affinity group identifier + """ + raise VimConnNotImplemented("Should have implemented this") + + def delete_affinity_group(self, affinity_group_id): + """Deletes an affinity or anti affinity group from the VIM identified by its id + Returns the used id or raise an exception + """ + raise VimConnNotImplemented("Should have implemented this") + def new_image(self, image_dict): """Adds a tenant image to VIM Returns the image id or raises an exception if failed @@ -566,6 +589,7 @@ class VimConnector: start, image_id, flavor_id, + affinity_group_list, net_list, cloud_config=None, disk_list=None, @@ -576,6 +600,8 @@ class VimConnector: Params: 'start': (boolean) indicates if VM must start or created in pause mode. 'image_id','flavor_id': image and flavor VIM id to use for the VM + 'affinity_group_list': list of affinity groups, each one is a dictionary. + Ignore if empty. 'net_list': list of interfaces, each one is a dictionary with: 'name': (optional) name for the interface. 'net_id': VIM network id where this interface must be connect to. Mandatory for type==virtual diff --git a/releasenotes/notes/feature_10906_anti-affinity-00437de83b4f71e0.yaml b/releasenotes/notes/feature_10906_anti-affinity-00437de83b4f71e0.yaml new file mode 100644 index 00000000..47d8a772 --- /dev/null +++ b/releasenotes/notes/feature_10906_anti-affinity-00437de83b4f71e0.yaml @@ -0,0 +1,22 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# 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. +####################################################################################### +--- +features: + - | + Feature 10906: Support for Anti-Affinity groups + Support of affinity and anti-affinity groups for Openstack based VIMs (server groups) + Allowed at VNF level. Only nfvi-node scope allowed by an Openstack VIM.