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:
parent
12ff14b9c1
commit
bb73104ca2
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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')
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue