Add port NUMA affinity policy

Added a new port extension: NUMA affinity policy. This extension adds
the "numa_affinity_policy" parameter to the "port" API and specifies
the NUMA affinity policy per port.

This parameter is passed to Nova when a virtual machine is created.
Nova will use this information to schedule the virtual machine.

For backwards compatibility, this parameter will be "None" by default.

Depends-On: https://review.opendev.org/#/c/740058/
Closes-Bug: #1886798

Change-Id: Ie3d68c098ddb727ab8333aa1de4064e67a4f00a7
This commit is contained in:
Rodolfo Alonso Hernandez 2020-07-08 16:13:51 +00:00
parent 62daa0dcbc
commit a217a5d290
19 changed files with 419 additions and 8 deletions

View File

@ -32,6 +32,7 @@ from neutron_lib.api.definitions import network_availability_zone
from neutron_lib.api.definitions import network_ip_availability
from neutron_lib.api.definitions import network_mtu
from neutron_lib.api.definitions import pagination
from neutron_lib.api.definitions import port_numa_affinity_policy
from neutron_lib.api.definitions import port_resource_request
from neutron_lib.api.definitions import port_security
from neutron_lib.api.definitions import portbindings
@ -82,6 +83,7 @@ ML2_SUPPORTED_API_EXTENSIONS = [
network_mtu.ALIAS,
network_availability_zone.ALIAS,
network_ip_availability.ALIAS,
port_numa_affinity_policy.ALIAS,
port_security.ALIAS,
provider_net.ALIAS,
port_resource_request.ALIAS,

View File

@ -1 +1 @@
I38991de2b4
532aa95457e2

View File

@ -0,0 +1,44 @@
# Copyright 2020 OpenStack Foundation
#
# 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 alembic import op
from neutron_lib import constants as n_const
from neutron_lib.db import constants as db_const
import sqlalchemy as sa
"""port_numa_affinity_policy
Revision ID: 532aa95457e2
Revises: I38991de2b4
Create Date: 2020-07-10 14:59:18.868245
"""
# revision identifiers, used by Alembic.
revision = '532aa95457e2'
down_revision = 'I38991de2b4'
def upgrade():
op.create_table('portnumaaffinitypolicies',
sa.Column('port_id',
sa.String(length=db_const.UUID_FIELD_SIZE),
sa.ForeignKey('ports.id', ondelete='CASCADE'),
primary_key=True),
sa.Column('numa_affinity_policy',
sa.Enum(*n_const.PORT_NUMA_POLICIES,
name='numa_affinity_policy'))
)

View File

@ -0,0 +1,36 @@
# Copyright 2020 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.
from neutron_lib import constants as n_const
from neutron_lib.db import constants as db_const
from neutron_lib.db import model_base
import sqlalchemy as sa
from neutron.db import models_v2
class PortNumaAffinityPolicy(model_base.BASEV2):
__tablename__ = 'portnumaaffinitypolicies'
port_id = sa.Column(sa.String(db_const.UUID_FIELD_SIZE),
sa.ForeignKey('ports.id', ondelete='CASCADE'),
primary_key=True)
numa_affinity_policy = sa.Column(sa.Enum(*n_const.PORT_NUMA_POLICIES,
name='numa_affinity_policy'))
port = sa.orm.relationship(
models_v2.Port, load_on_pending=True,
backref=sa.orm.backref('numa_affinity_policy', uselist=False,
cascade='delete', lazy='joined'))
revises_on_change = ('port', )

View File

@ -0,0 +1,67 @@
# Copyright (c) 2020 Red Hat, Inc.
#
# 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.api.definitions import port_numa_affinity_policy as pnap
from neutron_lib.api.definitions import portbindings
from neutron_lib import exceptions as n_exc
from neutron.objects.port.extensions import port_numa_affinity_policy as \
pnap_obj
class PortNumaAffinityPolicyDbMixin(object):
"""Mixin class to add NUMA affinity policy to a port"""
def _process_create_port(self, context, data, result):
if not data.get(pnap.NUMA_AFFINITY_POLICY):
result[pnap.NUMA_AFFINITY_POLICY] = None
return
obj = pnap_obj.PortNumaAffinityPolicy(
context, port_id=result['id'],
numa_affinity_policy=data[pnap.NUMA_AFFINITY_POLICY])
obj.create()
result[pnap.NUMA_AFFINITY_POLICY] = data[pnap.NUMA_AFFINITY_POLICY]
def _process_update_port(self, context, data, result):
if pnap.NUMA_AFFINITY_POLICY not in data:
return
if (result[portbindings.VIF_TYPE] not in
portbindings.VIF_UNPLUGGED_TYPES):
raise n_exc.PortBoundNUMAAffinityPolicy(
port_id=result['id'], host_id=result[portbindings.HOST_ID],
numa_affinity_policy=data[pnap.NUMA_AFFINITY_POLICY])
obj = pnap_obj.PortNumaAffinityPolicy.get_object(context,
port_id=result['id'])
if data[pnap.NUMA_AFFINITY_POLICY]:
if not obj:
return self._process_create_port(context, data, result)
obj.update_fields(
{pnap.NUMA_AFFINITY_POLICY: data[pnap.NUMA_AFFINITY_POLICY]})
obj.update()
else:
if obj:
obj.delete()
result[pnap.NUMA_AFFINITY_POLICY] = data[pnap.NUMA_AFFINITY_POLICY]
def _extend_port_dict(self, port_db, result):
if port_db.numa_affinity_policy:
result[pnap.NUMA_AFFINITY_POLICY] = (
port_db.numa_affinity_policy.numa_affinity_policy)
else:
result[pnap.NUMA_AFFINITY_POLICY] = None

View File

@ -0,0 +1,20 @@
# Copyright (c) 2020 Red Hat, Inc.
#
# 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.api.definitions import port_numa_affinity_policy
from neutron_lib.api import extensions as api_extensions
class Port_numa_affinity_policy(api_extensions.APIExtensionDescriptor):
api_definition = port_numa_affinity_policy

View File

@ -0,0 +1,44 @@
# Copyright (c) 2020 Red Hat, Inc.
#
# 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 constants as lib_constants
from neutron_lib.objects import common_types
from oslo_versionedobjects import fields as obj_fields
from neutron.db.models import port_numa_affinity_policy
from neutron.objects import base
# TODO(ralonsoh): move to neutron_lib.objects.common_types
class NumaAffinityPoliciesEnumField(obj_fields.AutoTypedField):
AUTO_TYPE = obj_fields.Enum(valid_values=lib_constants.PORT_NUMA_POLICIES)
@base.NeutronObjectRegistry.register
class PortNumaAffinityPolicy(base.NeutronDbObject):
# Version 1.0: Initial version
VERSION = '1.0'
db_model = port_numa_affinity_policy.PortNumaAffinityPolicy
primary_keys = ['port_id']
new_facade = True
fields = {
'port_id': common_types.UUIDField(),
'numa_affinity_policy': NumaAffinityPoliciesEnumField(),
}
foreign_keys = {'Port': {'port_id': 'id'}}

View File

@ -273,7 +273,8 @@ class Port(base.NeutronDbObject):
# Version 1.3: distributed_binding -> distributed_bindings
# Version 1.4: Attribute binding becomes ListOfObjectsField
# Version 1.5: Added qos_network_policy_id field
VERSION = '1.5'
# Version 1.6: Added numa_affinity_policy field
VERSION = '1.6'
db_model = models_v2.Port
@ -323,6 +324,7 @@ class Port(base.NeutronDbObject):
'binding_levels': obj_fields.ListOfObjectsField(
'PortBindingLevel', nullable=True
),
'numa_affinity_policy': obj_fields.StringField(nullable=True),
# TODO(ihrachys): consider adding a 'dns_assignment' fully synthetic
# field in later object iterations
@ -341,6 +343,7 @@ class Port(base.NeutronDbObject):
'distributed_bindings',
'dns',
'fixed_ips',
'numa_affinity_policy',
'qos_policy_id',
'qos_network_policy_id',
'security',
@ -501,6 +504,11 @@ class Port(base.NeutronDbObject):
db_obj.qos_network_policy_binding.policy_id)
fields_to_change.append('qos_network_policy_binding')
if db_obj.get('numa_affinity_policy'):
self.numa_affinity_policy = (
db_obj.numa_affinity_policy.numa_affinity_policy)
fields_to_change.append('numa_affinity_policy')
self.obj_reset_changes(fields_to_change)
def obj_make_compatible(self, primitive, target_version):
@ -529,6 +537,8 @@ class Port(base.NeutronDbObject):
break
if _target_version < (1, 5):
primitive.pop('qos_network_policy_id', None)
if _target_version < (1, 6):
primitive.pop('numa_affinity_policy', None)
@classmethod
def get_ports_by_router_and_network(cls, context, router_id, owner,

View File

@ -0,0 +1,47 @@
# Copyright 2020 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.
from neutron_lib.api.definitions import port_numa_affinity_policy as pnap
from neutron_lib.plugins.ml2 import api
from oslo_log import log as logging
from neutron.db import port_numa_affinity_policy_db
LOG = logging.getLogger(__name__)
class PortNumaAffinityPolicyExtensionDriver(
api.ExtensionDriver,
port_numa_affinity_policy_db.PortNumaAffinityPolicyDbMixin):
_supported_extension_alias = pnap.ALIAS
def initialize(self):
LOG.info('PortNumaAffinityPolicyExtensionDriver initialization '
'complete')
@property
def extension_alias(self):
return self._supported_extension_alias
def process_create_port(self, context, data, result):
self._process_create_port(context, data, result)
def process_update_port(self, context, data, result):
self._process_update_port(context, data, result)
def extend_port_dict(self, session, port_db, result):
self._extend_port_dict(port_db, result)

View File

@ -40,6 +40,7 @@ from neutron_lib.api.definitions import network_mtu as mtu_apidef
from neutron_lib.api.definitions import network_mtu_writable as mtuw_apidef
from neutron_lib.api.definitions import port as port_def
from neutron_lib.api.definitions import port_mac_address_regenerate
from neutron_lib.api.definitions import port_numa_affinity_policy as pnap_def
from neutron_lib.api.definitions import port_security as psec
from neutron_lib.api.definitions import portbindings
from neutron_lib.api.definitions import portbindings_extended as pbe_ext
@ -213,7 +214,9 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
subnet_onboard_def.ALIAS,
subnetpool_prefix_ops_def.ALIAS,
stateful_security_group.ALIAS,
addrgrp_def.ALIAS]
addrgrp_def.ALIAS,
pnap_def.ALIAS,
]
# List of agent types for which all binding_failed ports should try to be
# rebound when agent revive
@ -378,12 +381,11 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
]
def _check_mac_update_allowed(self, orig_port, port, binding):
unplugged_types = (portbindings.VIF_TYPE_BINDING_FAILED,
portbindings.VIF_TYPE_UNBOUND)
new_mac = port.get('mac_address')
mac_change = (new_mac is not None and
orig_port['mac_address'] != new_mac)
if (mac_change and binding.vif_type not in unplugged_types):
if (mac_change and binding.vif_type not in
portbindings.VIF_UNPLUGGED_TYPES):
raise exc.PortBound(port_id=orig_port['id'],
vif_type=binding.vif_type,
old_mac=orig_port['mac_address'],

View File

@ -53,6 +53,7 @@ NETWORK_API_EXTENSIONS+=",router-admin-state-down-before-update"
NETWORK_API_EXTENSIONS+=",router_availability_zone"
NETWORK_API_EXTENSIONS+=",security-group"
NETWORK_API_EXTENSIONS+=",port-mac-address-regenerate"
NETWORK_API_EXTENSIONS+=",port-numa-affinity-policy"
NETWORK_API_EXTENSIONS+=",port-security-groups-filtering"
NETWORK_API_EXTENSIONS+=",segment"
NETWORK_API_EXTENSIONS+=",segments-peer-subnet-host-routes"

View File

@ -266,3 +266,7 @@ def get_random_ipv6_mode():
def get_random_security_event():
return random.choice(log_const.LOG_EVENTS)
def get_random_port_numa_affinity_policy():
return random.choice(constants.PORT_NUMA_POLICIES)

View File

@ -450,7 +450,7 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase):
for arg in (('admin_state_up', 'device_id',
'mac_address', 'name', 'fixed_ips',
'tenant_id', 'device_owner', 'security_groups',
'propagate_uplink_status') +
'propagate_uplink_status', 'numa_affinity_policy') +
(arg_list or ())):
# Arg must be present
if arg in kwargs:

