Add port device profile extension

Added a new port extension: device profile (``port_device_profile``).
This extension adds the "device_profile" parameter to the "port" API
and specifies the device profile per port. This parameter is a
string.

This parameter is passed to Nova and Nova retrieves the requested
device profile from Cyborg. Reference:
  https://docs.openstack.org/api-ref/accelerator/v2/index.html#
    device-profiles

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

Closes-Bug: #1906602
Depends-On: https://review.opendev.org/c/openstack/neutron-lib/+/767586

Change-Id: I1202a8388e64ae4270ef4ca118993504ae7c1731
This commit is contained in:
Rodolfo Alonso Hernandez 2020-12-18 11:06:18 +00:00
parent 4184bae651
commit 8912ea5575
20 changed files with 318 additions and 9 deletions

View File

@ -51,7 +51,7 @@ msgpack-python==0.4.0
munch==2.1.0
netaddr==0.7.18
netifaces==0.10.4
neutron-lib==2.6.1
neutron-lib==2.8.0
openstacksdk==0.31.2
os-client-config==1.28.0
os-ken==0.3.0
@ -60,7 +60,7 @@ os-vif==1.15.1
osc-lib==1.8.0
oslo.cache==1.26.0
oslo.concurrency==3.26.0
oslo.config==6.8.0
oslo.config==8.0.0
oslo.context==2.22.0
oslo.db==4.44.0
oslo.i18n==3.20.0

View File

