From d63da24a1392b3c101a28f55ea1bfec805424e62 Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Tue, 26 Nov 2019 14:59:30 +0000 Subject: [PATCH] [OVN] Add hash ring methods OVNHashRing objects are used by OVN backend to register the nodes controlled. The methods implemented are: - add_node: including the group_name and optionally the node_uuid - remove_nodes_from_host: remove all nodes maching a hostname and group_name - touch_nodes_from_host: update a OVNHashRing register timestamp, filtering by hostname and group_name - touch_node: update a OVNHashRing register timestamp, filtering by node_uuid - get_active_nodes: retrieve any active node (filtering timestamp), group_name and hostname Previous paths in networking-ovn tree: ./networking_ovn/db/hash_ring.py -> ./neutron/db/ovn_hash_ring_db.py ./networking_ovn/tests/unit/db/test_hash_ring.py -> ./neutron/tests/unit/db/test_ovn_hash_ring_db.py Co-Authored-By: Lucas Alvares Gomes Change-Id: Ibb9c79771c48ccde564c39be9d781f3720802a2d Partially-Implements: blueprint neutron-ovn-merge --- neutron/db/ovn_hash_ring_db.py | 69 ++++++ .../tests/unit/db/test_ovn_hash_ring_db.py | 231 ++++++++++++++++++ 2 files changed, 300 insertions(+) create mode 100644 neutron/db/ovn_hash_ring_db.py create mode 100644 neutron/tests/unit/db/test_ovn_hash_ring_db.py diff --git a/neutron/db/ovn_hash_ring_db.py b/neutron/db/ovn_hash_ring_db.py new file mode 100644 index 00000000000..1cea2f6bef2 --- /dev/null +++ b/neutron/db/ovn_hash_ring_db.py @@ -0,0 +1,69 @@ +# Copyright 2019 Red Hat, Inc. +# 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. + +import datetime + +from neutron_lib.db import api as db_api +from oslo_config import cfg +from oslo_utils import timeutils +from oslo_utils import uuidutils + +from neutron.db.models import ovn as ovn_models + +CONF = cfg.CONF + + +# NOTE(ralonsoh): this was migrated from networking-ovn to neutron and should +# be refactored to be integrated in a OVO. +def add_node(context, group_name, node_uuid=None): + if node_uuid is None: + node_uuid = uuidutils.generate_uuid() + + with db_api.CONTEXT_WRITER.using(context): + context.session.add(ovn_models.OVNHashRing( + node_uuid=node_uuid, hostname=CONF.host, group_name=group_name)) + return node_uuid + + +def remove_nodes_from_host(context, group_name): + with db_api.CONTEXT_WRITER.using(context): + context.session.query(ovn_models.OVNHashRing).filter( + ovn_models.OVNHashRing.hostname == CONF.host, + ovn_models.OVNHashRing.group_name == group_name).delete() + + +def _touch(context, **filter_args): + with db_api.CONTEXT_WRITER.using(context): + context.session.query(ovn_models.OVNHashRing).filter_by( + **filter_args).update({'updated_at': timeutils.utcnow()}) + + +def touch_nodes_from_host(context, group_name): + _touch(context, hostname=CONF.host, group_name=group_name) + + +def touch_node(context, node_uuid): + _touch(context, node_uuid=node_uuid) + + +def get_active_nodes(context, interval, group_name, from_host=False): + limit = timeutils.utcnow() - datetime.timedelta(seconds=interval) + with db_api.CONTEXT_READER.using(context): + query = context.session.query(ovn_models.OVNHashRing).filter( + ovn_models.OVNHashRing.updated_at >= limit, + ovn_models.OVNHashRing.group_name == group_name) + if from_host: + query = query.filter_by(hostname=CONF.host) + return query.all() diff --git a/neutron/tests/unit/db/test_ovn_hash_ring_db.py b/neutron/tests/unit/db/test_ovn_hash_ring_db.py new file mode 100644 index 00000000000..7347c8399c4 --- /dev/null +++ b/neutron/tests/unit/db/test_ovn_hash_ring_db.py @@ -0,0 +1,231 @@ +# Copyright 2019 Red Hat, Inc. +# 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. + +import datetime + +import mock +from neutron_lib import context +from neutron_lib.db import api as db_api +from oslo_utils import timeutils +from oslo_utils import uuidutils +from sqlalchemy.orm import exc + +from neutron.db.models import ovn as ovn_models +from neutron.db import ovn_hash_ring_db +from neutron.tests.unit import testlib_api + +HASH_RING_TEST_GROUP = 'test_group' + + +class TestHashRing(testlib_api.SqlTestCaseLight): + + def setUp(self): + super(TestHashRing, self).setUp() + self.admin_ctx = context.get_admin_context() + self.addCleanup(self._delete_objs) + + def _delete_objs(self): + with db_api.CONTEXT_WRITER.using(self.admin_ctx): + self.admin_ctx.session.query( + ovn_models.OVNRevisionNumbers).delete() + + def _get_node_row(self, node_uuid): + try: + with db_api.CONTEXT_WRITER.using(self.admin_ctx): + return self.admin_ctx.session.query( + ovn_models.OVNHashRing).filter_by( + node_uuid=node_uuid).one() + except exc.NoResultFound: + return + + def _add_nodes_and_assert_exists(self, count=1, + group_name=HASH_RING_TEST_GROUP): + nodes = [] + for i in range(count): + node_uuid = ovn_hash_ring_db.add_node(self.admin_ctx, group_name) + self.assertIsNotNone(self._get_node_row(node_uuid)) + nodes.append(node_uuid) + return nodes + + def test_add_node(self): + self._add_nodes_and_assert_exists() + + def test_remove_nodes_from_host(self): + nodes = self._add_nodes_and_assert_exists(count=3) + + # Add another node from a different host + with mock.patch.object(ovn_hash_ring_db, 'CONF') as mock_conf: + mock_conf.host = 'another-host-' + uuidutils.generate_uuid() + another_host_node = self._add_nodes_and_assert_exists()[0] + + ovn_hash_ring_db.remove_nodes_from_host(self.admin_ctx, + HASH_RING_TEST_GROUP) + # Assert that all nodes from that host have been removed + for n in nodes: + self.assertIsNone(self._get_node_row(n)) + + # Assert that the node from another host wasn't removed + self.assertIsNotNone(self._get_node_row(another_host_node)) + + def test_touch_nodes_from_host(self): + nodes = self._add_nodes_and_assert_exists(count=3) + + # Add another node from a different host + with mock.patch.object(ovn_hash_ring_db, 'CONF') as mock_conf: + mock_conf.host = 'another-host-' + uuidutils.generate_uuid() + another_host_node = self._add_nodes_and_assert_exists()[0] + + # Assert that updated_at isn't updated yet + for node in nodes: + node_db = self._get_node_row(node) + self.assertEqual(node_db.created_at, node_db.updated_at) + + # Assert the same for the node from another host + node_db = self._get_node_row(another_host_node) + self.assertEqual(node_db.created_at, node_db.updated_at) + + # Touch the nodes from our host + ovn_hash_ring_db.touch_nodes_from_host(self.admin_ctx, + HASH_RING_TEST_GROUP) + + # Assert that updated_at is now updated + for node in nodes: + node_db = self._get_node_row(node) + self.assertGreater(node_db.updated_at, node_db.created_at) + + # Assert that the node from another host hasn't been touched + # (updated_at is not updated) + node_db = self._get_node_row(another_host_node) + self.assertEqual(node_db.created_at, node_db.updated_at) + + def test_active_nodes(self): + self._add_nodes_and_assert_exists(count=3) + + # Add another node from a different host + with mock.patch.object(ovn_hash_ring_db, 'CONF') as mock_conf: + mock_conf.host = 'another-host-' + uuidutils.generate_uuid() + another_host_node = self._add_nodes_and_assert_exists()[0] + + # Assert all nodes are active (within 60 seconds) + self.assertEqual(4, len(ovn_hash_ring_db.get_active_nodes( + self.admin_ctx, interval=60, group_name=HASH_RING_TEST_GROUP))) + + # Substract 60 seconds from utcnow() and touch the nodes from our host + fake_utcnow = timeutils.utcnow() - datetime.timedelta(seconds=60) + with mock.patch.object(timeutils, 'utcnow') as mock_utcnow: + mock_utcnow.return_value = fake_utcnow + ovn_hash_ring_db.touch_nodes_from_host(self.admin_ctx, + HASH_RING_TEST_GROUP) + + # Now assert that all nodes from our host are seeing as offline. + # Only the node from another host should be active + active_nodes = ovn_hash_ring_db.get_active_nodes( + self.admin_ctx, interval=60, group_name=HASH_RING_TEST_GROUP) + self.assertEqual(1, len(active_nodes)) + self.assertEqual(another_host_node, active_nodes[0].node_uuid) + + def test_active_nodes_from_host(self): + self._add_nodes_and_assert_exists(count=3) + + # Add another node from a different host + another_host_id = 'another-host-52359446-c366' + with mock.patch.object(ovn_hash_ring_db, 'CONF') as mock_conf: + mock_conf.host = another_host_id + self._add_nodes_and_assert_exists() + + # Assert only the 3 nodes from this host is returned + active_nodes = ovn_hash_ring_db.get_active_nodes( + self.admin_ctx, interval=60, group_name=HASH_RING_TEST_GROUP, + from_host=True) + self.assertEqual(3, len(active_nodes)) + self.assertNotIn(another_host_id, active_nodes) + + def test_touch_node(self): + nodes = self._add_nodes_and_assert_exists(count=3) + + # Assert no nodes were updated yet + for node in nodes: + node_db = self._get_node_row(node) + self.assertEqual(node_db.created_at, node_db.updated_at) + + # Touch one of the nodes + ovn_hash_ring_db.touch_node(self.admin_ctx, nodes[0]) + + # Assert it has been updated + node_db = self._get_node_row(nodes[0]) + self.assertGreater(node_db.updated_at, node_db.created_at) + + # Assert the other two nodes hasn't been updated + for node in nodes[1:]: + node_db = self._get_node_row(node) + self.assertEqual(node_db.created_at, node_db.updated_at) + + def test_active_nodes_different_groups(self): + another_group = 'another_test_group' + self._add_nodes_and_assert_exists(count=3) + self._add_nodes_and_assert_exists(count=2, group_name=another_group) + + active_nodes = ovn_hash_ring_db.get_active_nodes( + self.admin_ctx, interval=60, group_name=HASH_RING_TEST_GROUP) + self.assertEqual(3, len(active_nodes)) + for node in active_nodes: + self.assertEqual(HASH_RING_TEST_GROUP, node.group_name) + + active_nodes = ovn_hash_ring_db.get_active_nodes( + self.admin_ctx, interval=60, group_name=another_group) + self.assertEqual(2, len(active_nodes)) + for node in active_nodes: + self.assertEqual(another_group, node.group_name) + + def test_remove_nodes_from_host_different_groups(self): + another_group = 'another_test_group' + group1 = self._add_nodes_and_assert_exists(count=3) + group2 = self._add_nodes_and_assert_exists( + count=2, group_name=another_group) + + ovn_hash_ring_db.remove_nodes_from_host(self.admin_ctx, + HASH_RING_TEST_GROUP) + # Assert that all nodes from that group have been removed + for node in group1: + self.assertIsNone(self._get_node_row(node)) + + # Assert that all nodes from a different group are intact + for node in group2: + self.assertIsNotNone(self._get_node_row(node)) + + def test_touch_nodes_from_host_different_groups(self): + another_group = 'another_test_group' + group1 = self._add_nodes_and_assert_exists(count=3) + group2 = self._add_nodes_and_assert_exists( + count=2, group_name=another_group) + + # Assert that updated_at isn't updated yet + for node in group1 + group2: + node_db = self._get_node_row(node) + self.assertEqual(node_db.created_at, node_db.updated_at) + + # Touch the nodes from group1 + ovn_hash_ring_db.touch_nodes_from_host(self.admin_ctx, + HASH_RING_TEST_GROUP) + + # Assert that updated_at was updated for group1 + for node in group1: + node_db = self._get_node_row(node) + self.assertGreater(node_db.updated_at, node_db.created_at) + + # Assert that updated_at wasn't updated for group2 + for node in group2: + node_db = self._get_node_row(node) + self.assertEqual(node_db.created_at, node_db.updated_at)