View File

@ -0,0 +1,85 @@
# Copyright (c) 2020 Red Hat, Inc.
#
# 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 ddt
from neutron_lib.api.definitions import port_numa_affinity_policy as apidef
from neutron_lib.api.definitions import portbindings
from neutron_lib import constants
from neutron.db import db_base_plugin_v2
from neutron.db import port_numa_affinity_policy_db as pnap_db
from neutron.tests.unit.db import test_db_base_plugin_v2
class PortNumaAffinityPolicyExtensionExtensionTestPlugin(
db_base_plugin_v2.NeutronDbPluginV2,
pnap_db.PortNumaAffinityPolicyDbMixin):
"""Test plugin to mixin the port NUMA affinity policy extension."""
supported_extension_aliases = [apidef.ALIAS]
def create_port(self, context, port):
with context.session.begin(subtransactions=True):
new_port = super(
PortNumaAffinityPolicyExtensionExtensionTestPlugin,
self).create_port(context, port)
self._process_create_port(context, port['port'], new_port)
return new_port
def update_port(self, context, id, port):
with context.session.begin(subtransactions=True):
updated_port = super(
PortNumaAffinityPolicyExtensionExtensionTestPlugin,
self).update_port(context, id, port)
updated_port[portbindings.VIF_TYPE] = portbindings.VIF_TYPE_UNBOUND
self._process_update_port(context, port['port'], updated_port)
return updated_port
@ddt.ddt
class PortNumaAffinityPolicyExtensionExtensionTestCase(
test_db_base_plugin_v2.NeutronDbPluginV2TestCase):
"""Test API extension numa_affinity_policy attributes."""
def setUp(self, *args):
plugin = ('neutron.tests.unit.extensions.test_port_numa_affinity_'
'policy.PortNumaAffinityPolicyExtensionExtensionTestPlugin')
super(PortNumaAffinityPolicyExtensionExtensionTestCase,
self).setUp(plugin=plugin)
def _create_and_check_port_nap(self, numa_affinity_policy):
name = 'numa_affinity_policy'
keys = [('name', name), ('admin_state_up', True),
('status', self.port_create_status),
('numa_affinity_policy', numa_affinity_policy)]
with self.port(name=name,
numa_affinity_policy=numa_affinity_policy) as port:
for k, v in keys:
self.assertEqual(v, port['port'][k])
return port
def _update_and_check_port_nap(self, port, numa_affinity_policy):
data = {'port': {'numa_affinity_policy': numa_affinity_policy}}
req = self.new_update_request('ports', data,
port['port']['id'])
res = self.deserialize(self.fmt, req.get_response(self.api))
self.assertEqual(numa_affinity_policy,
res['port']['numa_affinity_policy'])
@ddt.data(*constants.PORT_NUMA_POLICIES, None)
def test_create_and_update_port_numa_affinity_policy(self,
numa_affinity_policy):
port = self._create_and_check_port_nap(numa_affinity_policy)
for new_nap in (*constants.PORT_NUMA_POLICIES, None):
self._update_and_check_port_nap(port, new_nap)