@ -33,6 +33,7 @@ from neutron_lib.api.definitions import network_ip_availability
from neutron_lib.api.definitions import network_mtu
from neutron_lib.api.definitions import network_mtu_writable
from neutron_lib.api.definitions import pagination
from neutron_lib.api.definitions import port_device_profile
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
@ -87,6 +88,7 @@ ML2_SUPPORTED_API_EXTENSIONS = [
network_mtu_writable.ALIAS,
network_availability_zone.ALIAS,
network_ip_availability.ALIAS,
port_device_profile.ALIAS,
port_numa_affinity_policy.ALIAS,
port_security.ALIAS,
provider_net.ALIAS,

View File

@ -1 +1 @@
26d1e9f5c766
1e0744e4ffea

View File

@ -0,0 +1,43 @@
# 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.db import constants as db_const
import sqlalchemy as sa
"""port_device_profile
Revision ID: 1e0744e4ffea
Revises: 26d1e9f5c766
Create Date: 2020-12-18 10:12:14.865465
"""
# revision identifiers, used by Alembic.
revision = '1e0744e4ffea'
down_revision = '26d1e9f5c766'
def upgrade():
op.create_table('portdeviceprofiles',
sa.Column('port_id',
sa.String(length=db_const.UUID_FIELD_SIZE),
sa.ForeignKey('ports.id', ondelete='CASCADE'),
primary_key=True),
sa.Column('device_profile',
sa.String(255),
nullable=True)
)

View File

@ -0,0 +1,34 @@
# 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.db import constants as db_const
from neutron_lib.db import model_base
import sqlalchemy as sa
from neutron.db import models_v2
class PortDeviceProfile(model_base.BASEV2):
__tablename__ = 'portdeviceprofiles'
port_id = sa.Column(sa.String(db_const.UUID_FIELD_SIZE),
sa.ForeignKey('ports.id', ondelete='CASCADE'),
primary_key=True)
device_profile = sa.Column('device_profile', sa.String(length=255))
port = sa.orm.relationship(
models_v2.Port, load_on_pending=True,
backref=sa.orm.backref('device_profile', uselist=False,
cascade='delete', lazy='joined'))
revises_on_change = ('port', )

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.api.definitions import port_device_profile as pdp
from neutron.objects.port.extensions import port_device_profile as pdp_obj
class PortDeviceProfileMixin(object):
"""Mixin class to add device profile (Cyborg) to a port"""
def _process_create_port(self, context, data, result):
if not data.get(pdp.DEVICE_PROFILE):
result[pdp.DEVICE_PROFILE] = None
return
obj = pdp_obj.PortDeviceProfile(
context, port_id=result['id'],
device_profile=data[pdp.DEVICE_PROFILE])
obj.create()
result[pdp.DEVICE_PROFILE] = data[pdp.DEVICE_PROFILE]
def _extend_port_dict(self, port_db, result):
if port_db.device_profile:
result[pdp.DEVICE_PROFILE] = port_db.device_profile.device_profile
else:
result[pdp.DEVICE_PROFILE] = 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_device_profile
from neutron_lib.api import extensions as api_extensions
class Port_device_profile(api_extensions.APIExtensionDescriptor):
api_definition = port_device_profile

View File

@ -0,0 +1,36 @@
# 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.objects import common_types
from oslo_versionedobjects import fields as obj_fields
from neutron.db.models import port_device_profile
from neutron.objects import base
@base.NeutronObjectRegistry.register
class PortDeviceProfile(base.NeutronDbObject):
# Version 1.0: Initial version
VERSION = '1.0'
db_model = port_device_profile.PortDeviceProfile
primary_keys = ['port_id']
fields = {
'port_id': common_types.UUIDField(),
'device_profile': obj_fields.StringField(nullable=True),
}
foreign_keys = {'Port': {'port_id': 'id'}}

View File

@ -274,7 +274,8 @@ class Port(base.NeutronDbObject):
# Version 1.4: Attribute binding becomes ListOfObjectsField
# Version 1.5: Added qos_network_policy_id field
# Version 1.6: Added numa_affinity_policy field
VERSION = '1.6'
# Version 1.7: Added port_device field
VERSION = '1.7'
db_model = models_v2.Port
@ -325,6 +326,7 @@ class Port(base.NeutronDbObject):
'PortBindingLevel', nullable=True
),
'numa_affinity_policy': obj_fields.StringField(nullable=True),
'device_profile': obj_fields.StringField(nullable=True),
# TODO(ihrachys): consider adding a 'dns_assignment' fully synthetic
# field in later object iterations
@ -339,6 +341,7 @@ class Port(base.NeutronDbObject):
'bindings',
'binding_levels',
'data_plane_status',
'device_profile',
'dhcp_options',
'distributed_bindings',
'dns',
@ -509,6 +512,10 @@ class Port(base.NeutronDbObject):
db_obj.numa_affinity_policy.numa_affinity_policy)
fields_to_change.append('numa_affinity_policy')
if db_obj.get('device_profile'):
self.device_profile = db_obj.device_profile.device_profile
fields_to_change.append('device_profile')
self.obj_reset_changes(fields_to_change)
def obj_make_compatible(self, primitive, target_version):
@ -539,6 +546,8 @@ class Port(base.NeutronDbObject):
primitive.pop('qos_network_policy_id', None)
if _target_version < (1, 6):
primitive.pop('numa_affinity_policy', None)
if _target_version < (1, 7):
primitive.pop('device_profile', None)
@classmethod
def get_ports_by_router_and_network(cls, context, router_id, owner,

View File

@ -0,0 +1,42 @@
# 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_device_profile as pdp
from neutron_lib.plugins.ml2 import api
from oslo_log import log as logging
from neutron.db import port_device_profile_db
LOG = logging.getLogger(__name__)
class PortDeviceProfileExtensionDriver(
api.ExtensionDriver, port_device_profile_db.PortDeviceProfileMixin):
_supported_extension_alias = pdp.ALIAS
def initialize(self):
LOG.info('PortDeviceProfileExtensionDriver 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 extend_port_dict(self, session, port_db, result):
self._extend_port_dict(port_db, result)

View File

@ -39,6 +39,7 @@ from neutron_lib.api.definitions import network_availability_zone
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_device_profile as pdp_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
@ -221,6 +222,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
stateful_security_group.ALIAS,
addrgrp_def.ALIAS,
pnap_def.ALIAS,
pdp_def.ALIAS,
]
# List of agent types for which all binding_failed ports should try to be

View File

@ -54,6 +54,7 @@ NETWORK_API_EXTENSIONS+=",router-admin-state-down-before-update"
NETWORK_API_EXTENSIONS+=",router_availability_zone"
NETWORK_API_EXTENSIONS+=",security-group"
NETWORK_API_EXTENSIONS+=",security-groups-remote-address-group"
NETWORK_API_EXTENSIONS+=",port-device-profile"
NETWORK_API_EXTENSIONS+=",port-mac-address-regenerate"
NETWORK_API_EXTENSIONS+=",port-numa-affinity-policy"
NETWORK_API_EXTENSIONS+=",port-security-groups-filtering"

View File

@ -450,8 +450,8 @@ 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', 'numa_affinity_policy') +
(arg_list or ())):
'propagate_uplink_status', 'numa_affinity_policy',
'device_profile') + (arg_list or ())):
# Arg must be present
if arg in kwargs:
data['port'][arg] = kwargs[arg]

View File

