Shared VxLAN (Part1: shadow agent)

1. What is the problem?
VLAN network has some restrictions that VxLAN network doesn't have.
For more flexible networking deployment, we consider supporting
cross-pod VxLAN network.

2. What is the solution to the problem?
We are going to use shadow agent/port mechanism to synchronize VTEP
information and make cross-pod VxLAN networking available, as discussed
in the specification document[1].

3. What the features need to be implemented to the Tricircle
to realize the solution?
This is the first patch for cross-pod VxLAN networking support, which
introduces the following changes:

(1) A new type driver for VxLAN network is added
(2) During processing update request from nova, local plugin populates
    agent info in the update body and sends update request to central
    neutron
(3) Central neutron extracts agent info from request body and registers
    shadow agent in Tricircle database
(4) During processing create request, if agent info is set in the
    binding:profile in the create body, local plugin creates or updates
    shadow agent before invoking real core plugin
(5) During processing update request, if "force_up" is set in the
    binding:profile in the update body, local plugin updates the port
    status to active to trigger l2 population

[1] https://review.openstack.org/#/c/429155/

Change-Id: I2e2a651887320e1345f6904393422c5a9a3d0827
This commit is contained in:
zhiyuan_cai 2016-12-24 11:21:44 +08:00
parent 12ff14b9c1
commit bb73104ca2
15 changed files with 652 additions and 108 deletions

View File

@ -78,6 +78,7 @@ function init_local_neutron_variables {
export Q_USE_PROVIDERNET_FOR_PUBLIC=True
Q_ML2_PLUGIN_VLAN_TYPE_OPTIONS=${Q_ML2_PLUGIN_VLAN_TYPE_OPTIONS:-}
Q_ML2_PLUGIN_VXLAN_TYPE_OPTIONS=${Q_ML2_PLUGIN_VXLAN_TYPE_OPTIONS:-}
# if VLAN options were not set in local.conf, use default VLAN bridge
# and VLAN options
if [ "$Q_ML2_PLUGIN_VLAN_TYPE_OPTIONS" == "" ]; then
@ -88,6 +89,7 @@ function init_local_neutron_variables {
local ext_option="extern:$TRICIRCLE_DEFAULT_EXT_RANGE"
local vlan_ranges=(network_vlan_ranges=$vlan_option,$ext_option)
Q_ML2_PLUGIN_VLAN_TYPE_OPTIONS=$vlan_ranges
Q_ML2_PLUGIN_VXLAN_TYPE_OPTIONS="vni_ranges=$TRICIRCLE_DEFAULT_VXLAN_RANGE"
local vlan_mapping="bridge:$TRICIRCLE_DEFAULT_VLAN_BRIDGE"
local ext_mapping="extern:$TRICIRCLE_DEFAULT_EXT_BRIDGE"
@ -167,13 +169,22 @@ function start_central_neutron_server {
iniset $NEUTRON_CONF.$server_index client auto_refresh_endpoint True
iniset $NEUTRON_CONF.$server_index client top_region_name $CENTRAL_REGION_NAME
local type_drivers=local
local tenant_network_types=local
if [ "$Q_ML2_PLUGIN_VLAN_TYPE_OPTIONS" != "" ]; then
iniset $NEUTRON_CONF.$server_index tricircle type_drivers local,vlan
iniset $NEUTRON_CONF.$server_index tricircle tenant_network_types local,vlan
type_drivers+=,vlan
tenant_network_types+=,vlan
iniset $NEUTRON_CONF.$server_index tricircle network_vlan_ranges `echo $Q_ML2_PLUGIN_VLAN_TYPE_OPTIONS | awk -F= '{print $2}'`
iniset $NEUTRON_CONF.$server_index tricircle bridge_network_type vlan
iniset $NEUTRON_CONF.$server_index tricircle enable_api_gateway False
fi
if [ "$Q_ML2_PLUGIN_VXLAN_TYPE_OPTIONS" != "" ]; then
type_drivers+=,vxlan
tenant_network_types+=,vxlan
iniset $NEUTRON_CONF.$server_index tricircle vni_ranges `echo $Q_ML2_PLUGIN_VXLAN_TYPE_OPTIONS | awk -F= '{print $2}'`
fi
iniset $NEUTRON_CONF.$server_index tricircle type_drivers $type_drivers
iniset $NEUTRON_CONF.$server_index tricircle tenant_network_types $tenant_network_types
iniset $NEUTRON_CONF.$server_index tricircle enable_api_gateway False
recreate_database $Q_DB_NAME$server_index
$NEUTRON_BIN_DIR/neutron-db-manage --config-file $NEUTRON_CONF.$server_index --config-file /$Q_PLUGIN_CONF_FILE upgrade head

View File

@ -14,6 +14,7 @@ TRICIRCLE_DEFAULT_VLAN_RANGE=${TRICIRCLE_DEFAULT_VLAN_RANGE:-101:150}
TRICIRCLE_DEFAULT_EXT_BRIDGE=${TRICIRCLE_DEFAULT_EXT_BRIDGE:-br-ext}
TRICIRCLE_DEFAULT_EXT_RANGE=${TRICIRCLE_DEFAULT_EXT_RANGE:-151:200}
TRICIRCLE_ADD_DEFAULT_BRIDGES=${TRICIRCLE_ADD_DEFAULT_BRIDGES:-False}
TRICIRCLE_DEFAULT_VXLAN_RANGE=${TRICIRCLE_DEFAULT_VXLAN_RANGE:-1001:2000}
TRICIRCLE_CONF_DIR=${TRICIRCLE_CONF_DIR:-/etc/tricircle}
TRICIRCLE_STATE_PATH=${TRICIRCLE_STATE_PATH:-/var/lib/tricircle}

View File

@ -43,16 +43,16 @@ Central Plugin.
- (String) keystone authorization url, for example, http://$service_host:5000/v3
* - ``auto_refresh_endpoint`` = ``True``
- (Boolean) if set to True, endpoint will be automatically refreshed if timeout accessing endpoint.
* - ``ew_bridge_cidr`` = ``100.0.0.0/9``
- (String) cidr pool of the east-west bridge network, for example, 100.0.0.0/9
* - ``bridge_cidr`` = ``100.0.0.0/9``
- (String) cidr pool of the bridge network, for example, 100.0.0.0/9
* - ``identity_url`` = ``http://127.0.0.1:35357/v3``
- (String) keystone service url, for example, http://$service_host:35357/v3
* - ``neutron_timeout`` = ``60``
- (Integer) timeout for neutron client in seconds.
* - ``ns_bridge_cidr`` = ``100.128.0.0/9``
- (String) cidr pool of the north-south bridge network, for example, 100.128.0.0/9
* - ``top_region_name`` = ``None``
- (String) region name of Central Neutron in which client needs to access, for example, CentralRegion.
* - ``cross_pod_vxlan_mode`` = ``p2p``
- (String) Cross-pod VxLAN networking support mode, possible choices are p2p l2gw and noop

View File

@ -61,3 +61,4 @@ tempest.test_plugins =
tricircle.network.type_drivers =
local = tricircle.network.drivers.type_local:LocalTypeDriver
vlan = tricircle.network.drivers.type_vlan:VLANTypeDriver
vxlan = tricircle.network.drivers.type_vxlan:VxLANTypeDriver

View File

@ -67,7 +67,10 @@ client_opts = [
' auto_refresh_endpoint set to True'),
cfg.StrOpt('bridge_cidr',
default='100.0.0.0/9',
help='cidr pool of the bridge network')
help='cidr pool of the bridge network'),
cfg.StrOpt('cross_pod_vxlan_mode', default='p2p',
choices=['p2p', 'l2gw', 'noop'],
help='Cross-pod VxLAN networking support mode')
]
client_opt_group = cfg.OptGroup('client')
cfg.CONF.register_group(client_opt_group)

View File

@ -75,6 +75,10 @@ SP_EXTRA_ID = '00000000-0000-0000-0000-000000000000'
TOP = 'top'
POD_NOT_SPECIFIED = 'not_specified_pod'
PROFILE_REGION = 'region'
PROFILE_HOST = 'host'
PROFILE_AGENT_TYPE = 'type'
PROFILE_TUNNEL_IP = 'tunnel_ip'
PROFILE_FORCE_UP = 'force_up'
# job type
JT_ROUTER = 'router'
@ -86,3 +90,9 @@ JT_SUBNET_UPDATE = 'subnet_update'
# network type
NT_LOCAL = 'local'
NT_VLAN = 'vlan'
NT_VxLAN = 'vxlan'
# cross-pod VxLAN networking support mode
NM_P2P = 'p2p'
NM_L2GW = 'l2gw'
NM_NOOP = 'noop'

View File

@ -453,6 +453,30 @@ def finish_job(context, job_id, successful, timestamp):
synchronize_session=False)
def ensure_agent_exists(context, pod_id, host, _type, tunnel_ip):
try:
context.session.begin()
agents = core.query_resource(
context, models.ShadowAgent,
[{'key': 'pod_id', 'comparator': 'eq', 'value': pod_id},
{'key': 'host', 'comparator': 'eq', 'value': host},
{'key': 'type', 'comparator': 'eq', 'value': _type}], [])
if agents:
return
core.create_resource(context, models.ShadowAgent,
{'id': uuidutils.generate_uuid(),
'pod_id': pod_id,
'host': host,
'type': _type,
'tunnel_ip': tunnel_ip})
context.session.commit()
except db_exc.DBDuplicateEntry:
# agent has already been created
context.session.rollback()
finally:
context.session.close()
def _is_user_context(context):
"""Indicates if the request context is a normal user."""
if not context:

