diff --git a/neutron/api/converters.py b/neutron/api/converters.py new file mode 100644 index 00000000000..58f448a2e41 --- /dev/null +++ b/neutron/api/converters.py @@ -0,0 +1,37 @@ +# Copyright (c) 2021 Ericsson Software Technology +# +# 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 uuid + +from neutron_lib.placement import utils as pl_utils + + +# TODO(przszc): Delete when https://review.opendev.org/813650 is released +def convert_to_sanitized_binding_profile_allocation(allocation, port_id, + min_bw_rules): + """Return binding-profile.allocation in the new format + + :param allocation: binding-profile.allocation attribute containting a + string with RP UUID + :param port_id: ID of the port that is being sanitized + :param min_bw_rules: A list of minimum bandwidth rules associated with the + port. + :return: A dict with allocation in {'': ''} format. + """ + if isinstance(allocation, dict): + return allocation + + group_id = str( + pl_utils.resource_request_group_uuid(uuid.UUID(port_id), min_bw_rules)) + return {group_id: allocation} diff --git a/neutron/cmd/sanitize_port_binding_profile_allocation.py b/neutron/cmd/sanitize_port_binding_profile_allocation.py new file mode 100644 index 00000000000..87077ef7a82 --- /dev/null +++ b/neutron/cmd/sanitize_port_binding_profile_allocation.py @@ -0,0 +1,87 @@ +# Copyright (c) 2021 Ericsson Software Technology +# +# 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 neutron_lib import context +from neutron_lib.db import api as db_api +from oslo_config import cfg +from oslo_db import options as db_options +from oslo_log import log as logging + +from neutron.api import converters as n_converters +from neutron.objects import ports as port_obj +from neutron.objects.qos import binding as qos_binding_obj +from neutron.objects.qos import rule as qos_rule_obj + + +LOG = logging.getLogger(__name__) + + +def setup_conf(): + conf = cfg.CONF + db_group, neutron_db_opts = db_options.list_opts()[0] + cfg.CONF.register_cli_opts(neutron_db_opts, db_group) + conf() + + +def main(): + """Main method for sanitizing "ml2_port_bindings.profile" column. + + This script will sanitize "ml2_port_bindings.profile" columns existing in + the database. In Yoga release the format of this column has changed from: + {'allocation': ''} + to: + {'allocation': {'': ''}} + + where group_uuid is generated based on port_id and ID of QoS rules + belonging to that group. + """ + setup_conf() + admin_ctx = context.get_admin_context() + with db_api.CONTEXT_WRITER.using(admin_ctx): + for port_binding in port_obj.PortBinding.get_objects(admin_ctx): + # NOTE(przszc): Before minimum packet rate rule was introduced, + # binding-profile.allocation attribute could contain only a single + # RP UUID, responsible for providing minimum bandwidth resources. + # Because of that, whenever we find allocation attribute that still + # uses old format, we can safely assume that we need to generate + # minimum bandwidth group UUID. + allocation = port_binding.profile.get('allocation') + if (not allocation or isinstance(allocation, dict)): + continue + + qos_port_binding = qos_binding_obj.QosPolicyPortBinding.get_object( + admin_ctx, port_id=port_binding.port_id) + if not qos_port_binding: + LOG.error( + 'Failed to sanitize binding-profile.allocation attribute ' + '%s for port %s: Did not find associated QoS policy.', + allocation, port_binding.port_id) + continue + + min_bw_rules = qos_rule_obj.QosMinimumBandwidthRule.get_objects( + admin_ctx, qos_policy_id=qos_port_binding.policy_id) + if not min_bw_rules: + LOG.error( + 'Failed to sanitize binding-profile.allocation attribute ' + '%s for port %s: Associated QoS policy %s has no minimum ' + 'bandwidth rules.', allocation, port_binding.port_id, + qos_port_binding.policy_id) + continue + + port_binding.profile = {'allocation': + n_converters.convert_to_sanitized_binding_profile_allocation( + allocation, port_binding.port_id, min_bw_rules)} + LOG.info('Port %s updated, New binding-profile.allocation format: ' + '%s', port_binding.port_id, port_binding.profile) + port_binding.update() diff --git a/neutron/cmd/upgrade_checks/checks.py b/neutron/cmd/upgrade_checks/checks.py index 3b7bf3a2d26..7662d2f9d58 100644 --- a/neutron/cmd/upgrade_checks/checks.py +++ b/neutron/cmd/upgrade_checks/checks.py @@ -29,6 +29,7 @@ from neutron.db.models import agent as agent_model from neutron.db.models.plugins.ml2 import vlanallocation from neutron.db.models import segment from neutron.db import models_v2 +from neutron.objects import ports as port_obj OVN_ALEMBIC_TABLE_NAME = "ovn_alembic_version" @@ -106,6 +107,12 @@ def get_duplicate_network_segment_count(): return query.count() +def port_binding_profiles(): + ctx = context.get_admin_context() + return [port_binding.profile + for port_binding in port_obj.PortBinding.get_objects(ctx)] + + class CoreChecks(base.BaseChecks): def get_checks(self): @@ -127,6 +134,8 @@ class CoreChecks(base.BaseChecks): self.port_mac_address_sanity), (_('NetworkSegments unique constraint check'), self.networksegments_unique_constraint_check), + (_('Port Binding profile sanity check'), + self.port_binding_profile_sanity), ] @staticmethod @@ -365,3 +374,31 @@ class CoreChecks(base.BaseChecks): upgradecheck.Code.SUCCESS, _("No networksegments sharing the same network_id, network_type " "and physical_network found.")) + + @staticmethod + def port_binding_profile_sanity(checker): + """Checks that "ml2_port_bindings.profile" uses the new format + + All allocation information should be stored in the following format: + {'allocation': {'': ''}}. + """ + if not cfg.CONF.database.connection: + return upgradecheck.Result( + upgradecheck.Code.WARNING, + _("Database connection string is not set. Check for " + "ml2_port_bindings.profile sanity can't be done.")) + + for profile in port_binding_profiles(): + allocation = profile.get('allocation') + if (allocation and not isinstance(allocation, dict)): + return upgradecheck.Result( + upgradecheck.Code.FAILURE, + _("ml2_port_bindings.profile rows are not correctly " + "formated in the database. The script " + "neutron-sanitize-port-binding-profile-allocation " + "should be executed")) + + return upgradecheck.Result( + upgradecheck.Code.SUCCESS, + _("All ml2_port_bindings.profile rows are correctly formated in " + "the database.")) diff --git a/neutron/tests/unit/cmd/upgrade_checks/test_checks.py b/neutron/tests/unit/cmd/upgrade_checks/test_checks.py index 91e3bd7e39a..69e3bacbf95 100644 --- a/neutron/tests/unit/cmd/upgrade_checks/test_checks.py +++ b/neutron/tests/unit/cmd/upgrade_checks/test_checks.py @@ -211,3 +211,19 @@ class TestChecks(base.BaseTestCase): result = checks.CoreChecks.\ networksegments_unique_constraint_check(mock.ANY) self.assertEqual(returned_code, result.code) + + def test_port_binding_profile_sanity(self): + new_format = {"allocation": + {"397aec7a-1f69-11ec-9f1a-7b14e597e275": + "41d7391e-1f69-11ec-a899-8f9d6d950f8d"}} + old_format = {"allocation": "41d7391e-1f69-11ec-a899-8f9d6d950f8d"} + cases = (([new_format], Code.SUCCESS), + ([old_format], Code.FAILURE)) + with mock.patch.object( + checks, 'port_binding_profiles') \ + as mock_port_binding_profiles: + for profile, returned_code in cases: + mock_port_binding_profiles.return_value = profile + result = checks.CoreChecks.port_binding_profile_sanity( + mock.ANY) + self.assertEqual(returned_code, result.code) diff --git a/releasenotes/notes/sanitize-port-binding-profile-5b0cba2566f7f950.yaml b/releasenotes/notes/sanitize-port-binding-profile-5b0cba2566f7f950.yaml new file mode 100644 index 00000000000..f5874cd99dd --- /dev/null +++ b/releasenotes/notes/sanitize-port-binding-profile-5b0cba2566f7f950.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Added a check to verify if all rows of ``ml2_port_bindings`` table in the + DB are using the new format for ``profile`` column. This check is part of + upgrade check, that can be executed with ``neutron-status upgrade check`` + command. If some rows are using obsolete format, they can be sanitized + with a script that can be executed with + ``neutron-sanitize-port-binding-profile-allocation`` command. diff --git a/setup.cfg b/setup.cfg index a7e6c1ac752..fbc66f2be72 100644 --- a/setup.cfg +++ b/setup.cfg @@ -61,6 +61,7 @@ console_scripts = neutron-ovn-metadata-agent = neutron.cmd.eventlet.agents.ovn_metadata:main neutron-ovn-migration-mtu = neutron.cmd.ovn.migration_mtu:main neutron-ovn-db-sync-util = neutron.cmd.ovn.neutron_ovn_db_sync_util:main + neutron-sanitize-port-binding-profile-allocation = neutron.cmd.sanitize_port_binding_profile_allocation:main neutron-sanitize-port-mac-addresses = neutron.cmd.sanitize_port_mac_addresses:main ml2ovn-trace = neutron.cmd.ovn.ml2ovn_trace:main neutron.core_plugins =