@ -0,0 +1,58 @@
# 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_device_profile as apidef
from neutron_lib.db import api as db_api
from neutron.db import db_base_plugin_v2
from neutron.db import port_device_profile_db as pdp_db
from neutron.tests.unit.db import test_db_base_plugin_v2
class PortDeviceProfileExtensionTestPlugin(
db_base_plugin_v2.NeutronDbPluginV2,
pdp_db.PortDeviceProfileMixin):
"""Test plugin to mixin the port device profile extension."""
supported_extension_aliases = [apidef.ALIAS]
def create_port(self, context, port):
with db_api.CONTEXT_WRITER.using(context):
new_port = super(PortDeviceProfileExtensionTestPlugin,
self).create_port(context, port)
self._process_create_port(context, port['port'], new_port)
return new_port
@ddt.ddt
class PortDeviceProfileExtensionTestCase(
test_db_base_plugin_v2.NeutronDbPluginV2TestCase):
"""Test API extension numa_affinity_policy attributes."""
def setUp(self, *args):
plugin = ('neutron.tests.unit.extensions.test_port_device_profile.'
'PortDeviceProfileExtensionTestPlugin')
super(PortDeviceProfileExtensionTestCase, self).setUp(plugin=plugin)
@ddt.data('device_profile_1', None)
def test_create_and_check_port_device_profile(self, device_profile):
keys = [('name', 'name_1'),
('admin_state_up', True),
('status', self.port_create_status),
('device_profile', device_profile)]
with self.port(name='name_1', device_profile=device_profile) as port:
for k, v in keys:
self.assertEqual(v, port['port'][k])
return port

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_device_profile
from neutron.objects.port.extensions import port_numa_affinity_policy
from neutron.objects import ports
from neutron.objects.qos import policy as qos_policy
@ -542,6 +543,8 @@ FIELD_TYPE_VALUE_GENERATOR_MAP = {
obj_fields.StringField: lambda: helpers.get_random_string(10),
port_numa_affinity_policy.NumaAffinityPoliciesEnumField:
tools.get_random_port_numa_affinity_policy,
port_device_profile.PortDeviceProfile:
lambda: helpers.get_random_string(255)
}

View File

@ -69,7 +69,8 @@ object_data = {
'NetworkSegment': '1.0-57b7f2960971e3b95ded20cbc59244a8',
'NetworkSegmentRange': '1.0-bdec1fffc9058ea676089b1f2f2b3cf3',
'NetworkSubnetLock': '1.0-140de39d4b86ae346dc3d70b885bea53',
'Port': '1.6-c9a1ecc035181aeb0af76eb395c09ac0',
'Port': '1.7-d8c1cfe42cfa3719a5d810eeab79e006',
'PortDeviceProfile': '1.0-b98c7083cc3e93d176fd7a91ae13af32',
'PortNumaAffinityPolicy': '1.0-38fcea43e7bfb2536461f3d053c43aa3',
'PortBinding': '1.0-3306deeaa6deb01e33af06777d48d578',
'PortBindingLevel': '1.1-50d47f63218f87581b6cd9a62db574e5',

View File

@ -488,6 +488,12 @@ class PortDbObjectTestCase(obj_test_base.BaseDbObjectTestCase,
self.assertNotIn('numa_affinity_policy',
port_v1_5['versioned_object.data'])
def test_v1_7_to_v1_6_drops_device_profile(self):
port_new = self._create_test_port()
port_v1_6 = port_new.obj_to_primitive(target_version='1.6')
self.assertNotIn('device_profile',
port_v1_6['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

@ -0,0 +1,13 @@
---
features:
- |
Introduce the attribute ``port_device_profile`` to ports that
specifies the device profile needed per port. This parameter is
a string. This parameter is passed to Nova and Nova retrieves
the requested profile from Cyborg:
`Device profiles <https://docs.openstack.org/api-ref/accelerator/v2/index.html#device-profiles>`_.
Operators can turn on this feature via the configuration option::
[ml2]
extension_drivers = port_device_profile

View File

@ -16,7 +16,7 @@ Jinja2>=2.10 # BSD License (3 clause)
keystonemiddleware>=5.1.0 # Apache-2.0
netaddr>=0.7.18 # BSD
netifaces>=0.10.4 # MIT
neutron-lib>=2.6.1 # Apache-2.0
neutron-lib>=2.8.0 # Apache-2.0
python-neutronclient>=6.7.0 # Apache-2.0
tenacity>=6.0.0 # Apache-2.0
SQLAlchemy>=1.2.0 # MIT
@ -26,7 +26,7 @@ alembic>=0.9.6 # MIT
stevedore>=1.20.0 # Apache-2.0
oslo.cache>=1.26.0 # Apache-2.0
oslo.concurrency>=3.26.0 # Apache-2.0
oslo.config>=6.8.0 # Apache-2.0
oslo.config>=8.0.0 # Apache-2.0
oslo.context>=2.22.0 # Apache-2.0
oslo.db>=4.44.0 # Apache-2.0
oslo.i18n>=3.20.0 # Apache-2.0

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_device_profile = neutron.plugins.ml2.extensions.port_device_profile:PortDeviceProfileExtensionDriver
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