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
== "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],
"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,
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
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"]]
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):
"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
--- /dev/null
+#######################################################################################
+# 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")
"vld": deploy_item_list,
},
},
+ "affinity-or-anti-affinity-group": deploy_item_list,
},
"additionalProperties": False,
}
start,
image_id,
flavor_id,
+ affinity_group_list,
net_list,
cloud_config=None,
disk_list=None,
start,
image_id,
flavor_id,
+ affinity_group_list,
net_list,
cloud_config=None,
disk_list=None,
start,
image_id=None, # <image project>:(image|image-family):<image/family id>
flavor_id=None,
+ affinity_group_list=None,
net_list=None,
cloud_config=None,
disk_list=None,
start,
image_id,
flavor_id,
+ affinity_group_list,
net_list,
cloud_config=None,
disk_list=None,
"""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
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,
userdata,
config_drive,
block_device_mapping,
+ server_group_id,
)
)
server = self.nova.servers.create(
userdata=userdata,
config_drive=config_drive,
block_device_mapping=block_device_mapping,
+ scheduler_hints=scheduller_hints,
) # , description=description)
vm_start_time = time.time()
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)
start,
image_id,
flavor_id,
+ affinity_group_list,
net_list,
cloud_config=None,
disk_list=None,
start=False,
image_id=None,
flavor_id=None,
+ affinity_group_list=[],
net_list=[],
cloud_config=None,
disk_list=None,
start,
image_id,
flavor_id,
+ affinity_group_list,
net_list,
cloud_config=None,
disk_list=None,
"""
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
start,
image_id,
flavor_id,
+ affinity_group_list,
net_list,
cloud_config=None,
disk_list=None,
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
--- /dev/null
+#######################################################################################
+# 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.