View File

@ -0,0 +1,38 @@
# Copyright (c) 2020 Red Hat, Inc.
#
# 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 constants
from neutron.objects.port.extensions import port_numa_affinity_policy
from neutron.tests.unit.objects import test_base as obj_test_base
from neutron.tests.unit import testlib_api
class PortNumaAffinityPolicyIfaceObjectTestCase(
obj_test_base.BaseObjectIfaceTestCase):
_test_class = port_numa_affinity_policy.PortNumaAffinityPolicy
class PortNumaAffinityPolicyDbObjectTestCase(
obj_test_base.BaseDbObjectTestCase,
testlib_api.SqlTestCase):
_test_class = port_numa_affinity_policy.PortNumaAffinityPolicy
def setUp(self):
super(PortNumaAffinityPolicyDbObjectTestCase, self).setUp()
self.update_obj_fields(
{'port_id': lambda: self._create_test_port_id(),
'numa_affinity_policy': constants.PORT_NUMA_POLICY_PREFERRED})

View File

@ -44,6 +44,7 @@ from neutron.objects import base
from neutron.objects.db import api as obj_db_api
from neutron.objects import flavor
from neutron.objects import network as net_obj
from neutron.objects.port.extensions import port_numa_affinity_policy
from neutron.objects import ports
from neutron.objects.qos import policy as qos_policy
from neutron.objects import rbac_db
@ -541,6 +542,8 @@ FIELD_TYPE_VALUE_GENERATOR_MAP = {
obj_fields.ListOfStringsField: tools.get_random_string_list,
obj_fields.ObjectField: lambda: None,
obj_fields.StringField: lambda: helpers.get_random_string(10),
port_numa_affinity_policy.NumaAffinityPoliciesEnumField:
tools.get_random_port_numa_affinity_policy,
}

