Allow to set/modify network mtu

This patch adds ``net-mtu-writable`` API extension that allows to write
to network ``mtu`` attribute.

The patch also adds support for the extension to ml2, as well as covers
the feature with unit and tempest tests. Agent side implementation of
the feature is moved into a separate patch to ease review.

DocImpact: neutron controller now supports ``net-mtu-writable`` API
           extension.
APIImpact: new ``net-mtu-writable`` API extension was added.

Related-Bug: #1671634
Change-Id: Ib232796562edd8fa69ec06b0cc5cb752c1467add
This commit is contained in:
Ihar Hrachyshka 2017-08-07 10:18:11 -07:00 committed by Kevin Benton
parent e97ee8a972
commit f21c7e2851
13 changed files with 301 additions and 72 deletions

View File

@ -29,10 +29,10 @@ architectures should avoid cases 2 and 3.
.. note::
You can trigger MTU recalculation for existing networks by changing the
MTU configuration and restarting the ``neutron-server`` service.
However, propagating MTU calculations to the data plane may require
users to delete and recreate ports on the network.
After you adjust MTU configuration options in ``neutron.conf`` and
``ml2_conf.ini``, you should update ``mtu`` attribute for all existing
networks that need a new MTU. (Network MTU update is available for all core
plugins that implement the ``net-mtu-writable`` API extension.)
When using the Open vSwitch or Linux bridge drivers, new MTU calculations
will be propogated automatically after restarting the ``l3-agent`` service.

View File

@ -379,6 +379,7 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
args = {'tenant_id': n['tenant_id'],
'id': n.get('id') or uuidutils.generate_uuid(),
'name': n['name'],
'mtu': n.get('mtu'),
'admin_state_up': n['admin_state_up'],
'status': n.get('status', constants.NET_STATUS_ACTIVE),
'description': n.get('description')}
@ -465,21 +466,34 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
network = self._get_network(context, id)
return self._make_network_dict(network, fields, context=context)
@db_api.retry_if_session_inactive()
def _get_networks(self, context, filters=None, fields=None,
sorts=None, limit=None, marker=None,
page_reverse=False):
marker_obj = ndb_utils.get_marker_obj(self, context, 'network',
limit, marker)
return model_query.get_collection(
context, models_v2.Network,
# if caller needs postprocessing, it should implement it explicitly
dict_func=None,
filters=filters, fields=fields,
sorts=sorts,
limit=limit,
marker_obj=marker_obj,
page_reverse=page_reverse)
@db_api.retry_if_session_inactive()
def get_networks(self, context, filters=None, fields=None,
sorts=None, limit=None, marker=None,
page_reverse=False):
marker_obj = ndb_utils.get_marker_obj(self, context, 'network',
limit, marker)
make_network_dict = functools.partial(self._make_network_dict,
context=context)
return model_query.get_collection(context, models_v2.Network,
make_network_dict,
filters=filters, fields=fields,
sorts=sorts,
limit=limit,
marker_obj=marker_obj,
page_reverse=page_reverse)
return [
make_network_dict(net, fields)
for net in self._get_networks(
context, filters=filters, fields=fields, sorts=sorts,
limit=limit, marker=marker, page_reverse=page_reverse)
]
@db_api.retry_if_session_inactive()
def get_networks_count(self, context, filters=None):

View File

@ -1 +1 @@
349b6fd605a6
7d32f979895f

View File