View File

@ -0,0 +1,48 @@
# Copyright 2017 Huawei Technologies Co., Ltd.
# 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 migrate
import sqlalchemy as sql
def upgrade(migrate_engine):
meta = sql.MetaData()
meta.bind = migrate_engine
shadow_agents = sql.Table(
'shadow_agents', meta,
sql.Column('id', sql.String(length=36), primary_key=True),
sql.Column('pod_id', sql.String(length=64), nullable=False),
sql.Column('host', sql.String(length=255), nullable=False),
sql.Column('type', sql.String(length=36), nullable=False),
sql.Column('tunnel_ip', sql.String(length=48), nullable=False),
migrate.UniqueConstraint(
'pod_id', 'host', 'type',
name='pod_id0host0type'),
mysql_engine='InnoDB',
mysql_charset='utf8')
shadow_agents.create()
pods = sql.Table('pods', meta, autoload=True)
fkey = {'columns': [shadow_agents.c.pod_id],
'references': [pods.c.pod_id]}
migrate.ForeignKeyConstraint(columns=fkey['columns'],
refcolumns=fkey['references'],
name=fkey.get('name')).create()
def downgrade(migrate_engine):
raise NotImplementedError('downgrade not support')

View File

@ -112,3 +112,23 @@ class AsyncJobLog(core.ModelBase, core.DictBase):
timestamp = sql.Column('timestamp', sql.TIMESTAMP,
server_default=sql.text('CURRENT_TIMESTAMP'),
index=True)
class ShadowAgent(core.ModelBase, core.DictBase):
__tablename__ = 'shadow_agents'
__table_args__ = (
schema.UniqueConstraint(
'pod_id', 'host', 'type',
name='pod_id0host0type'),
)
attributes = ['id', 'pod_id', 'host', 'type', 'tunnel_ip']
id = sql.Column('id', sql.String(length=36), primary_key=True)
pod_id = sql.Column('pod_id', sql.String(length=64),
sql.ForeignKey('pods.pod_id'),
nullable=False)
host = sql.Column('host', sql.String(length=255), nullable=False)
type = sql.Column('type', sql.String(length=36), nullable=False)
# considering IPv6 address, set the length to 48
tunnel_ip = sql.Column('tunnel_ip', sql.String(length=48), nullable=False)

View File