View File

@ -69,7 +69,8 @@ object_data = {
'NetworkSegment': '1.0-57b7f2960971e3b95ded20cbc59244a8',
'NetworkSegmentRange': '1.0-bdec1fffc9058ea676089b1f2f2b3cf3',
'NetworkSubnetLock': '1.0-140de39d4b86ae346dc3d70b885bea53',
'Port': '1.5-98f35183d876c9beb188f4bf44d4d886',
'Port': '1.6-c9a1ecc035181aeb0af76eb395c09ac0',
'PortNumaAffinityPolicy': '1.0-38fcea43e7bfb2536461f3d053c43aa3',
'PortBinding': '1.0-3306deeaa6deb01e33af06777d48d578',
'PortBindingLevel': '1.1-50d47f63218f87581b6cd9a62db574e5',
'PortDataPlaneStatus': '1.0-25be74bda46c749653a10357676c0ab2',

View File

@ -482,6 +482,12 @@ class PortDbObjectTestCase(obj_test_base.BaseDbObjectTestCase,
self.assertNotIn('qos_network_policy_id',
port_v1_4['versioned_object.data'])
def test_v1_6_to_v1_5_drops_numa_affinity_policy(self):
port_new = self._create_test_port()
port_v1_5 = port_new.obj_to_primitive(target_version='1.5')
self.assertNotIn('numa_affinity_policy',
port_v1_5['versioned_object.data'])
def test_get_ports_ids_by_security_groups_except_router(self):
sg_id = self._create_test_security_group_id()
filter_owner = constants.ROUTER_INTERFACE_OWNERS_SNAT

View File

@ -113,6 +113,7 @@ neutron.ml2.extension_drivers =
dns = neutron.plugins.ml2.extensions.dns_integration:DNSExtensionDriverML2
data_plane_status = neutron.plugins.ml2.extensions.data_plane_status:DataPlaneStatusExtensionDriver
dns_domain_ports = neutron.plugins.ml2.extensions.dns_integration:DNSDomainPortsExtensionDriver
port_numa_affinity_policy = neutron.plugins.ml2.extensions.port_numa_affinity_policy:PortNumaAffinityPolicyExtensionDriver
uplink_status_propagation = neutron.plugins.ml2.extensions.uplink_status_propagation:UplinkStatusPropagationExtensionDriver
tag_ports_during_bulk_creation = neutron.plugins.ml2.extensions.tag_ports_during_bulk_creation:TagPortsDuringBulkCreationExtensionDriver
subnet_dns_publish_fixed_ip = neutron.plugins.ml2.extensions.subnet_dns_publish_fixed_ip:SubnetDNSPublishFixedIPExtensionDriver