@ -0,0 +1,38 @@
# 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.
#
"""add mtu for networks
Revision ID: 7d32f979895f
Revises: c8c222d42aa9
Create Date: 2017-07-13 19:25:29.204547
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '7d32f979895f'
down_revision = '349b6fd605a6'
# require the migration rule that dropped the mtu column in the past
depends_on = ('b67e765a3524',)
def upgrade():
op.add_column('networks',
sa.Column('mtu',
sa.Integer(),
nullable=True))

View File

@ -267,6 +267,9 @@ class Network(standard_attr.HasStandardAttributes, model_base.BASEV2,
lazy='subquery',
cascade='all, delete, delete-orphan')
availability_zone_hints = sa.Column(sa.String(255))
# TODO(ihrachys) provide data migration path to fill in mtus for existing
# networks in Queens when all controllers run Pike+ code
mtu = sa.Column(sa.Integer, nullable=True)
dhcp_agents = orm.relationship(
'Agent', lazy='subquery', viewonly=True,
secondary=ndab_model.NetworkDhcpAgentBinding.__table__)

View File

@ -0,0 +1,58 @@
# 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 import converters
from neutron_lib.api.definitions import network
from neutron_lib.api.definitions import network_mtu
from neutron_lib.api import extensions
#TODO(ihrachys) migrate api definition to neutron-lib
MTU = 'mtu'
RESOURCE_ATTRIBUTE_MAP = {
network.COLLECTION_NAME: {
MTU: {'allow_post': True, 'allow_put': True, 'is_visible': True,
'validate': {'type:non_negative': None}, 'default': 0,
'convert_to': converters.convert_to_int},
},
}
class Netmtu_writable(extensions.ExtensionDescriptor):
"""Extension class supporting writable network MTU."""
@classmethod
def get_name(cls):
return 'Network MTU (writable)'
@classmethod
def get_alias(cls):
return 'net-mtu-writable'
@classmethod
def get_description(cls):
return 'Provides a writable MTU attribute for a network resource.'
@classmethod
def get_updated(cls):
return '2017-07-12T00:00:00-00:00'
def get_extended_resources(self, version):
if version == "2.0":
return RESOURCE_ATTRIBUTE_MAP
else:
return {}
def get_required_extensions(self):
return [network_mtu.ALIAS]

View File

@ -186,10 +186,6 @@ class Network(rbac_db.NeutronRbacObject):
synthetic_fields = [
'dns_domain',
# MTU is not stored in the database any more, it's a synthetic field
# that may be used by plugins to provide a canonical representation for
# the resource
'mtu',
'qos_policy_id',
'security',
'segments',

View File

@ -19,7 +19,6 @@ from neutron_lib.api.definitions import network as net_def
from neutron_lib.api.definitions import port as port_def
from neutron_lib.api.definitions import port_security as psec
from neutron_lib.api.definitions import portbindings
from neutron_lib.api.definitions import provider_net
from neutron_lib.api.definitions import subnet as subnet_def
from neutron_lib.api import validators
from neutron_lib.callbacks import events
@ -77,9 +76,10 @@ from neutron.db import subnet_service_type_db_models as service_type_db
from neutron.db import vlantransparent_db
from neutron.extensions import allowedaddresspairs as addr_pair
from neutron.extensions import availability_zone as az_ext
from neutron.extensions import multiprovidernet as mpnet
from neutron.extensions import netmtu_writable as mtu_ext
from neutron.extensions import providernet as provider
from neutron.extensions import vlantransparent
from neutron.plugins.common import utils as p_utils
from neutron.plugins.ml2.common import exceptions as ml2_exc
from neutron.plugins.ml2 import config # noqa
from neutron.plugins.ml2 import db
@ -146,7 +146,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
"dhcp_agent_scheduler",
"multi-provider", "allowed-address-pairs",
"extra_dhcp_opt", "subnet_allocation",
"net-mtu", "vlan-transparent",
"net-mtu", "net-mtu-writable",
"vlan-transparent",
"address-scope",
"availability_zone",
"network_availability_zone",
@ -724,14 +725,16 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
self._delete_objects(context, resource, to_delete)
return objects
def _get_network_mtu(self, network):
def _get_network_mtu(self, network_db, validate=True):
mtus = []
try:
segments = network[mpnet.SEGMENTS]
segments = network_db['segments']
except KeyError:
segments = [network]
segments = [network_db]
for s in segments:
segment_type = s[provider_net.NETWORK_TYPE]
segment_type = s.get('network_type')
if segment_type is None:
continue
try:
type_driver = self.type_manager.drivers[segment_type].obj
except KeyError:
@ -742,7 +745,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
# a bad setup, it's better to be safe than sorry here. Also,
# several unit tests use non-existent driver types that may
# trigger the exception here.
if segment_type and s[provider_net.SEGMENTATION_ID]:
if segment_type and s['segmentation_id']:
LOG.warning(
_LW("Failed to determine MTU for segment "
"%(segment_type)s:%(segment_id)s; network "
@ -750,18 +753,29 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
"accurate"),
{
'segment_type': segment_type,
'segment_id': s[provider_net.SEGMENTATION_ID],
'network_id': network['id'],
'segment_id': s['segmentation_id'],
'network_id': network_db['id'],
}
)
else:
mtu = type_driver.get_mtu(s[provider_net.PHYSICAL_NETWORK])
mtu = type_driver.get_mtu(s['physical_network'])
# Some drivers, like 'local', may return None; the assumption
# then is that for the segment type, MTU has no meaning or
# unlimited, and so we should then ignore those values.
if mtu:
mtus.append(mtu)
return min(mtus) if mtus else 0
max_mtu = min(mtus) if mtus else p_utils.get_deployment_physnet_mtu()
net_mtu = network_db.get('mtu')
if validate:
# validate that requested mtu conforms to allocated segments
if net_mtu and max_mtu and max_mtu < net_mtu:
msg = _("Requested MTU is too big, maximum is %d") % max_mtu
raise exc.InvalidInput(error_message=msg)
# if mtu is not set in database, use the maximum possible
return net_mtu or max_mtu
def _before_create_network(self, context, network):
net_data = network[net_def.RESOURCE_NAME]
@ -773,23 +787,29 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
tenant_id = net_data['tenant_id']
with db_api.context_manager.writer.using(context):
net_db = self.create_network_db(context, network)
result = self._make_network_dict(net_db, process_extensions=False,
context=context)
self.extension_manager.process_create_network(context, net_data,
result)
self._process_l3_create(context, result, net_data)
net_data['id'] = result['id']
net_data['id'] = net_db.id
self.type_manager.create_network_segments(context, net_data,
tenant_id)
net_db.mtu = self._get_network_mtu(net_db)
result = self._make_network_dict(net_db, process_extensions=False,
context=context)
self.extension_manager.process_create_network(
context,
# NOTE(ihrachys) extensions expect no id in the dict
{k: v for k, v in net_data.items() if k != 'id'},
result)
self._process_l3_create(context, result, net_data)
self.type_manager.extend_network_dict_provider(context, result)
# Update the transparent vlan if configured
if utils.is_extension_supported(self, 'vlan-transparent'):
vlt = vlantransparent.get_vlan_transparent(net_data)
net_db['vlan_transparent'] = vlt
result['vlan_transparent'] = vlt
result[api.MTU] = self._get_network_mtu(result)
if az_ext.AZ_HINTS in net_data:
self.validate_availability_zones(context, 'network',
net_data[az_ext.AZ_HINTS])
@ -853,6 +873,17 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
# Expire the db_network in current transaction, so that the join
# relationship can be updated.
context.session.expire(db_network)
if (
mtu_ext.MTU in net_data or
# NOTE(ihrachys) mtu may be null for existing networks,
# calculate and update it as needed; the conditional can be
# removed in Queens when we populate all mtu attributes and
# enforce it's not nullable on database level
db_network.mtu is None):
db_network.mtu = self._get_network_mtu(db_network,
validate=False)
updated_network = self._make_network_dict(
db_network, context=context)
self.type_manager.extend_network_dict_provider(
@ -889,27 +920,42 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
@db_api.retry_if_session_inactive()
def get_network(self, context, id, fields=None):
with db_api.context_manager.reader.using(context):
result = super(Ml2Plugin, self).get_network(context, id, None)
self.type_manager.extend_network_dict_provider(context, result)
result[api.MTU] = self._get_network_mtu(result)
# NOTE(ihrachys) use writer manager to be able to update mtu
# TODO(ihrachys) remove in Queens+ when mtu is not nullable
with db_api.context_manager.writer.using(context):
net_db = self._get_network(context, id)
return db_utils.resource_fields(result, fields)
# NOTE(ihrachys) pre Pike networks may have null mtus; update them
# in database if needed
# TODO(ihrachys) remove in Queens+ when mtu is not nullable
if net_db.mtu is None:
net_db.mtu = self._get_network_mtu(net_db, validate=False)
net_data = self._make_network_dict(net_db, context=context)
self.type_manager.extend_network_dict_provider(context, net_data)
return db_utils.resource_fields(net_data, fields)
@db_api.retry_if_session_inactive()
def get_networks(self, context, filters=None, fields=None,
sorts=None, limit=None, marker=None, page_reverse=False):
with db_api.context_manager.reader.using(context):
nets = super(Ml2Plugin,
self).get_networks(context, filters, None, sorts,
limit, marker, page_reverse)
self.type_manager.extend_networks_dict_provider(context, nets)
# NOTE(ihrachys) use writer manager to be able to update mtu
# TODO(ihrachys) remove in Queens when mtu is not nullable
with db_api.context_manager.writer.using(context):
nets_db = super(Ml2Plugin, self)._get_networks(
context, filters, None, sorts, limit, marker, page_reverse)
nets = self._filter_nets_provider(context, nets, filters)
for net in nets:
net[api.MTU] = self._get_network_mtu(net)
# NOTE(ihrachys) pre Pike networks may have null mtus; update them
# in database if needed
# TODO(ihrachys) remove in Queens+ when mtu is not nullable
net_data = []
for net in nets_db:
if net.mtu is None:
net.mtu = self._get_network_mtu(net, validate=False)
net_data.append(self._make_network_dict(net, context=context))
self.type_manager.extend_networks_dict_provider(context, net_data)
nets = self._filter_nets_provider(context, net_data, filters)
return [db_utils.resource_fields(net, fields) for net in nets]
def get_network_contexts(self, context, network_ids):
@ -970,11 +1016,17 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
with db_api.context_manager.writer.using(context):
result, net_db, ipam_sub = self._create_subnet_precommit(
context, subnet)
# NOTE(ihrachys) pre Pike networks may have null mtus; update them
# in database if needed
# TODO(ihrachys) remove in Queens+ when mtu is not nullable
if net_db['mtu'] is None:
net_db['mtu'] = self._get_network_mtu(net_db, validate=False)
self.extension_manager.process_create_subnet(
context, subnet[subnet_def.RESOURCE_NAME], result)
network = self._make_network_dict(net_db, context=context)
self.type_manager.extend_network_dict_provider(context, network)
network[api.MTU] = self._get_network_mtu(network)
mech_context = driver_context.SubnetContext(self, context,
result, network)
self.mechanism_manager.create_subnet_precommit(mech_context)
@ -1513,7 +1565,10 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
@db_api.retry_if_session_inactive(context_var_name='plugin_context')
def get_bound_port_context(self, plugin_context, port_id, host=None,
cached_networks=None):
with db_api.context_manager.reader.using(plugin_context) as session:
# NOTE(ihrachys) use writer manager to be able to update mtu when
# fetching network
# TODO(ihrachys) remove in Queens+ when mtu is not nullable
with db_api.context_manager.writer.using(plugin_context) as session:
try:
port_db = (session.query(models_v2.Port).
enable_eagerloads(False).
@ -1566,7 +1621,10 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
@db_api.retry_if_session_inactive(context_var_name='plugin_context')
def get_bound_ports_contexts(self, plugin_context, dev_ids, host=None):
result = {}
with db_api.context_manager.reader.using(plugin_context):
# NOTE(ihrachys) use writer manager to be able to update mtu when
# fetching network
# TODO(ihrachys) remove in Queens+ when mtu is not nullable
with db_api.context_manager.writer.using(plugin_context):
dev_to_full_pids = db.partial_port_ids_to_full_ids(
plugin_context, dev_ids)
# get all port objects for IDs
@ -1816,6 +1874,12 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
elif event == events.PRECOMMIT_DELETE:
self.type_manager.release_network_segment(context, segment)
# change in segments could affect resulting network mtu, so let's
# recalculate it
network_db = self._get_network(context, network_id)
network_db.mtu = self._get_network_mtu(network_db)
network_db.save(session=context.session)
try:
self._notify_mechanism_driver_for_segment_change(
event, context, network_id)

View File

@ -21,6 +21,7 @@ NETWORK_API_EXTENSIONS+=",l3_agent_scheduler"
NETWORK_API_EXTENSIONS+=",metering"
NETWORK_API_EXTENSIONS+=",multi-provider"
NETWORK_API_EXTENSIONS+=",net-mtu"
NETWORK_API_EXTENSIONS+=",net-mtu-writable"
NETWORK_API_EXTENSIONS+=",network-ip-availability"
NETWORK_API_EXTENSIONS+=",network_availability_zone"
NETWORK_API_EXTENSIONS+=",pagination"

View File

@ -15,8 +15,10 @@
from tempest.lib import decorators
from tempest import test
import testtools
from neutron.tests.tempest.api import base
from neutron.tests.tempest import config
class NetworksTestJSON(base.BaseNetworkTest):
@ -129,6 +131,35 @@ class NetworksTestJSON(base.BaseNetworkTest):
_check_list_networks_fields(['project_id', 'tenant_id'], True, True)
# TODO(ihrachys): check that bad mtu is not allowed; current API extension
# definition doesn't enforce values
# TODO(ihrachys): check that new segment reservation updates mtu, once
# https://review.openstack.org/#/c/353115/ is merged
class NetworksMtuTestJSON(base.BaseNetworkTest):
required_extensions = ['net-mtu', 'net-mtu-writable']
@decorators.idempotent_id('c79dbf94-ee26-420f-a56f-382aaccb1a41')
def test_create_network_custom_mtu(self):
# 68 should be supported by all implementations, as per api-ref
network = self.create_network(mtu=68)
body = self.client.show_network(network['id'])['network']
self.assertEqual(68, body['mtu'])
@decorators.idempotent_id('2d35d49d-9d16-465c-92c7-4768eb717688')
@testtools.skipUnless(config.CONF.network_feature_enabled.ipv6,
'IPv6 is not enabled')
def test_update_network_custom_mtu(self):
# 68 should be supported by all implementations, as per api-ref
network = self.create_network(mtu=68)
body = self.client.show_network(network['id'])['network']
self.assertEqual(68, body['mtu'])
# 1280 should be supported by all ipv6 compliant implementations
self.client.update_network(network['id'], mtu=1280)
body = self.client.show_network(network['id'])['network']
self.assertEqual(1280, body['mtu'])
class NetworksSearchCriteriaTest(base.BaseSearchCriteriaTest):
resource = 'network'

View File

@ -6150,6 +6150,7 @@ class DbModelMixin(object):
"admin_state_up=True, "
"vlan_transparent=None, "
"availability_zone_hints=None, "
"mtu=None, "
"standard_attr_id=None}>")
final_exp = exp_start_with + exp_middle + exp_end_with
self.assertEqual(final_exp, actual_repr_output)

View File

@ -1367,8 +1367,8 @@ class Test_GetNetworkMtu(Ml2PluginV2TestCase):
plugin.type_manager.drivers['driver1'].obj = mock_type_driver
net = {
'name': 'net1',
pnet.NETWORK_TYPE: 'driver1',
pnet.PHYSICAL_NETWORK: 'physnet1',
'network_type': 'driver1',
'physical_network': 'physnet1',
}
plugin._get_network_mtu(net)
mock_type_driver.get_mtu.assert_called_once_with('physnet1')
@ -1380,6 +1380,12 @@ class Test_GetNetworkMtu(Ml2PluginV2TestCase):
def get_mtu(self, physical_network=None):
return mtu
def validate_provider_segment(self, segment):
pass
def is_partial_segment(self, segment):
return False
driver_mock = mock.Mock()
driver_mock.obj = FakeDriver()
plugin.type_manager.drivers[driver] = driver_mock
@ -1392,8 +1398,8 @@ class Test_GetNetworkMtu(Ml2PluginV2TestCase):
'name': 'net1',
mpnet.SEGMENTS: [
{
pnet.NETWORK_TYPE: 'driver1',
pnet.PHYSICAL_NETWORK: 'physnet1'
'network_type': 'driver1',
'physical_network': 'physnet1'
},
]
}
@ -1408,12 +1414,12 @@ class Test_GetNetworkMtu(Ml2PluginV2TestCase):
'name': 'net1',
mpnet.SEGMENTS: [
{
pnet.NETWORK_TYPE: 'driver1',
pnet.PHYSICAL_NETWORK: 'physnet1'
'network_type': 'driver1',
'physical_network': 'physnet1'
},
{
pnet.NETWORK_TYPE: 'driver2',
pnet.PHYSICAL_NETWORK: 'physnet2'
'network_type': 'driver2',
'physical_network': 'physnet2'
},
]
}
@ -1425,8 +1431,8 @@ class Test_GetNetworkMtu(Ml2PluginV2TestCase):
net = {
'name': 'net1',
pnet.NETWORK_TYPE: 'driver1',
pnet.PHYSICAL_NETWORK: 'physnet1',
'network_type': 'driver1',
'physical_network': 'physnet1',
}
self.assertEqual(1400, plugin._get_network_mtu(net))
@ -1436,10 +1442,10 @@ class Test_GetNetworkMtu(Ml2PluginV2TestCase):
net = {
'name': 'net1',
pnet.NETWORK_TYPE: 'driver1',
pnet.PHYSICAL_NETWORK: 'physnet1',
'network_type': 'driver1',
'physical_network': 'physnet1',
}
self.assertEqual(0, plugin._get_network_mtu(net))
self.assertEqual(1500, plugin._get_network_mtu(net))
def test_unknown_segment_type_ignored(self):
plugin = directory.get_plugin()
@ -1450,12 +1456,12 @@ class Test_GetNetworkMtu(Ml2PluginV2TestCase):
'name': 'net1',
mpnet.SEGMENTS: [
{
pnet.NETWORK_TYPE: 'driver1',
pnet.PHYSICAL_NETWORK: 'physnet1'
'network_type': 'driver1',
'physical_network': 'physnet1'
},
{
pnet.NETWORK_TYPE: 'driver2',
pnet.PHYSICAL_NETWORK: 'physnet2'
'network_type': 'driver2',
'physical_network': 'physnet2'
},
]
}

View File

@ -0,0 +1,17 @@
---
features:
- |
The new ``net-mtu-writable`` extension API definition has been added. The
new extension indicates that the network ``mtu`` attribute is writeable.
Plugins supporting the new extension are expected to also support
``net-mtu``. The first plugin that gets support for the new extension is
``ml2``.
other:
- |
Changing MTU configuration options (``global_physnet_mtu``,
``physical_network_mtus``, and ``path_mtu``) and restarting
``neutron-serer`` no longer affects existing networks' MTUs. Nevertheless,
new networks will use new option values for MTU calculation. To reflect
configuration changes for existing networks, one may use the new
``net-mtu-writable`` API extension to update ``mtu`` attribute for those
networks.