@ -85,6 +85,11 @@ tricircle_opts = [
'usable for VLAN provider and tenant networks, as '
'well as ranges of VLAN tags on each available for '
'allocation to tenant networks.')),
cfg.ListOpt('vni_ranges',
default=[],
help=_('Comma-separated list of <vni_min>:<vni_max> tuples '
'enumerating ranges of VXLAN VNI IDs that are '
'available for tenant network allocation.')),
cfg.StrOpt('bridge_network_type',
default='',
help=_('Type of l3 bridge network, this type should be enabled '
@ -603,12 +608,21 @@ class TricirclePlugin(db_base_plugin_v2.NeutronDbPluginV2,
# because its device_id is not empty
if t_constants.PROFILE_REGION in port['port'].get(
'binding:profile', {}):
# this update request comes from local Neutron
res = super(TricirclePlugin, self).update_port(context, port_id,
port)
region_name = port['port']['binding:profile'][
t_constants.PROFILE_REGION]
profile_dict = port['port']['binding:profile']
region_name = profile_dict[t_constants.PROFILE_REGION]
t_ctx = t_context.get_context_from_neutron_context(context)
pod = db_api.get_pod_by_name(t_ctx, region_name)
net = self.get_network(context, res['network_id'])
if net[provider_net.NETWORK_TYPE] == t_constants.NT_VxLAN:
# if a local type network happens to be a vxlan network, local
# plugin will still send agent info, so we double check here
self.helper.create_shadow_agent_if_needed(t_ctx,
profile_dict, pod)
entries = [(ip['subnet_id'],
t_constants.RT_SUBNET) for ip in res['fixed_ips']]
entries.append((res['network_id'], t_constants.RT_NETWORK))

View File

@ -0,0 +1,57 @@
# Copyright 2017 Huawei Technologies Co., Ltd.
# 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 oslo_config import cfg
from oslo_log import log
from neutron.plugins.ml2 import driver_api
from neutron.plugins.ml2.drivers import type_vxlan
from neutron_lib import exceptions as n_exc
from tricircle.common import constants
from tricircle.common.i18n import _LE
LOG = log.getLogger(__name__)
class VxLANTypeDriver(type_vxlan.VxlanTypeDriver):
def __init__(self):
super(VxLANTypeDriver, self).__init__()
def get_type(self):
return constants.NT_VxLAN
def initialize(self):
try:
self._initialize(cfg.CONF.tricircle.vni_ranges)
except n_exc.NetworkTunnelRangeError:
LOG.exception(_LE("Failed to parse vni_ranges. "
"Service terminated!"))
raise SystemExit()
def reserve_provider_segment(self, context, segment):
res = super(VxLANTypeDriver,
self).reserve_provider_segment(context, segment)
res[driver_api.NETWORK_TYPE] = constants.NT_VxLAN
return res
def allocate_tenant_segment(self, context):
res = super(VxLANTypeDriver,
self).allocate_tenant_segment(context)
res[driver_api.NETWORK_TYPE] = constants.NT_VxLAN
return res
def get_mtu(self, physical_network=None):
pass

View File

@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import copy
import netaddr
import six
@ -35,6 +36,37 @@ AZ_HINTS = 'availability_zone_hints'
EXTERNAL = 'router:external' # neutron.extensions.external_net.EXTERNAL
TYPE_VLAN = 'vlan' # neutron.plugins.common.constants.TYPE_VLAN
OVS_AGENT_DATA_TEMPLATE = {
'agent_type': None,
'binary': 'neutron-openvswitch-agent',
'host': None,
'topic': constants.L2_AGENT_TOPIC,
'configurations': {
'ovs_hybrid_plug': False,
'in_distributed_mode': False,
'datapath_type': 'system',
'arp_responder_enabled': False,
'tunneling_ip': None,
'vhostuser_socket_dir': '/var/run/openvswitch',
'devices': 0,
'ovs_capabilities': {
'datapath_types': ['netdev', 'system'],
'iface_types': ['geneve', 'gre', 'internal', 'ipsec_gre', 'lisp',
'patch', 'stt', 'system', 'tap', 'vxlan']},
'log_agent_heartbeats': False,
'l2_population': True,
'tunnel_types': ['vxlan'],
'extensions': [],
'enable_distributed_routing': False,
'bridge_mappings': {}}}
AGENT_DATA_TEMPLATE_MAP = {
constants.AGENT_TYPE_OVS: OVS_AGENT_DATA_TEMPLATE}
TUNNEL_IP_HANDLE_MAP = {
constants.AGENT_TYPE_OVS: lambda agent: agent[
'configurations']['tunneling_ip']}
class NetworkHelper(object):
def __init__(self, call_obj=None):
@ -668,3 +700,38 @@ class NetworkHelper(object):
return False
router_az_hint = router_az_hints[0]
return bool(db_api.get_pod_by_name(t_ctx, router_az_hint))
@staticmethod
def construct_agent_data(agent_type, host, tunnel_ip):
if agent_type not in AGENT_DATA_TEMPLATE_MAP:
return {}
data = copy.copy(AGENT_DATA_TEMPLATE_MAP[agent_type])
data['agent_type'] = agent_type
data['host'] = host
data['configurations']['tunneling_ip'] = tunnel_ip
return data
@staticmethod
def fill_agent_data(agent_type, host, agent, profile, tunnel_ip=None):
_tunnel_ip = None
if tunnel_ip:
# explicitly specified tunnel IP has the highest priority
_tunnel_ip = tunnel_ip
elif agent_type in TUNNEL_IP_HANDLE_MAP:
tunnel_handle = TUNNEL_IP_HANDLE_MAP[agent_type]
_tunnel_ip = tunnel_handle(agent)
if not _tunnel_ip:
return
profile[t_constants.PROFILE_HOST] = host
profile[t_constants.PROFILE_AGENT_TYPE] = agent_type
profile[t_constants.PROFILE_TUNNEL_IP] = _tunnel_ip
@staticmethod
def create_shadow_agent_if_needed(t_ctx, profile, pod):
if t_constants.PROFILE_HOST not in profile:
return
agent_host = profile[t_constants.PROFILE_HOST]
agent_type = profile[t_constants.PROFILE_AGENT_TYPE]
agent_tunnel = profile[t_constants.PROFILE_TUNNEL_IP]
db_api.ensure_agent_exists(t_ctx, pod['pod_id'], agent_host,
agent_type, agent_tunnel)

View File

@ -18,6 +18,8 @@ import six
from oslo_config import cfg
from oslo_log import log
from neutron_lib.api.definitions import portbindings
from neutron_lib.api.definitions import provider_net
import neutron_lib.constants as q_constants
import neutron_lib.exceptions as q_exceptions
@ -40,7 +42,10 @@ from tricircle.network import helper
tricircle_opts = [
cfg.StrOpt('real_core_plugin', help=_('The core plugin the Tricircle '
'local plugin will invoke.')),
cfg.StrOpt('central_neutron_url', help=_('Central Neutron server url'))]
cfg.StrOpt('central_neutron_url', help=_('Central Neutron server url')),
cfg.IPOpt('l2gw_tunnel_ip', help=_('Tunnel IP of L2 gateway, need to set '
'when client.cross_pod_vxlan_mode is '
'set to l2gw'))]
tricircle_opt_group = cfg.OptGroup('tricircle')
cfg.CONF.register_group(tricircle_opt_group)
@ -49,6 +54,9 @@ cfg.CONF.register_opts(tricircle_opts, group=tricircle_opt_group)
LOG = log.getLogger(__name__)
VIF_AGENT_TYPE_MAP = {
portbindings.VIF_TYPE_OVS: q_constants.AGENT_TYPE_OVS}
class TricirclePlugin(plugin.Ml2Plugin):
def __init__(self):
@ -77,10 +85,11 @@ class TricirclePlugin(plugin.Ml2Plugin):
@staticmethod
def _adapt_network_body(network):
network_type = network.get('provider:network_type')
network_type = network.get(provider_net.NETWORK_TYPE)
if network_type == t_constants.NT_LOCAL:
for key in ['provider:network_type', 'provider:physical_network',
'provider:segmentation_id']:
for key in (provider_net.NETWORK_TYPE,
provider_net.PHYSICAL_NETWORK,
provider_net.SEGMENTATION_ID):
network.pop(key, None)
# remove az_hint from network
@ -452,27 +461,114 @@ class TricirclePlugin(plugin.Ml2Plugin):
# get_subnet will create bottom subnet if it doesn't exist
self.get_subnet(context, subnet_id)
for field in ('name', 'device_id'):
for field in ('name', 'device_id', 'binding:host_id'):
if port_body.get(field):
t_port[field] = port_body[field]
self._handle_security_group(t_ctx, context, t_port)
self._create_shadow_agent(context, port_body)
b_port = self.core_plugin.create_port(context, {'port': t_port})
return b_port
def _create_shadow_agent(self, context, port_body):
"""Create shadow agent before creating shadow port
Called inside self.create_port function. Shadow port is created by xjob
daemon. Xjob daemon will insert agent information(agent type, tunnel
ip and host) in the binding profile of the request body. This function
checks if the necessary information is in the request body, if so, it
invokes real core plugin to create or update shadow agent. For other
kinds of port creation requests, this function is called but does not
take effect.
:param context: neutron context
:param port_body: port update body
:return: None
"""
if not utils.is_extension_supported(self.core_plugin, 'agent'):
return
profile_dict = port_body.get(portbindings.PROFILE, {})
if t_constants.PROFILE_TUNNEL_IP not in profile_dict:
return
agent_type = profile_dict[t_constants.PROFILE_AGENT_TYPE]
tunnel_ip = profile_dict[t_constants.PROFILE_TUNNEL_IP]
agent_host = port_body[portbindings.HOST_ID]
agent_state = helper.NetworkHelper.construct_agent_data(
agent_type, agent_host, tunnel_ip)
self.core_plugin.create_or_update_agent(context, agent_state)
def _fill_agent_info_in_profile(self, context, port_id, host,
profile_dict):
"""Fill agent information in the binding profile
Called inside self.update_port function. When local plugin handles
port update request, it checks if host is in the body, if so, local
plugin will send a port update request to central Neutron to tell
central plugin that the port has been bound to a host. The information
of the agent in the host is inserted in the update body by calling this
function. So after central Neutron receives the request, it can save
the agent information in the Tricircle shadow agent table.
:param context: neutron object
:param port_id: port uuid
:param host: host the port is bound to
:param profile_dict: binding profile dict in the port update body
:return: None
"""
if not utils.is_extension_supported(self.core_plugin, 'agent'):
return
if cfg.CONF.client.cross_pod_vxlan_mode == t_constants.NM_NOOP:
return
port = self.core_plugin.get_port(context, port_id)
net = self.core_plugin.get_network(context, port['network_id'])
if net[provider_net.NETWORK_TYPE] != t_constants.NT_VxLAN:
return
vif_type = port[portbindings.VIF_TYPE]
if vif_type not in VIF_AGENT_TYPE_MAP:
return
agent_type = VIF_AGENT_TYPE_MAP[vif_type]
agents = self.core_plugin.get_agents(
context, filters={'agent_type': [agent_type], 'host': [host]})
if not agents:
return
if cfg.CONF.client.cross_pod_vxlan_mode == t_constants.NM_P2P:
helper.NetworkHelper.fill_agent_data(agent_type, host, agents[0],
profile_dict)
elif cfg.CONF.client.cross_pod_vxlan_mode == t_constants.NM_L2GW:
if not cfg.CONF.tricircle.l2gw_tunnel_ip:
LOG.error(_LE('Cross-pod VxLAN networking mode is set to l2gw '
'but L2 gateway tunnel ip is not configured'))
return
l2gw_tunnel_ip = cfg.CONF.tricircle.l2gw_tunnel_ip
helper.NetworkHelper.fill_agent_data(agent_type, host, agents[0],
profile_dict,
tunnel_ip=l2gw_tunnel_ip)
def update_port(self, context, _id, port):
profile_dict = port['port'].get(portbindings.PROFILE, {})
if profile_dict.pop(t_constants.PROFILE_FORCE_UP, None):
port['port']['status'] = q_constants.PORT_STATUS_ACTIVE
port['port'][
portbindings.VNIC_TYPE] = q_constants.ATTR_NOT_SPECIFIED
b_port = self.core_plugin.update_port(context, _id, port)
if port['port'].get('device_owner', '').startswith('compute') and (
port['port'].get('binding:host_id')):
port['port'].get(portbindings.HOST_ID)):
# we check both "device_owner" and "binding:host_id" to ensure the
# request comes from nova. and ovs agent will not call update_port.
# it updates port status via rpc and direct db operation
region_name = cfg.CONF.nova.region_name
update_dict = {'binding:profile': {
update_dict = {portbindings.PROFILE: {
t_constants.PROFILE_REGION: region_name}}
self._fill_agent_info_in_profile(
context, _id, port['port'][portbindings.HOST_ID],
update_dict[portbindings.PROFILE])
t_ctx = t_context.get_context_from_neutron_context(context)
self.neutron_handle.handle_update(t_ctx, 'port', _id,
{'port': update_dict})
return self.core_plugin.update_port(context, _id, port)
return b_port
def get_port(self, context, _id, fields=None):
try:

View File

@ -33,7 +33,6 @@ import neutron_lib.context as q_context
import neutron_lib.exceptions as q_lib_exc
from neutron_lib.plugins import directory
import neutron.api.v2.attributes as neutron_attributes
import neutron.conf.common as q_config
from neutron.db import _utils
@ -418,6 +417,18 @@ class FakeClient(object):
ret_list.append(res)
return ret_list
def update_resources(self, _type, ctx, _id, body):
if self.region_name == 'top':
res_list = self._res_map[self.region_name][_type + 's']
else:
res_list = self._res_map[self.region_name][_type]
updated = False
for res in res_list:
if res['id'] == _id:
updated = True
res.update(body[_type])
return updated
def delete_resources(self, _type, ctx, _id):
index = -1
if self.region_name == 'top':
@ -448,17 +459,7 @@ class FakeClient(object):
self.delete_resources('network', ctx, net_id)
def update_networks(self, ctx, net_id, network):
net_data = network[neutron_attributes.NETWORK]
if self.region_name == 'pod_1':
bottom_nets = BOTTOM1_NETS
else:
bottom_nets = BOTTOM2_NETS
for net in bottom_nets:
if net['id'] == net_id:
net['description'] = net_data['description']
net['admin_state_up'] = net_data['admin_state_up']
net['shared'] = net_data['shared']
self.update_resources('network', ctx, net_id, network)
def list_subnets(self, ctx, filters=None):
return self.list_resources('subnet', ctx, filters)
@ -472,33 +473,13 @@ class FakeClient(object):
self.delete_resources('subnet', ctx, subnet_id)
def update_ports(self, ctx, port_id, body):
subnet_data = body[neutron_attributes.PORT]
if self.region_name == 'pod_1':
ports = BOTTOM1_PORTS
else:
ports = BOTTOM2_PORTS
for port in ports:
if port['id'] == port_id:
for key in subnet_data:
port[key] = subnet_data[key]
return
self.update_resources('port', ctx, port_id, body)
def update_subnets(self, ctx, subnet_id, body):
subnet_data = body[neutron_attributes.SUBNET]
if self.region_name == 'pod_1':
subnets = BOTTOM1_SUBNETS
else:
subnets = BOTTOM2_SUBNETS
for subnet in subnets:
if subnet['id'] == subnet_id:
for key in subnet_data:
subnet[key] = subnet_data[key]
return
raise ipam_exc.InvalidSubnetRequest(
reason=_("updated subnet id not found"))
updated = self.update_resources('subnet', ctx, subnet_id, body)
if not updated:
raise ipam_exc.InvalidSubnetRequest(
reason=_("updated subnet id not found"))
def create_ports(self, ctx, body):
return self.create_resources('port', ctx, body)
@ -1733,11 +1714,18 @@ class PluginTest(unittest.TestCase,
@staticmethod
def _prepare_port_test(tenant_id, ctx, pod_name, index, t_net_id,
b_net_id, vif_type=portbindings.VIF_TYPE_UNBOUND,
b_net_id, t_subnet_id, b_subnet_id, add_ip=True,
vif_type=portbindings.VIF_TYPE_UNBOUND,
device_onwer='compute:None'):
t_port_id = uuidutils.generate_uuid()
b_port_id = uuidutils.generate_uuid()
if add_ip:
ip_address = ''
for subnet in TOP_SUBNETS:
if subnet['id'] == t_subnet_id:
ip_address = subnet['cidr'].replace('.0/24', '.5')
t_port = {
'id': t_port_id,
'name': 'top_port_%d' % index,
@ -1755,6 +1743,9 @@ class PluginTest(unittest.TestCase,
'binding:host_id': 'zhiyuan-5',
'status': 'ACTIVE'
}
if add_ip:
t_port.update({'fixed_ips': [{'subnet_id': t_subnet_id,
'ip_address': ip_address}]})
TOP_PORTS.append(DotDict(t_port))
b_port = {
@ -1776,6 +1767,9 @@ class PluginTest(unittest.TestCase,
'binding:host_id': 'zhiyuan-5',
'status': 'ACTIVE'
}
if add_ip:
b_port.update({'fixed_ips': [{'subnet_id': b_subnet_id,
'ip_address': ip_address}]})
if pod_name == 'pod_1':
BOTTOM1_PORTS.append(DotDict(b_port))
@ -1794,7 +1788,8 @@ class PluginTest(unittest.TestCase,
@staticmethod
def _prepare_network_test(tenant_id, ctx, region_name, index,
enable_dhcp=True, az_hints=None):
enable_dhcp=True, az_hints=None,
network_type=constants.NT_LOCAL):
t_net_id = b_net_id = uuidutils.generate_uuid()
t_subnet_id = b_subnet_id = uuidutils.generate_uuid()
@ -1807,6 +1802,7 @@ class PluginTest(unittest.TestCase,
'description': 'description',
'admin_state_up': False,
'shared': False,
'provider:network_type': network_type,
'availability_zone_hints': az_hints
}
t_subnet = {
@ -2069,10 +2065,12 @@ class PluginTest(unittest.TestCase,
self._basic_pod_route_setup()
neutron_context = FakeNeutronContext()
t_ctx = context.get_db_context()
t_net_id, t_subnet_id, b_net_id, _ = self._prepare_network_test(
(t_net_id, t_subnet_id,
b_net_id, b_subnet_id) = self._prepare_network_test(
project_id, t_ctx, 'pod_1', 1)
t_port_id, b_port_id = self._prepare_port_test(
project_id, t_ctx, 'pod_1', 1, t_net_id, b_net_id)
project_id, t_ctx, 'pod_1', 1, t_net_id, b_net_id,
t_subnet_id, b_subnet_id)
t_sg_id, _ = self._prepare_sg_test(project_id, t_ctx, 'pod_1')
fake_plugin = FakePlugin()
@ -2147,11 +2145,13 @@ class PluginTest(unittest.TestCase,
self._basic_pod_route_setup()
neutron_context = FakeNeutronContext()
t_ctx = context.get_db_context()
t_net_id, t_subnet_id, b_net_id, _ = self._prepare_network_test(
(t_net_id, t_subnet_id,
b_net_id, b_subnet_id) = self._prepare_network_test(
tenant_id, t_ctx, 'pod_1', 1)
(t_port_id, b_port_id) = self._prepare_port_test(
tenant_id, t_ctx, 'pod_1', 1, t_net_id, b_net_id, vif_type='ovs',
device_onwer='compute:None')
tenant_id, t_ctx, 'pod_1', 1, t_net_id, b_net_id,
t_subnet_id, b_subnet_id,
vif_type='ovs', device_onwer='compute:None')
fake_plugin = FakePlugin()
mock_context.return_value = t_ctx
@ -2175,7 +2175,8 @@ class PluginTest(unittest.TestCase,
t_ctx = context.get_db_context()
neutron_context = FakeNeutronContext()
mock_context.return_value = t_ctx
t_net_id, t_subnet_id, b_net_id, _ = self._prepare_network_test(
(t_net_id, t_subnet_id,
b_net_id, b_subnet_id) = self._prepare_network_test(
tenant_id, t_ctx, 'pod_1', 1)
fake_plugin = FakePlugin()
fake_client = FakeClient('pod_1')
@ -2186,7 +2187,7 @@ class PluginTest(unittest.TestCase,
for port_type in non_vm_port_types:
(t_port_id, b_port_id) = self._prepare_port_test(
tenant_id, t_ctx, 'pod_1', 1, t_net_id, b_net_id,
device_onwer=port_type)
t_subnet_id, b_subnet_id, add_ip=False, device_onwer=port_type)
update_body = {
'port': {'binding:host_id': 'zhiyuan-6'}
}
@ -2200,6 +2201,53 @@ class PluginTest(unittest.TestCase,
bottom_port = fake_client.get_ports(t_ctx, b_port_id)
self.assertEqual(bottom_port['binding:host_id'], 'zhiyuan-5')
@patch.object(directory, 'get_plugin', new=fake_get_plugin)
@patch.object(driver.Pool, 'get_instance', new=fake_get_instance)
@patch.object(_utils, 'filter_non_model_columns',
new=fake_filter_non_model_columns)
@patch.object(context, 'get_context_from_neutron_context')
def test_update_vm_port(self, mock_context):
tenant_id = TEST_TENANT_ID
self._basic_pod_route_setup()
t_ctx = context.get_db_context()
neutron_context = FakeNeutronContext()
mock_context.return_value = t_ctx
(t_net_id, t_subnet_id,
b_net_id, b_subnet_id) = self._prepare_network_test(
tenant_id, t_ctx, 'pod_1', 1, network_type=constants.NT_LOCAL)
fake_plugin = FakePlugin()
(t_port_id, b_port_id) = self._prepare_port_test(
tenant_id, t_ctx, 'pod_1', 1, t_net_id, b_net_id,
t_subnet_id, b_subnet_id)
update_body = {
'port': {'binding:profile': {
'region': 'pod_1',
'host': 'fake_host',
'type': 'Open vSwitch agent',
'tunnel_ip': '192.168.1.101'
}}
}
fake_plugin.update_port(
neutron_context, t_port_id, update_body)
agents = core.query_resource(t_ctx, models.ShadowAgent, [], [])
# we only create shadow agent for vxlan network
self.assertEqual(len(agents), 0)
client = FakeClient()
# in fact provider attribute is not allowed to be updated, but in test
# we just change the network type for convenience
client.update_networks(
t_ctx, t_net_id,
{'network': {'provider:network_type': constants.NT_VxLAN}})
fake_plugin.update_port(
neutron_context, t_port_id, update_body)
agents = core.query_resource(t_ctx, models.ShadowAgent, [], [])
self.assertEqual(len(agents), 1)
self.assertEqual(agents[0]['type'], 'Open vSwitch agent')
self.assertEqual(agents[0]['host'], 'fake_host')
self.assertEqual(agents[0]['tunnel_ip'], '192.168.1.101')
@patch.object(directory, 'get_plugin', new=fake_get_plugin)
@patch.object(driver.Pool, 'get_instance', new=fake_get_instance)
@patch.object(ipam_pluggable_backend.IpamPluggableBackend,

View File

@ -22,12 +22,14 @@ import unittest
from oslo_config import cfg
from oslo_utils import uuidutils
from neutron_lib.api.definitions import portbindings
import neutron_lib.constants as q_constants
import neutron_lib.exceptions as q_exceptions
from tricircle.common import client
from tricircle.common import constants
import tricircle.common.context as t_context
from tricircle.network import helper
import tricircle.network.local_plugin as plugin
@ -39,12 +41,15 @@ BOTTOM_NETS = []
BOTTOM_SUBNETS = []
BOTTOM_PORTS = []
BOTTOM_SGS = []
BOTTOM_AGENTS = []
RES_LIST = [TOP_NETS, TOP_SUBNETS, TOP_PORTS, TOP_SGS,
BOTTOM_NETS, BOTTOM_SUBNETS, BOTTOM_PORTS, BOTTOM_SGS]
BOTTOM_NETS, BOTTOM_SUBNETS, BOTTOM_PORTS, BOTTOM_SGS,
BOTTOM_AGENTS]
RES_MAP = {'network': {True: TOP_NETS, False: BOTTOM_NETS},
'subnet': {True: TOP_SUBNETS, False: BOTTOM_SUBNETS},
'port': {True: TOP_PORTS, False: BOTTOM_PORTS},
'security_group': {True: TOP_SGS, False: BOTTOM_SGS}}
'security_group': {True: TOP_SGS, False: BOTTOM_SGS},
'agent': {True: [], False: BOTTOM_AGENTS}}
def create_resource(_type, is_top, body):
@ -86,6 +91,8 @@ def delete_resource(_type, is_top, body):
class FakeCorePlugin(object):
supported_extension_aliases = ['agent']
def create_network(self, context, network):
create_resource('network', False, network['network'])
return network['network']
@ -129,6 +136,12 @@ class FakeCorePlugin(object):
def get_security_group(self, context, _id, fields=None, tenant_id=None):
return get_resource('security_group', False, _id)
def get_agents(self, context, filters=None, fields=None):
return list_resource('agent', False, filters)
def create_or_update_agent(self, context, agent_state):
pass
class FakeSession(object):
class WithWrapper(object):
@ -309,6 +322,23 @@ class PluginTest(unittest.TestCase):
self.assertEqual('vlan', b_net_type)
self.assertDictEqual(port, b_port)
def _prepare_vm_port(self, t_net, t_subnet, index, t_sgs=[]):
port_id = uuidutils.generate_uuid()
cidr = t_subnet['cidr']
ip_address = '%s.%d' % (cidr[:cidr.rindex('.')], index + 3)
mac_address = 'fa:16:3e:96:41:0%d' % (index + 3)
t_port = {'id': port_id,
'tenant_id': self.tenant_id,
'admin_state_up': True,
'network_id': t_net['id'],
'mac_address': mac_address,
'fixed_ips': [{'subnet_id': t_subnet['id'],
'ip_address': ip_address}],
'binding:profile': {},
'security_groups': t_sgs}
TOP_PORTS.append(t_port)
return t_port
@patch.object(t_context, 'get_context_from_neutron_context', new=mock.Mock)
def test_get_network(self):
t_net, t_subnet, t_port, _ = self._prepare_resource()
@ -369,35 +399,66 @@ class PluginTest(unittest.TestCase):
self.assertRaises(q_exceptions.InvalidIpForNetwork,
self.plugin.create_port, self.context, port_body)
port_id = uuidutils.generate_uuid()
t_port = {'id': port_id,
'tenant_id': self.tenant_id,
'admin_state_up': True,
'network_id': t_net['id'],
'mac_address': 'fa:16:3e:96:41:04',
'fixed_ips': [{'subnet_id': t_subnet['id'],
'ip_address': '10.0.1.4'}],
'binding:profile': {},
'security_groups': [t_sg['id']]}
TOP_PORTS.append(t_port)
t_vm_port = self._prepare_vm_port(t_net, t_subnet, 1, [t_sg['id']])
b_port = self.plugin.create_port(self.context, port_body)
self.assertDictEqual(t_port, b_port)
self.assertDictEqual(t_vm_port, b_port)
@patch.object(FakeCorePlugin, 'create_or_update_agent')
@patch.object(t_context, 'get_context_from_neutron_context', new=mock.Mock)
def test_create_port_with_tunnel_ip(self, mock_agent):
t_net, t_subnet, t_port, t_sg = self._prepare_resource()
# core plugin supports "agent" extension and body contains tunnel ip
port_body = {
'port': {'network_id': t_net['id'],
'fixed_ips': q_constants.ATTR_NOT_SPECIFIED,
'security_groups': [],
portbindings.HOST_ID: 'host1',
portbindings.PROFILE: {
constants.PROFILE_TUNNEL_IP: '192.168.1.101',
constants.PROFILE_AGENT_TYPE: 'Open vSwitch agent'}}
}
self.plugin.create_port(self.context, port_body)
agent_state = copy.copy(helper.OVS_AGENT_DATA_TEMPLATE)
agent_state['agent_type'] = 'Open vSwitch agent'
agent_state['host'] = 'host1'
agent_state['configurations']['tunneling_ip'] = '192.168.1.101'
mock_agent.assert_called_once_with(self.context, agent_state)
# core plugin supports "agent" extension but body doesn't contain
# tunnel ip
port_body = {
'port': {'network_id': t_net['id'],
'fixed_ips': q_constants.ATTR_NOT_SPECIFIED,
'security_groups': []}
}
self.plugin.create_port(self.context, port_body)
# core plugin doesn't support "agent" extension but body contains
# tunnel ip
FakeCorePlugin.supported_extension_aliases = []
port_body = {
'port': {'network_id': t_net['id'],
'fixed_ips': q_constants.ATTR_NOT_SPECIFIED,
'security_groups': [],
portbindings.HOST_ID: 'host1',
portbindings.PROFILE: {
constants.PROFILE_TUNNEL_IP: '192.168.1.101',
constants.PROFILE_AGENT_TYPE: 'Open vSwitch agent'}}
}
self.plugin.create_port(self.context, port_body)
FakeCorePlugin.supported_extension_aliases = ['agent']
# create_or_update_agent is called only when core plugin supports
# "agent" extension and body contains tunnel ip
mock_agent.assert_has_calls([mock.call(self.context, agent_state)])
@patch.object(t_context, 'get_context_from_neutron_context', new=mock.Mock)
def test_get_port(self):
t_net, t_subnet, t_port, _ = self._prepare_resource()
port_id = uuidutils.generate_uuid()
t_port = {'id': port_id,
'tenant_id': self.tenant_id,
'admin_state_up': True,
'network_id': t_net['id'],
'mac_address': 'fa:16:3e:96:41:04',
'fixed_ips': [{'subnet_id': t_subnet['id'],
'ip_address': '10.0.1.4'}],
'binding:profile': {},
'security_groups': []}
TOP_PORTS.append(t_port)
t_port = self.plugin.get_port(self.context, port_id)
t_vm_port = self._prepare_vm_port(t_net, t_subnet, 1)
t_port = self.plugin.get_port(self.context, t_vm_port['id'])
b_port = get_resource('port', False, t_port['id'])
self.assertDictEqual(t_port, b_port)
@ -405,19 +466,9 @@ class PluginTest(unittest.TestCase):
def test_get_ports(self):
t_net, t_subnet, t_port, t_sg = self._prepare_resource()
t_ports = []
for i in (4, 5):
port_id = uuidutils.generate_uuid()
t_port = {'id': port_id,
'tenant_id': self.tenant_id,
'admin_state_up': True,
'network_id': t_net['id'],
'mac_address': 'fa:16:3e:96:41:04',
'fixed_ips': [{'subnet_id': t_subnet['id'],
'ip_address': '10.0.1.%d' % i}],
'binding:profile': {},
'security_groups': [t_sg['id']]}
TOP_PORTS.append(t_port)
t_ports.append(t_port)
for i in (1, 2):
t_vm_port = self._prepare_vm_port(t_net, t_subnet, i, [t_sg['id']])
t_ports.append(t_vm_port)
self.plugin.get_ports(self.context,
{'id': [t_ports[0]['id'], t_ports[1]['id'],
'fake_port_id']})
@ -426,19 +477,112 @@ class PluginTest(unittest.TestCase):
b_port.pop('project_id')
self.assertDictEqual(t_ports[i], b_port)
@patch.object(FakeCorePlugin, 'update_port')
@patch.object(t_context, 'get_context_from_neutron_context')
@patch.object(FakeNeutronHandle, 'handle_update')
def test_update_port(self, mock_update, mock_context):
def test_update_port(self, mock_update, mock_context, mock_core_update):
t_net, t_subnet, _, _ = self._prepare_resource()
b_net = self.plugin.get_network(self.context, t_net['id'])
cfg.CONF.set_override('region_name', 'Pod1', 'nova')
mock_context.return_value = self.context
update_body = {'port': {'device_owner': 'compute:None',
'binding:host_id': 'fake_host'}}
port_id = 'fake_port_id'
host_id = 'fake_host'
fake_port = {
'id': port_id,
'network_id': b_net['id'],
'binding:vif_type': 'fake_vif_type'}
fake_agent = {
'agent_type': 'Open vSwitch agent',
'host': host_id,
'configurations': {
'tunneling_ip': '192.168.1.101'}}
create_resource('port', False, fake_port)
create_resource('agent', False, fake_agent)
update_body = {'port': {'device_owner': 'compute:None',
'binding:host_id': host_id}}
self.plugin.update_port(self.context, port_id, update_body)
mock_update.assert_called_once_with(
# network is not vxlan type
mock_update.assert_called_with(
self.context, 'port', port_id,
{'port': {'binding:profile': {'region': 'Pod1'}}})
# update network type from vlan to vxlan
update_resource('network', False, b_net['id'],
{'provider:network_type': 'vxlan'})
self.plugin.update_port(self.context, port_id, update_body)
# port vif type is not recognized
mock_update.assert_called_with(
self.context, 'port', port_id,
{'port': {'binding:profile': {'region': 'Pod1'}}})
# update network type from fake_vif_type to ovs
update_resource('port', False, port_id,
{'binding:vif_type': 'ovs'})
self.plugin.update_port(self.context, port_id,
{'port': {'device_owner': 'compute:None',
'binding:host_id': 'fake_another_host'}})
# agent in the specific host is not found
mock_update.assert_called_with(
self.context, 'port', port_id,
{'port': {'binding:profile': {'region': 'Pod1'}}})
self.plugin.update_port(self.context, port_id, update_body)
# default p2p mode, update with agent host tunnel ip
mock_update.assert_called_with(
self.context, 'port', port_id,
{'port': {'binding:profile': {'region': 'Pod1',
'tunnel_ip': '192.168.1.101',
'type': 'Open vSwitch agent',
'host': host_id}}})
cfg.CONF.set_override('cross_pod_vxlan_mode', 'l2gw', 'client')
cfg.CONF.set_override('l2gw_tunnel_ip', '192.168.1.105', 'tricircle')
update_body = {'port': {'device_owner': 'compute:None',
'binding:host_id': host_id}}
self.plugin.update_port(self.context, port_id, update_body)
# l2gw mode, update with configured l2 gateway tunnel ip
mock_update.assert_called_with(
self.context, 'port', port_id,
{'port': {'binding:profile': {'region': 'Pod1',
'tunnel_ip': '192.168.1.105',
'type': 'Open vSwitch agent',
'host': host_id}}})
cfg.CONF.set_override('l2gw_tunnel_ip', '', 'tricircle')
cfg.CONF.set_override('cross_pod_vxlan_mode', 'l2gw', 'client')
self.plugin.update_port(self.context, port_id, update_body)
# l2gw mode, but l2 gateway tunnel ip is not configured
mock_update.assert_called_with(
self.context, 'port', port_id,
{'port': {'binding:profile': {'region': 'Pod1'}}})
cfg.CONF.set_override('cross_pod_vxlan_mode', 'noop', 'client')
self.plugin.update_port(self.context, port_id, update_body)
# noop mode
mock_update.assert_called_with(
self.context, 'port', port_id,
{'port': {'binding:profile': {'region': 'Pod1'}}})
FakeCorePlugin.supported_extension_aliases = []
self.plugin.update_port(self.context, port_id, update_body)
# core plugin doesn't support "agent" extension
mock_update.assert_called_with(
self.context, 'port', port_id,
{'port': {'binding:profile': {'region': 'Pod1'}}})
FakeCorePlugin.supported_extension_aliases = ['agent']
self.plugin.update_port(self.context, port_id,
{'port': {portbindings.PROFILE: {
constants.PROFILE_FORCE_UP: True}}})
mock_core_update.assert_called_with(
self.context, port_id,
{'port': {'status': q_constants.PORT_STATUS_ACTIVE,
portbindings.PROFILE: {},
portbindings.VNIC_TYPE: q_constants.ATTR_NOT_SPECIFIED}})
@patch.object(t_context, 'get_context_from_neutron_context')
def test_update_subnet(self, mock_context):
_, t_subnet, t_port, _ = self._prepare_resource(enable_dhcp=False)