NSX|V3: Add support for native DHCP service

This patch allows VMs on Neutron networks to switch from
Neutron DHCP service to NSX native DHCP service.

It also includes admin tool to enable native DHCP on existing
Neutron networks.

Change-Id: I4739443aa743744d80afc2acdd8b11229e0f076c
This commit is contained in:
Shih-Hao Li 2016-06-20 07:16:49 -07:00
parent b2467e9889
commit 5dc1c19037
17 changed files with 1298 additions and 14 deletions

View File

@ -127,6 +127,7 @@ function neutron_plugin_configure_service {
_nsxv3_ini_set insecure $NSX_INSECURE
_nsxv3_ini_set ca_file $NSX_CA_FILE
_nsxv3_ini_set default_bridge_cluster $DEFAULT_BRIDGE_CLUSTER_UUID
_nsxv3_ini_set dhcp_profile_uuid $DHCP_PROFILE_UUID
}
function neutron_plugin_setup_interface_driver {

View File

@ -11,3 +11,4 @@ DEFAULT_OVERLAY_TZ_UUID=<FILL_IN>
EDGE_CLUSTER_UUID=<FILL_IN>
NSX_MANAGER=<FILL_IN>
NSX_CONTROLLERS=<FILL_IN>
DHCP_PROFILE_UUID=<FILL_IN>

View File

@ -347,6 +347,26 @@ nsx_v3_opts = [
help=_("If true, an internal metadata network will be created "
"for a router only when the router is attached to a "
"DHCP-disabled subnet.")),
cfg.BoolOpt('native_dhcp_metadata',
default=False,
help=_("If true, DHCP and metadata proxy services will be "
"provided by NSX backend.")),
cfg.StrOpt('dhcp_profile_uuid',
help=_("This is the UUID of the NSX DHCP Profile that will be "
"used to enable native DHCP service. It needs to be "
"created in NSX before starting Neutron with the NSX "
"plugin.")),
cfg.IntOpt('dhcp_lease_time',
default=86400,
help=_("DHCP default lease time.")),
cfg.StrOpt('dns_domain',
default='openstacklocal',
help=_("Domain to use for building the hostnames.")),
cfg.ListOpt('nameservers',
default=[],
help=_("List of nameservers to configure for the DHCP "
"binding entries. These will be used if there are no "
"nameservers defined on the subnet.")),
cfg.BoolOpt('log_security_groups_blocked_traffic',
default=False,
help=_("(Optional) Indicates whether distributed-firewall "

View File

@ -22,6 +22,7 @@ ADMIN_STATUSES = [ADMIN_STATE_UP, ADMIN_STATE_DOWN]
# Port attachment types
ATTACHMENT_VIF = "VIF"
ATTACHMENT_LR = "LOGICALROUTER"
ATTACHMENT_DHCP = "DHCP_SERVICE"
ATTACHMENT_CIF = "CIF"
CIF_RESOURCE_TYPE = "CifAttachmentContext"
@ -54,3 +55,6 @@ VIF_TYPE_DVS = 'dvs'
# NSXv3 L2 Gateway constants
BRIDGE_ENDPOINT = "BRIDGEENDPOINT"
# NSX service type
SERVICE_DHCP = "dhcp"

View File

@ -114,6 +114,63 @@ def add_neutron_nsx_security_group_mapping(session, neutron_id, nsx_id):
return mapping
def get_nsx_service_binding(session, network_id, service_type):
return session.query(nsx_models.NeutronNsxServiceBinding).filter_by(
network_id=network_id, nsx_service_type=service_type).one()
def add_neutron_nsx_service_binding(session, network_id, port_id,
service_type, service_id):
"""Store enabled NSX services on each Neutron network.
:param session: database session object
:param network_id: identifier of Neutron network enabling the service
:param port_id: identifier of Neutron port providing the service
:param service_type: type of NSX service
:param service_id: identifier of NSX service
"""
with session.begin(subtransactions=True):
binding = nsx_models.NeutronNsxServiceBinding(
network_id=network_id, port_id=port_id,
nsx_service_type=service_type, nsx_service_id=service_id)
session.add(binding)
return binding
def delete_neutron_nsx_service_binding(session, network_id, service_type):
return session.query(nsx_models.NeutronNsxServiceBinding).filter_by(
network_id=network_id, nsx_service_type=service_type).delete()
def get_nsx_dhcp_bindings(session, port_id):
return [binding for binding in session.query(
nsx_models.NeutronNsxDhcpBinding).filter_by(port_id=port_id)]
def add_neutron_nsx_dhcp_binding(session, port_id, subnet_id, ip_address,
service_id, binding_id):
"""Store DHCP binding of each Neutron port.
:param session: database session object
:param port_id: identifier of Neutron port with DHCP binding
:param subnet_id: identifier of Neutron subnet for the port
:param ip_address: IP address for the port in this subnet.
:param service_id: identifier of NSX DHCP service
:param binding_id: identifier of NSX DHCP binding
"""
with session.begin(subtransactions=True):
binding = nsx_models.NeutronNsxDhcpBinding(
port_id=port_id, subnet_id=subnet_id, ip_address=ip_address,
nsx_service_id=service_id, nsx_binding_id=binding_id)
session.add(binding)
return binding
def delete_neutron_nsx_dhcp_binding(session, port_id, binding_id):
return session.query(nsx_models.NeutronNsxDhcpBinding).filter_by(
port_id=port_id, nsx_binding_id=binding_id).delete()
def get_nsx_switch_ids(session, neutron_id):
# This function returns a list of NSX switch identifiers because of
# the possibility of chained logical switches

View File

@ -1 +1 @@
c288bb6a7252
c644ec62c585

View File

@ -0,0 +1,57 @@
# Copyright 2016 VMware, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""NSXv3 add nsx_service_bindings and nsx_dhcp_bindings tables
Revision ID: c644ec62c585
Revises: c288bb6a7252
Create Date: 2016-04-29 23:19:39.523196
"""
# revision identifiers, used by Alembic.
revision = 'c644ec62c585'
down_revision = 'c288bb6a7252'
from alembic import op
import sqlalchemy as sa
from vmware_nsx.common import nsx_constants
nsx_service_type_enum = sa.Enum(
nsx_constants.SERVICE_DHCP,
name='neutron_nsx_service_bindings_service_type')
def upgrade():
op.create_table(
'neutron_nsx_service_bindings',
sa.Column('network_id', sa.String(36), nullable=False),
sa.Column('port_id', sa.String(36), nullable=True),
sa.Column('nsx_service_type', nsx_service_type_enum, nullable=False),
sa.Column('nsx_service_id', sa.String(36), nullable=False),
sa.ForeignKeyConstraint(['network_id'], ['networks.id'],
ondelete='CASCADE'),
sa.PrimaryKeyConstraint('network_id', 'nsx_service_type'))
op.create_table(
'neutron_nsx_dhcp_bindings',
sa.Column('port_id', sa.String(36), nullable=False),
sa.Column('subnet_id', sa.String(36), nullable=False),
sa.Column('ip_address', sa.String(64), nullable=False),
sa.Column('nsx_service_id', sa.String(36), nullable=False),
sa.Column('nsx_binding_id', sa.String(36), nullable=False),
sa.ForeignKeyConstraint(['port_id'], ['ports.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('port_id', 'nsx_binding_id'))

View File

@ -26,6 +26,8 @@ from sqlalchemy import sql
from neutron.db import model_base
from neutron.db import models_v2
from vmware_nsx.common import nsx_constants
class TzNetworkBinding(model_base.BASEV2):
"""Represents a binding of a virtual network with a transport zone.
@ -149,6 +151,32 @@ class NeutronNsxRouterMapping(model_base.BASEV2):
nsx_id = sa.Column(sa.String(36))
class NeutronNsxServiceBinding(model_base.BASEV2):
"""Represents a binding of a Neutron network with enabled NSX services."""
__tablename__ = 'neutron_nsx_service_bindings'
network_id = sa.Column(sa.String(36),
sa.ForeignKey('networks.id', ondelete='CASCADE'),
nullable=False, primary_key=True)
port_id = sa.Column(sa.String(36), nullable=True)
nsx_service_type = sa.Column(
sa.Enum(nsx_constants.SERVICE_DHCP,
name='neutron_nsx_service_bindings_service_type'),
nullable=False, primary_key=True)
nsx_service_id = sa.Column(sa.String(36), nullable=False)
class NeutronNsxDhcpBinding(model_base.BASEV2):
"""Represents a binding of a Neutron port with DHCP address binding."""
__tablename__ = 'neutron_nsx_dhcp_bindings'
port_id = sa.Column(sa.String(36),
sa.ForeignKey('ports.id', ondelete="CASCADE"),
nullable=False, primary_key=True)
subnet_id = sa.Column(sa.String(36), nullable=False)
ip_address = sa.Column(sa.String(64), nullable=False)
nsx_service_id = sa.Column(sa.String(36), nullable=False)
nsx_binding_id = sa.Column(sa.String(36), nullable=False, primary_key=True)
class MultiProviderNetworks(model_base.BASEV2):
"""Networks provisioned through multiprovider extension."""

View File

@ -0,0 +1,62 @@
# Copyright 2016 VMware, Inc.
# All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import netaddr
from neutron_lib.api import validators
from neutron_lib import constants
from oslo_config import cfg
from vmware_nsx.common import utils
def build_dhcp_server_config(network, subnet, port, project_name):
# Prepare the configutation for a new logical DHCP server.
server_ip = "%s/%u" % (port['fixed_ips'][0]['ip_address'],
netaddr.IPNetwork(subnet['cidr']).prefixlen)
dns_servers = subnet['dns_nameservers']
if not dns_servers or not validators.is_attr_set(dns_servers):
dns_servers = cfg.CONF.nsx_v3.nameservers
gateway_ip = subnet['gateway_ip']
if not validators.is_attr_set(gateway_ip):
gateway_ip = None
# The following code is based on _generate_opts_per_subnet() in
# neutron/agent/linux/dhcp.py. It prepares DHCP options for a subnet.
# Add route for directly connected network.
host_routes = [{'network': subnet['cidr'], 'next_hop': '0.0.0.0'}]
# Copy routes from subnet host_routes attribute.
for hr in subnet['host_routes']:
if hr.destination == constants.IPv4_ANY:
if not gateway_ip:
gateway_ip = hr.nexthop
else:
host_routes.append({'network': hr.destination,
'next_hop': hr.nexthop})
# If gateway_ip is defined, add default route via this gateway.
if gateway_ip:
host_routes.append({'network': constants.IPv4_ANY,
'next_hop': gateway_ip})
options = {'option121': {'static_routes': host_routes}}
tags = utils.build_v3_tags_payload(
network, resource_type='os-neutron-net-id', project_name=project_name)
return {'dhcp_profile_id': cfg.CONF.nsx_v3.dhcp_profile_uuid,
'server_ip': server_ip,
'dns_servers': dns_servers,
'domain_name': cfg.CONF.nsx_v3.dns_domain,
'gateway_ip': gateway_ip,
'options': options,
'tags': tags}

View File

@ -425,3 +425,88 @@ class LogicalRouterPort(AbstractRESTResource):
raise nsx_exc.ResourceNotFound(
manager=client._get_nsx_managers_from_conf(),
operation="get router link port")
class DhcpProfile(AbstractRESTResource):
@property
def uri_segment(self):
return 'dhcp/server-profiles'
def create(self, *args, **kwargs):
pass
def update(self, uuid, *args, **kwargs):
pass
class LogicalDhcpServer(AbstractRESTResource):
@property
def uri_segment(self):
return 'dhcp/services'
def _construct_server(self, body, dhcp_profile_id=None, server_ip=None,
dns_servers=None, domain_name=None, gateway_ip=None,
options=None, tags=None):
if dhcp_profile_id:
body['dhcp_profile_id'] = dhcp_profile_id
if server_ip:
body['ipv4_dhcp_server']['dhcp_server_ip'] = server_ip
if dns_servers:
body['ipv4_dhcp_server']['dns_nameservers'] = dns_servers
if domain_name:
body['ipv4_dhcp_server']['domain_name'] = domain_name
if gateway_ip:
body['ipv4_dhcp_server']['gateway_ip'] = gateway_ip
if options:
body['ipv4_dhcp_server']['options'] = options
if tags:
body['tags'] = tags
def create(self, dhcp_profile_id, server_ip, dns_servers=None,
domain_name=None, gateway_ip=None, options=None, tags=None):
body = {'ipv4_dhcp_server': {}}
self._construct_server(body, dhcp_profile_id, server_ip, dns_servers,
domain_name, gateway_ip, options, tags)
return self._client.create(body=body)
@utils.retry_upon_exception_nsxv3(
nsx_exc.StaleRevision,
max_attempts=cfg.CONF.nsx_v3.retries)
def update(self, uuid, dhcp_profile_id=None, server_ip=None,
dns_servers=None, domain_name=None, gateway_ip=None,
options=None, tags=None):
body = self._client.get(uuid)
self._construct_server(body, dhcp_profile_id, server_ip, dns_servers,
domain_name, gateway_ip, options, tags)
return self._client.update(uuid, body=body)
def create_binding(self, server_uuid, mac, ip, hostname=None,
lease_time=None, options=None):
body = {'mac_address': mac, 'ip_address': ip}
if hostname:
body['host_name'] = hostname
if lease_time:
body['lease_time'] = lease_time
if options:
body['options'] = options
url = "%s/bindings" % server_uuid
return self._client.url_post(url, body)
def get_binding(self, server_uuid, binding_uuid):
url = "%s/bindings/%s" % (server_uuid, binding_uuid)
return self._client.url_get(url)
@utils.retry_upon_exception_nsxv3(
nsx_exc.StaleRevision,
max_attempts=cfg.CONF.nsx_v3.retries)
def update_binding(self, server_uuid, binding_uuid, **kwargs):
body = self.get_binding(server_uuid, binding_uuid)
body.update(kwargs)
url = "%s/bindings/%s" % (server_uuid, binding_uuid)
return self._client.url_put(url, body)
def delete_binding(self, server_uuid, binding_uuid):
url = "%s/bindings/%s" % (server_uuid, binding_uuid)
return self._client.url_delete(url)

View File

@ -62,6 +62,7 @@ from neutron_lib.api import validators
from neutron_lib import constants as const
from neutron_lib import exceptions as n_exc
from oslo_config import cfg
from oslo_db import exception as db_exc
from oslo_log import log
from oslo_utils import excutils
from oslo_utils import importutils
@ -83,6 +84,7 @@ from vmware_nsx.nsxlib import v3 as nsxlib
from vmware_nsx.nsxlib.v3 import client as nsx_client
from vmware_nsx.nsxlib.v3 import cluster as nsx_cluster
from vmware_nsx.nsxlib.v3 import dfw_api as firewall
from vmware_nsx.nsxlib.v3 import native_dhcp
from vmware_nsx.nsxlib.v3 import resources as nsx_resources
from vmware_nsx.nsxlib.v3 import router
from vmware_nsx.nsxlib.v3 import security
@ -157,8 +159,7 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
self.cfg_group = 'nsx_v3' # group name for nsx_v3 section in nsx.ini
self.tier0_groups_dict = {}
self._setup_dhcp()
self._start_rpc_notifiers()
self._init_dhcp()
self._port_client = nsx_resources.LogicalPort(self._nsx_client)
self.nsgroup_manager, self.default_section = (
@ -336,6 +337,31 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
with locking.LockManager.get_lock('nsxv3_nsgroup_manager_init'):
return security.init_nsgroup_manager_and_default_section_rules()
def _init_dhcp(self):
if cfg.CONF.nsx_v3.native_dhcp_metadata:
self._init_native_dhcp()
else:
self._setup_dhcp()
self._start_rpc_notifiers()
def _init_native_dhcp(self):
if cfg.CONF.dhcp_agent_notification:
msg = _("Need to disable dhcp_agent_notification when "
"native_dhcp_metadata is enabled")
raise nsx_exc.NsxPluginException(err_msg=msg)
if not cfg.CONF.nsx_v3.dhcp_profile_uuid:
raise cfg.RequiredOptError("dhcp_profile_uuid")
try:
nsx_resources.DhcpProfile(self._nsx_client).get(
cfg.CONF.nsx_v3.dhcp_profile_uuid)
self._dhcp_server = nsx_resources.LogicalDhcpServer(
self._nsx_client)
except nsx_exc.ManagerError:
with excutils.save_and_reraise_exception():
LOG.error(_LE("Unable to retrieve DHCP Profile %s, "
"native DHCP service is not supported"),
cfg.CONF.nsx_v3.dhcp_profile_uuid)
def _setup_rpc(self):
self.endpoints = [dhcp_rpc.DhcpRpcCallback(),
agents_db.AgentExtRpcCallback(),
@ -571,10 +597,9 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
# after the network creation is done
neutron_net_id = created_net['id']
nsx_db.add_neutron_nsx_network_mapping(
context.session,
neutron_net_id,
nsx_net_id)
context.session,
neutron_net_id,
nsx_net_id)
except Exception:
with excutils.save_and_reraise_exception():
# Undo creation on the backend
@ -715,17 +740,224 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
return updated_net
def _has_no_dhcp_enabled_subnet(self, context, network):
# Check if there is no DHCP-enabled subnet in the network.
for subnet in network.subnets:
if subnet.enable_dhcp:
return False
return True
def _has_single_dhcp_enabled_subnet(self, context, network):
# Check if there is only one DHCP-enabled subnet in the network.
count = 0
for subnet in network.subnets:
if subnet.enable_dhcp:
count += 1
if count > 1:
return False
return True if count == 1 else False
def _enable_native_dhcp(self, context, network, subnet):
# Enable native DHCP service on the backend for this network.
# First create a Neutron DHCP port and use its assigned IP
# address as the DHCP server address in an API call to create a
# LogicalDhcpServer on the backend. Then create the corresponding
# logical port for the Neutron port with DHCP attachment as the
# LogicalDhcpServer UUID.
port_data = {
"name": "",
"admin_state_up": True,
"device_id": cfg.CONF.nsx_v3.dhcp_profile_uuid,
"device_owner": const.DEVICE_OWNER_DHCP,
"network_id": network['id'],
"tenant_id": network["tenant_id"],
"mac_address": const.ATTR_NOT_SPECIFIED,
"fixed_ips": [{"subnet_id": subnet['id']}]
}
neutron_port = super(NsxV3Plugin, self).create_port(
context, {'port': port_data})
server_data = native_dhcp.build_dhcp_server_config(
network, subnet, neutron_port, context.tenant_name)
nsx_net_id = self._get_network_nsx_id(context, network['id'])
tags = utils.build_v3_tags_payload(
neutron_port, resource_type='os-neutron-dport-id',
project_name=context.tenant_name)
dhcp_server = None
try:
dhcp_server = self._dhcp_server.create(**server_data)
LOG.info(_LI("Created logical DHCP server %(server)s for network "
"%(network)s"),
{'server': dhcp_server['id'], 'network': network['id']})
nsx_port = self._port_client.create(
nsx_net_id, dhcp_server['id'], tags=tags,
attachment_type=nsx_constants.ATTACHMENT_DHCP)
LOG.info(_LI("Created DHCP logical port %(port)s for "
"network %(network)s"),
{'port': nsx_port['id'], 'network': network['id']})
except nsx_exc.ManagerError:
with excutils.save_and_reraise_exception():
LOG.error(_LE("Unable to create logical DHCP server for "
"network %s"), network['id'])
if dhcp_server:
self._dhcp_server.delete(dhcp_server['id'])
super(NsxV3Plugin, self).delete_port(
context, neutron_port['id'])
try:
# Add neutron_port_id -> nsx_port_id mapping to the DB.
nsx_db.add_neutron_nsx_port_mapping(
context.session, neutron_port['id'], nsx_net_id,
nsx_port['id'])
# Add neutron_net_id -> dhcp_service_id mapping to the DB.
nsx_db.add_neutron_nsx_service_binding(
context.session, network['id'], neutron_port['id'],
nsx_constants.SERVICE_DHCP, dhcp_server['id'])
except db_exc.DBError:
with excutils.save_and_reraise_exception():
LOG.error(_LE("Failed to create mapping for DHCP port %s,"
"deleting port and logical DHCP server"),
neutron_port['id'])
self._dhcp_server.delete(dhcp_server['id'])
self._cleanup_port(context, neutron_port['id'], nsx_port['id'])
def _disable_native_dhcp(self, context, network):
# Disable native DHCP service on the backend for this network.
# First delete the DHCP port in this network. Then delete the
# corresponding LogicalDhcpServer for this network.
dhcp_service = nsx_db.get_nsx_service_binding(
context.session, network['id'], nsx_constants.SERVICE_DHCP)
if not dhcp_service:
LOG.error(_LE("DHCP service not enabled on backend for network "
"%s"), network['id'])
return
if dhcp_service['port_id']:
self.delete_port(context, dhcp_service['port_id'])
else:
LOG.error(_LE("Unable to find DHCP port for network %s"),
network['id'])
try:
self._dhcp_server.delete(dhcp_service['nsx_service_id'])
LOG.info(_LI("Deleted logical DHCP server %(server)s for network "
"%(network)s"),
{'server': dhcp_service['nsx_service_id'],
'network': network['id']})
except nsx_exc.ManagerError:
with excutils.save_and_reraise_exception():
LOG.error(_LE("Unable to delete logical DHCP server %(server)s"
"for network %(network)s"),
{'server': dhcp_service['nsx_service_id'],
'network': network['id']})
try:
# Delete neutron_id -> dhcp_service_id mapping from the DB.
nsx_db.delete_neutron_nsx_service_binding(
context.session, network['id'], nsx_constants.SERVICE_DHCP)
except db_exc.DBError:
with excutils.save_and_reraise_exception():
LOG.error(_LE("Unable to delete DHCP server mapping for "
"network %s"), network['id'])
def create_subnet(self, context, subnet):
# TODO(berlin): public external subnet announcement
return super(NsxV3Plugin, self).create_subnet(context, subnet)
if (cfg.CONF.nsx_v3.native_dhcp_metadata and
subnet['subnet'].get('enable_dhcp', False)):
lock = 'nsxv3_network_' + subnet['subnet']['network_id']
with locking.LockManager.get_lock(lock):
# Check if it is the first DHCP-enabled subnet to create.
network = self._get_network(context,
subnet['subnet']['network_id'])
if self._has_no_dhcp_enabled_subnet(context, network):
created_subnet = super(NsxV3Plugin, self).create_subnet(
context, subnet)
self._enable_native_dhcp(context, network, created_subnet)
else:
msg = _("Can not create more than one DHCP-enabled subnet "
"in network %s") % subnet['subnet']['network_id']
raise n_exc.InvalidInput(error_message=msg)
else:
created_subnet = super(NsxV3Plugin, self).create_subnet(
context, subnet)
return created_subnet
def delete_subnet(self, context, subnet_id):
# TODO(berlin): cancel public external subnet announcement
return super(NsxV3Plugin, self).delete_subnet(context, subnet_id)
if cfg.CONF.nsx_v3.native_dhcp_metadata:
subnet = self.get_subnet(context, subnet_id)
if subnet['enable_dhcp']:
lock = 'nsxv3_network_' + subnet['network_id']
with locking.LockManager.get_lock(lock):
# Check if it is the last DHCP-enabled subnet to delete.
network = self._get_network(context, subnet['network_id'])
if self._has_single_dhcp_enabled_subnet(context, network):
super(NsxV3Plugin, self).delete_subnet(
context, subnet_id)
self._disable_native_dhcp(context, network)
return
super(NsxV3Plugin, self).delete_subnet(context, subnet_id)
def update_subnet(self, context, subnet_id, subnet):
updated_subnet = super(NsxV3Plugin, self).update_subnet(
context, subnet_id, subnet)
enable_native_dhcp = 0 # assume no need to change DHCP
if (cfg.CONF.nsx_v3.native_dhcp_metadata and
'enable_dhcp' in subnet['subnet']):
orig_subnet = self.get_subnet(context, subnet_id)
enable_dhcp = subnet['subnet']['enable_dhcp']
if orig_subnet['enable_dhcp'] != enable_dhcp:
lock = 'nsxv3_network_' + orig_subnet['network_id']
with locking.LockManager.get_lock(lock):
network = self._get_network(
context, orig_subnet['network_id'])
if enable_dhcp:
if self._has_no_dhcp_enabled_subnet(context, network):
updated_subnet = super(
NsxV3Plugin, self).update_subnet(
context, subnet_id, subnet)
enable_native_dhcp = 1 # need to enable DHCP
else:
msg = (_("Multiple DHCP-enabled subnets is not "
"allowed in network %s") %
orig_subnet['network_id'])
raise n_exc.InvalidInput(error_message=msg)
elif self._has_single_dhcp_enabled_subnet(context,
network):
updated_subnet = super(
NsxV3Plugin, self).update_subnet(
context, subnet_id, subnet)
enable_native_dhcp = -1 # need to disable DHCP
# Process native DHCP if needed.
if enable_native_dhcp > 0:
self._enable_native_dhcp(context, network, updated_subnet)
elif enable_native_dhcp < 0:
self._disable_native_dhcp(context, network)
else:
updated_subnet = super(NsxV3Plugin, self).update_subnet(
context, subnet_id, subnet)
if cfg.CONF.nsx_v3.native_dhcp_metadata and enable_native_dhcp >= 0:
# Check if needs to update logical DHCP server for native DHCP.
dns_servers = subnet['subnet'].get('dns_nameservers')
gateway_ip = subnet['subnet'].get('gateway_ip')
kwargs = {}
if (dns_servers and
dns_servers != updated_subnet['dns_nameservers']):
kwargs['dns_servers'] = dns_servers
if gateway_ip and gateway_ip != updated_subnet['gateway_ip']:
kwargs['gateway_ip'] = gateway_ip
if kwargs:
dhcp_service = nsx_db.get_nsx_service_binding(
context.session, orig_subnet['network_id'],
nsx_constants.SERVICE_DHCP)
try:
self._dhcp_server.update(dhcp_service['nsx_service_id'],
**kwargs)
except nsx_exc.ManagerError:
with excutils.save_and_reraise_exception():
LOG.error(_LE("Unable to update logical DHCP server "
"%(server)s for network %(network)s"),
{'server': dhcp_service['nsx_service_id'],
'network': orig_subnet['network_id']})
if cfg.CONF.nsx_v3.metadata_on_demand:
# If enable_dhcp is changed on a subnet attached to a router,
# update internal metadata network accordingly.
@ -994,6 +1226,205 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
LOG.warning(err_msg)
raise n_exc.InvalidInput(error_message=err_msg)
def _filter_ipv4_dhcp_fixed_ips(self, context, fixed_ips):
ips = []
for fixed_ip in fixed_ips:
if netaddr.IPNetwork(fixed_ip['ip_address']).version != 4:
continue
subnet = self.get_subnet(context, fixed_ip['subnet_id'])
if subnet['enable_dhcp']:
ips.append(fixed_ip)
return ips
def _add_dhcp_binding(self, context, port):
if not port["device_owner"].startswith(
const.DEVICE_OWNER_COMPUTE_PREFIX):
return
dhcp_service = nsx_db.get_nsx_service_binding(
context.session, port['network_id'], nsx_constants.SERVICE_DHCP)
if not dhcp_service:
return
for fixed_ip in self._filter_ipv4_dhcp_fixed_ips(
context, port['fixed_ips']):
binding = self._add_dhcp_binding_on_server(
context, dhcp_service['nsx_service_id'], fixed_ip['subnet_id'],
fixed_ip['ip_address'], port)
try:
nsx_db.add_neutron_nsx_dhcp_binding(
context.session, port['id'], fixed_ip['subnet_id'],
fixed_ip['ip_address'], dhcp_service['nsx_service_id'],
binding['id'])
except db_exc.DBError:
LOG.error(_LE("Failed to add mapping of DHCP binding "
"%(binding)s for port %(port)s, deleting"
"DHCP binding on server"),
{'binding': binding['id'], 'port': port['id']})
self._delete_dhcp_binding_on_server(context, binding)
def _add_dhcp_binding_on_server(self, context, dhcp_service_id, subnet_id,
ip, port):
try:
hostname = 'host-%s' % ip.replace('.', '-')
options = {'option121': {'static_routes': [
{'network': '%s' % nsx_rpc.METADATA_DHCP_ROUTE,
'next_hop': ip}]}}
binding = self._dhcp_server.create_binding(
dhcp_service_id, port['mac_address'], ip, hostname,
cfg.CONF.nsx_v3.dhcp_lease_time, options)
LOG.info(_LI("Created static binding (mac: %(mac)s, ip: %(ip)s) "
"for port %(port)s on logical DHCP server "
"%(server)s"),
{'mac': port['mac_address'], 'ip': ip, 'port': port['id'],
'server': dhcp_service_id})
return binding
except nsx_exc.ManagerError:
with excutils.save_and_reraise_exception():
LOG.error(_LE("Unable to create static binding (mac: %(mac)s, "
"ip: %(ip)s) for port %(port)s on logical DHCP "
"server %(server)s"),
{'mac': port['mac_address'], 'ip': ip,
'port': port['id'], 'server': dhcp_service_id})
def _delete_dhcp_binding(self, context, port):
# Do not check device_owner here because Nova may have already
# deleted that before Neutron's port deletion.
bindings = nsx_db.get_nsx_dhcp_bindings(context.session, port['id'])
for binding in bindings:
self._delete_dhcp_binding_on_server(context, binding)
try:
nsx_db.delete_neutron_nsx_dhcp_binding(
context.session, binding['port_id'],
binding['nsx_binding_id'])
except db_exc.DBError:
LOG.error(_LE("Unable to delete mapping of DHCP binding "
"%(binding)s for port %(port)s"),
{'binding': binding['nsx_binding_id'],
'port': binding['port_id']})
def _delete_dhcp_binding_on_server(self, context, binding):
try:
self._dhcp_server.delete_binding(
binding['nsx_service_id'], binding['nsx_binding_id'])
LOG.info(_LI("Deleted static binding for port %(port)s) on "
"logical DHCP server %(server)s"),
{'port': binding['port_id'],
'server': binding['nsx_service_id']})
except nsx_exc.ManagerError:
with excutils.save_and_reraise_exception():
LOG.error(_LE("Unable to delete static binding for port "
"%(port)s) on logical DHCP server %(server)s"),
{'port': binding['port_id'],
'server': binding['nsx_service_id']})
def _find_dhcp_binding(self, subnet_id, ip_address, bindings):
for binding in bindings:
if (subnet_id == binding['subnet_id'] and
ip_address == binding['ip_address']):
return binding
def _update_dhcp_binding(self, context, old_port, new_port):
# First check if any IPv4 address in fixed_ips is changed.
# Then update DHCP server setting or DHCP static binding
# depending on the port type.
# Note that Neutron allows a port with multiple IPs in the
# same subnet. But backend DHCP server may not support that.
# Collect IPv4 DHCP addresses from original and updated fixed_ips
# in the form of [(subnet_id, ip_address)].
old_fixed_ips = set([(fixed_ip['subnet_id'], fixed_ip['ip_address'])
for fixed_ip in self._filter_ipv4_dhcp_fixed_ips(
context, old_port['fixed_ips'])])
new_fixed_ips = set([(fixed_ip['subnet_id'], fixed_ip['ip_address'])
for fixed_ip in self._filter_ipv4_dhcp_fixed_ips(
context, new_port['fixed_ips'])])
# Find out the subnet/IP differences before and after the update.
ips_to_add = list(new_fixed_ips - old_fixed_ips)
ips_to_delete = list(old_fixed_ips - new_fixed_ips)
ip_change = (ips_to_add or ips_to_delete)
if old_port["device_owner"] == const.DEVICE_OWNER_DHCP and ip_change:
# Update backend DHCP server address if the IP address of a DHCP
# port is changed.
if len(new_fixed_ips) != 1:
msg = _("Can only configure one IP address on a DHCP server")
raise n_exc.InvalidInput(error_message=msg)
# Locate the backend DHCP server for this DHCP port.
dhcp_service = nsx_db.get_nsx_service_binding(
context.session, old_port['network_id'],
nsx_constants.SERVICE_DHCP)
new_ip = ips_to_add[0][1]
try:
self._dhcp_server.update(dhcp_service['nsx_service_id'],
server_ip=new_ip)
LOG.info(_LI("Updated IP %(ip)s for logical DHCP server "
"%(server)s"),
{'ip': new_ip,
'server': dhcp_service['nsx_service_id']})
except nsx_exc.ManagerError:
with excutils.save_and_reraise_exception():
LOG.error(_LE("Unable to update IP %(ip)s for logical "
"DHCP server %(server)s"),
{'ip': new_ip,
'server': dhcp_service['nsx_service_id']})
elif old_port["device_owner"].startswith(
const.DEVICE_OWNER_COMPUTE_PREFIX):
# Update static DHCP bindings for a compute port.
bindings = nsx_db.get_nsx_dhcp_bindings(context.session,
old_port['id'])
if ip_change:
# If IP address is changed, update associated DHCP bindings.
# Mac address (if changed) will be updated at the same time.
if ([subnet_id for (subnet_id, ip) in ips_to_add] ==
[subnet_id for (subnet_id, ip) in ips_to_delete]):
# No change on subnet_id, just update corresponding IPs.
for i, (subnet_id, ip) in enumerate(ips_to_delete):
binding = self._find_dhcp_binding(subnet_id, ip,
bindings)
if binding:
self._update_dhcp_binding_on_server(
context, binding, new_port['mac_address'],
ips_to_add[i][1])
else:
for (subnet_id, ip) in ips_to_delete:
binding = self._find_dhcp_binding(subnet_id, ip,
bindings)
if binding:
self._delete_dhcp_binding_on_server(context,
binding)
if ips_to_add:
dhcp_service = nsx_db.get_nsx_service_binding(
context.session, new_port['network_id'],
nsx_constants.SERVICE_DHCP)
for (subnet_id, ip) in ips_to_add:
self._add_dhcp_binding_on_server(
context, dhcp_service['nsx_service_id'],
subnet_id, ip, new_port)
elif old_port['mac_address'] != new_port['mac_address']:
# If only Mac address is changed, update the Mac address in
# all associated DHCP bindings.
for binding in bindings:
self._update_dhcp_binding_on_server(
context, binding, new_port['mac_address'],
binding['ip_address'])
def _update_dhcp_binding_on_server(self, context, binding, mac, ip):
try:
self._dhcp_server.update_binding(
binding['nsx_service_id'], binding['nsx_binding_id'],
mac_address=mac, ip_address=ip)
LOG.info(_LI("Updated static binding (mac: %(mac)s, ip: %(ip)s) "
"for port %(port)s on logical DHCP server "
"%(server)s"),
{'mac': mac, 'ip': ip, 'port': binding['port_id'],
'server': binding['nsx_service_id']})
except nsx_exc.ManagerError:
with excutils.save_and_reraise_exception():
LOG.error(_LE("Unable to update static binding (mac: %(mac)s, "
"ip: %(ip)s) for port %(port)s on logical DHCP "
"server %(server)s"),
{'mac': mac, 'ip': ip, 'port': binding['port_id'],
'server': binding['nsx_service_id']})
def create_port(self, context, port, l2gw_port_check=False):
port_data = port['port']
dhcp_opts = port_data.get(ext_edo.EXTRADHCPOPTS, [])
@ -1062,6 +1493,11 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
# latest db model for the extension functions
port_model = self._get_port(context, port_data['id'])
self._apply_dict_extend_functions('ports', port_data, port_model)
# Add Mac/IP binding to native DHCP server and neutron DB.
if cfg.CONF.nsx_v3.native_dhcp_metadata:
self._add_dhcp_binding(context, port_data)
nsx_rpc.handle_port_metadata_access(self, context, neutron_db)
return port_data
@ -1098,11 +1534,13 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
security.update_lport_with_security_groups(
context, nsx_port_id, port.get(ext_sg.SECURITYGROUPS, []), [])
self.disassociate_floatingips(context, port_id)
# Remove Mac/IP binding from native DHCP server and neutron DB.
if cfg.CONF.nsx_v3.native_dhcp_metadata:
self._delete_dhcp_binding(context, port)
nsx_rpc.handle_port_metadata_access(self, context, port,
is_delete=True)
ret_val = super(NsxV3Plugin, self).delete_port(context, port_id)
return ret_val
super(NsxV3Plugin, self).delete_port(context, port_id)
def _update_port_preprocess_security(
self, context, port, id, updated_port):
@ -1356,6 +1794,10 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
context, id, {'port': original_port},
updated_port, original_port)
# Update DHCP bindings.
if cfg.CONF.nsx_v3.native_dhcp_metadata:
self._update_dhcp_binding(context, original_port, updated_port)
return updated_port
def _extend_get_port_dict_binding(self, context, port):

View File

@ -0,0 +1,109 @@
# Copyright 2016 VMware, Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
from neutron.callbacks import registry
from neutron_lib import constants as const
from oslo_config import cfg
from vmware_nsx._i18n import _LI
from vmware_nsx.common import nsx_constants
from vmware_nsx.dhcp_meta import rpc as nsx_rpc
from vmware_nsx.nsxlib.v3 import client
from vmware_nsx.nsxlib.v3 import cluster
from vmware_nsx.nsxlib.v3 import native_dhcp
from vmware_nsx.nsxlib.v3 import resources
from vmware_nsx.shell.admin.plugins.common import constants
from vmware_nsx.shell.admin.plugins.common import formatters
from vmware_nsx.shell.admin.plugins.common import utils as admin_utils
from vmware_nsx.shell.admin.plugins.nsxv3.resources import utils
import vmware_nsx.shell.nsxadmin as shell
LOG = logging.getLogger(__name__)
neutron_client = utils.NeutronDbClient()
@admin_utils.output_header
def list_dhcp_bindings(resource, event, trigger, **kwargs):
"""List DHCP bindings in Neutron."""
ports = neutron_client.get_ports()
comp_ports = [port for port in ports if port['device_owner'].startswith(
const.DEVICE_OWNER_COMPUTE_PREFIX)]
LOG.info(formatters.output_formatter(constants.DHCP_BINDING, comp_ports,
['id', 'mac_address', 'fixed_ips']))
@admin_utils.output_header
def nsx_update_dhcp_bindings(resource, event, trigger, **kwargs):
"""Resync DHCP bindings for NSXv3 CrossHairs."""
cluster_api = cluster.NSXClusteredAPI()
nsx_client = client.NSX3Client(cluster_api)
client._set_default_api_cluster(cluster_api)
port_resource = resources.LogicalPort(nsx_client)
dhcp_server_resource = resources.LogicalDhcpServer(nsx_client)
port_bindings = {} # lswitch_id: [(mac, ip, prefix_length), ...]
server_bindings = {} # lswitch_id: dhcp_server_id
ports = neutron_client.get_ports()
for port in ports:
network_id = port['network_id']
device_owner = port['device_owner']
if device_owner == const.DEVICE_OWNER_DHCP:
# For each DHCP-enabled network, create a logical DHCP server
# and update the attachment type to DHCP on the corresponding
# logical port of the Neutron DHCP port.
subnet_id = port['fixed_ips'][0]['subnet_id']
subnet = neutron_client.get_subnet(subnet_id)
network = neutron_client.get_network(port['network_id'])
if len(port['fixed_ips']) > 1:
LOG.info(_LI("Network %(network)s has multiple subnets - "
"only enable native DHCP on subnet %(subnet)s"),
{'network': port['network_id'], 'subnet': subnet_id})
server_data = native_dhcp.build_dhcp_server_config(
network, subnet, port, 'NSX Neutron plugin upgrade')
dhcp_server = dhcp_server_resource.create(**server_data)
lswitch_id, lport_id = neutron_client.get_lswitch_and_lport_id(
port['id'])
port_resource.update(lport_id, dhcp_server['id'],
attachment_type=nsx_constants.ATTACHMENT_DHCP)
server_bindings[lswitch_id] = dhcp_server['id']
elif device_owner.startswith(const.DEVICE_OWNER_COMPUTE_PREFIX):
lswitch_id = neutron_client.net_id_to_lswitch_id(network_id)
bindings = port_bindings.get(lswitch_id, [])
bindings.append((port['mac_address'],
port['fixed_ips'][0]['ip_address']))
port_bindings[lswitch_id] = bindings
# Populate mac/IP bindings in each logical DHCP server.
for lswitch_id, bindings in port_bindings.items():
dhcp_server_id = server_bindings[lswitch_id]
for (mac, ip) in bindings:
hostname = 'host-%s' % ip.replace('.', '-')
options = {'option121': {'static_routes': [
{'network': '%s' % nsx_rpc.METADATA_DHCP_ROUTE,
'next_hop': ip}]}}
dhcp_server_resource.create_binding(
dhcp_server_id, mac, ip, hostname,
cfg.CONF.nsx_v3.dhcp_lease_time, options)
registry.subscribe(list_dhcp_bindings,
constants.DHCP_BINDING,
shell.Operations.LIST.value)
registry.subscribe(nsx_update_dhcp_bindings,
constants.DHCP_BINDING,
shell.Operations.NSX_UPDATE.value)

View File

@ -0,0 +1,50 @@
# Copyright 2016 VMware, Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from neutron import context
from neutron.db import db_base_plugin_v2
from vmware_nsx.db import db as nsx_db
class NeutronDbClient(db_base_plugin_v2.NeutronDbPluginV2):
def __init__(self):
super(NeutronDbClient, self).__init__()
self.context = context.get_admin_context()
def get_ports(self, filters=None, fields=None):
return super(NeutronDbClient, self).get_ports(
self.context, filters=filters, fields=fields)
def get_networks(self, filters=None, fields=None):
return super(NeutronDbClient, self).get_networks(
self.context, filters=filters, fields=fields)
def get_network(self, network_id):
return super(NeutronDbClient, self).get_network(
self.context, network_id)
def get_subnet(self, subnet_id):
return super(NeutronDbClient, self).get_subnet(self.context, subnet_id)
def get_lswitch_and_lport_id(self, port_id):
return nsx_db.get_nsx_switch_and_port_id(self.context.session, port_id)
def lswitch_id_to_net_id(self, lswitch_id):
net_ids = nsx_db.get_net_ids(self.context.session, lswitch_id)
return net_ids[0] if net_ids else None
def net_id_to_lswitch_id(self, net_id):
lswitch_ids = nsx_db.get_nsx_switch_ids(self.context.session, net_id)
return lswitch_ids[0] if lswitch_ids else None

View File

@ -89,6 +89,9 @@ nsxv3_resources = {
[Operations.LIST_MISMATCHES.value]),
constants.ROUTERS: Resource(constants.ROUTERS,
[Operations.LIST_MISMATCHES.value]),
constants.DHCP_BINDING: Resource(constants.DHCP_BINDING,
[Operations.LIST.value,
Operations.NSX_UPDATE.value]),
}
# Add supported NSX-V resources in this dictionary

View File

@ -67,6 +67,12 @@ def make_fake_switch(switch_uuid=None, tz_uuid=None, name=FAKE_NAME):
return fake_switch
def make_fake_dhcp_profile():
return {"id": uuidutils.generate_uuid(),
"edge_cluster_id": uuidutils.generate_uuid(),
"edge_cluster_member_indexes": [0, 1]}
def get_resource(resource):
return {'id': resource.split('/')[-1]}

View File

@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import mock
import netaddr
import six
from webob import exc
@ -44,9 +45,14 @@ from oslo_config import cfg
from oslo_serialization import jsonutils
from oslo_utils import uuidutils
from vmware_nsx.common import exceptions as nsx_exc
from vmware_nsx.common import nsx_constants
from vmware_nsx.common import utils
from vmware_nsx.db import db as nsx_db
from vmware_nsx.dhcp_meta import rpc as nsx_rpc
from vmware_nsx.nsxlib.v3 import client as nsx_client
from vmware_nsx.nsxlib.v3 import cluster as nsx_cluster
from vmware_nsx.nsxlib.v3 import resources as nsx_resources
from vmware_nsx.plugins.nsx_v3 import plugin as nsx_plugin
from vmware_nsx.tests import unit as vmware
from vmware_nsx.tests.unit.extensions import test_metadata
@ -761,3 +767,352 @@ class TestNsxV3Utils(NsxV3PluginTestCaseMixin):
{'scope': 'os-api-version',
'tag': version.version_info.release_string()}]
self.assertEqual(sorted(expected), sorted(tags))
class NsxNativeDhcpTestCase(NsxV3PluginTestCaseMixin):
def setUp(self):
super(NsxNativeDhcpTestCase, self).setUp()
self._orig_dhcp_agent_notification = cfg.CONF.dhcp_agent_notification
self._orig_native_dhcp_metadata = cfg.CONF.nsx_v3.native_dhcp_metadata
cfg.CONF.set_override('dhcp_agent_notification', False)
cfg.CONF.set_override('native_dhcp_metadata', True, 'nsx_v3')
self._patcher = mock.patch.object(nsx_resources.DhcpProfile, 'get')
self._patcher.start()
# Need to run _init_native_dhcp() manually because plugin was started
# before setUp() overrides CONF.nsx_v3.native_dhcp_metadata.
self.plugin._init_native_dhcp()
def tearDown(self):
self._patcher.stop()
cfg.CONF.set_override('dhcp_agent_notification',
self._orig_dhcp_agent_notification)
cfg.CONF.set_override('native_dhcp_metadata',
self._orig_native_dhcp_metadata, 'nsx_v3')
super(NsxNativeDhcpTestCase, self).tearDown()
def _verify_dhcp_service(self, network_id, tenant_id, enabled):
# Verify if DHCP service is enabled on a network.
port_res = self._list_ports('json', 200, network_id,
tenant_id=tenant_id,
device_owner=constants.DEVICE_OWNER_DHCP)
port_list = self.deserialize('json', port_res)
self.assertEqual(len(port_list['ports']) == 1, enabled)
def _verify_dhcp_binding(self, subnet, port_data, update_data,
assert_data):
# Verify if DHCP binding is updated.
with mock.patch.object(nsx_resources.LogicalDhcpServer,
'update_binding') as update_dhcp_binding:
device_owner = constants.DEVICE_OWNER_COMPUTE_PREFIX + 'None'
device_id = uuidutils.generate_uuid()
with self.port(subnet=subnet, device_owner=device_owner,
device_id=device_id, **port_data) as port:
# Retrieve the DHCP binding info created in the DB for the
# new port.
dhcp_binding = nsx_db.get_nsx_dhcp_bindings(
context.get_admin_context().session, port['port']['id'])[0]
# Update the port with provided data.
self.plugin.update_port(
context.get_admin_context(), port['port']['id'],
update_data)
binding_data = {'mac_address': port['port']['mac_address'],
'ip_address': port['port']['fixed_ips'][0][
'ip_address']}
# Extend basic binding data with to-be-asserted data.
binding_data.update(assert_data)
# Verify the update call.
update_dhcp_binding.assert_called_once_with(
dhcp_binding['nsx_service_id'],
dhcp_binding['nsx_binding_id'], **binding_data)
def test_dhcp_profile_configuration(self):
# Test if dhcp_agent_notification and dhcp_profile_uuid are
# configured correctly.
orig_dhcp_agent_notification = cfg.CONF.dhcp_agent_notification
cfg.CONF.set_override('dhcp_agent_notification', True)
self.assertRaises(nsx_exc.NsxPluginException, self.plugin._init_dhcp)
cfg.CONF.set_override('dhcp_agent_notification',
orig_dhcp_agent_notification)
orig_dhcp_profile_uuid = cfg.CONF.nsx_v3.dhcp_profile_uuid
cfg.CONF.set_override('dhcp_profile_uuid', '', 'nsx_v3')
self.assertRaises(cfg.RequiredOptError, self.plugin._init_dhcp)
cfg.CONF.set_override('dhcp_profile_uuid', orig_dhcp_profile_uuid,
'nsx_v3')
def test_dhcp_service_with_create_network(self):
# Test if DHCP service is disabled on a network when it is created.
with self.network() as network:
self._verify_dhcp_service(network['network']['id'],
network['network']['tenant_id'], False)
def test_dhcp_service_with_create_non_dhcp_subnet(self):
# Test if DHCP service is disabled on a network when a DHCP-disabled
# subnet is created.
with self.network() as network:
with self.subnet(network=network, enable_dhcp=False):
self._verify_dhcp_service(network['network']['id'],
network['network']['tenant_id'],
False)
def test_dhcp_service_with_create_multiple_non_dhcp_subnets(self):
# Test if DHCP service is disabled on a network when multiple
# DHCP-disabled subnets are created.
with self.network() as network:
with self.subnet(network=network, cidr='10.0.0.0/24',
enable_dhcp=False):
with self.subnet(network=network, cidr='20.0.0.0/24',
enable_dhcp=False):
self._verify_dhcp_service(network['network']['id'],
network['network']['tenant_id'],
False)
def test_dhcp_service_with_create_dhcp_subnet(self):
# Test if DHCP service is enabled on a network when a DHCP-enabled
# subnet is created.
with self.network() as network:
with self.subnet(network=network, enable_dhcp=True):
self._verify_dhcp_service(network['network']['id'],
network['network']['tenant_id'],
True)
def test_dhcp_service_with_create_multiple_dhcp_subnets(self):
# Test if multiple DHCP-enabled subnets cannot be created in a network.
with self.network() as network:
with self.subnet(network=network, cidr='10.0.0.0/24',
enable_dhcp=True):
subnet = {'subnet': {'network_id': network['network']['id'],
'cidr': '20.0.0.0/24',
'enable_dhcp': True}}
self.assertRaises(
n_exc.InvalidInput, self.plugin.create_subnet,
context.get_admin_context(), subnet)
def test_dhcp_service_with_delete_dhcp_subnet(self):
# Test if DHCP service is disabled on a network when a DHCP-disabled
# subnet is deleted.
with self.network() as network:
with self.subnet(network=network, enable_dhcp=True) as subnet:
self._verify_dhcp_service(network['network']['id'],
network['network']['tenant_id'],
True)
self.plugin.delete_subnet(context.get_admin_context(),
subnet['subnet']['id'])
self._verify_dhcp_service(network['network']['id'],
network['network']['tenant_id'],
False)
def test_dhcp_service_with_update_dhcp_subnet(self):
# Test if DHCP service is enabled on a network when a DHCP-disabled
# subnet is updated to DHCP-enabled.
with self.network() as network:
with self.subnet(network=network, enable_dhcp=False) as subnet:
self._verify_dhcp_service(network['network']['id'],
network['network']['tenant_id'], False)
data = {'subnet': {'enable_dhcp': True}}
self.plugin.update_subnet(context.get_admin_context(),
subnet['subnet']['id'], data)
self._verify_dhcp_service(network['network']['id'],
network['network']['tenant_id'],
True)
def test_dhcp_service_with_update_multiple_dhcp_subnets(self):
# Test if a DHCP-disabled subnet cannot be updated to DHCP-enabled
# if a DHCP-enabled subnet already exists in the same network.
with self.network() as network:
with self.subnet(network=network, cidr='10.0.0.0/24',
enable_dhcp=True):
with self.subnet(network=network, cidr='20.0.0.0/24',
enable_dhcp=False) as subnet:
self._verify_dhcp_service(network['network']['id'],
network['network']['tenant_id'],
True)
data = {'subnet': {'enable_dhcp': True}}
self.assertRaises(
n_exc.InvalidInput, self.plugin.update_subnet,
context.get_admin_context(), subnet['subnet']['id'],
data)
def test_dhcp_service_with_update_dhcp_port(self):
# Test if DHCP server IP is updated when the corresponding DHCP port
# IP is changed.
with mock.patch.object(nsx_resources.LogicalDhcpServer,
'update') as update_logical_dhcp_server:
with self.subnet(cidr='10.0.0.0/24', enable_dhcp=True) as subnet:
dhcp_service = nsx_db.get_nsx_service_binding(
context.get_admin_context().session,
subnet['subnet']['network_id'], nsx_constants.SERVICE_DHCP)
port = self.plugin.get_port(context.get_admin_context(),
dhcp_service['port_id'])
old_ip = port['fixed_ips'][0]['ip_address']
new_ip = str(netaddr.IPAddress(old_ip) + 1)
data = {'port': {'fixed_ips': [
{'subnet_id': subnet['subnet']['id'],
'ip_address': new_ip}]}}
self.plugin.update_port(context.get_admin_context(),
dhcp_service['port_id'], data)
update_logical_dhcp_server.assert_called_once_with(
dhcp_service['nsx_service_id'], server_ip=new_ip)
def test_dhcp_binding_with_create_port(self):
# Test if DHCP binding is added when a compute port is created.
with mock.patch.object(nsx_resources.LogicalDhcpServer,
'create_binding',
return_value={"id": uuidutils.generate_uuid()}
) as create_dhcp_binding:
with self.subnet(enable_dhcp=True) as subnet:
device_owner = constants.DEVICE_OWNER_COMPUTE_PREFIX + 'None'
device_id = uuidutils.generate_uuid()
with self.port(subnet=subnet, device_owner=device_owner,
device_id=device_id) as port:
dhcp_service = nsx_db.get_nsx_service_binding(
context.get_admin_context().session,
subnet['subnet']['network_id'],
nsx_constants.SERVICE_DHCP)
ip = port['port']['fixed_ips'][0]['ip_address']
hostname = 'host-%s' % ip.replace('.', '-')
options = {'option121': {'static_routes': [
{'network': '%s' % nsx_rpc.METADATA_DHCP_ROUTE,
'next_hop': ip}]}}
create_dhcp_binding.assert_called_once_with(
dhcp_service['nsx_service_id'],
port['port']['mac_address'], ip, hostname,
cfg.CONF.nsx_v3.dhcp_lease_time, options)
def test_dhcp_binding_with_delete_port(self):
# Test if DHCP binding is removed when the associated compute port
# is deleted.
with mock.patch.object(nsx_resources.LogicalDhcpServer,
'delete_binding') as delete_dhcp_binding:
with self.subnet(enable_dhcp=True) as subnet:
device_owner = constants.DEVICE_OWNER_COMPUTE_PREFIX + 'None'
device_id = uuidutils.generate_uuid()
with self.port(subnet=subnet, device_owner=device_owner,
device_id=device_id) as port:
dhcp_binding = nsx_db.get_nsx_dhcp_bindings(
context.get_admin_context().session,
port['port']['id'])[0]
self.plugin.delete_port(
context.get_admin_context(), port['port']['id'])
delete_dhcp_binding.assert_called_once_with(
dhcp_binding['nsx_service_id'],
dhcp_binding['nsx_binding_id'])
def test_dhcp_binding_with_update_port_delete_ip(self):
# Test if DHCP binding is deleted when the IP of the associated
# compute port is deleted.
with mock.patch.object(nsx_resources.LogicalDhcpServer,
'delete_binding') as delete_dhcp_binding:
with self.subnet(enable_dhcp=True) as subnet:
device_owner = constants.DEVICE_OWNER_COMPUTE_PREFIX + 'None'
device_id = uuidutils.generate_uuid()
with self.port(subnet=subnet, device_owner=device_owner,
device_id=device_id) as port:
dhcp_binding = nsx_db.get_nsx_dhcp_bindings(
context.get_admin_context().session,
port['port']['id'])[0]
data = {'port': {'fixed_ips': [],
'admin_state_up': False,
secgrp.SECURITYGROUPS: []}}
self.plugin.update_port(
context.get_admin_context(), port['port']['id'], data)
delete_dhcp_binding.assert_called_once_with(
dhcp_binding['nsx_service_id'],
dhcp_binding['nsx_binding_id'])
def test_dhcp_binding_with_update_port_ip(self):
# Test if DHCP binding is updated when the IP of the associated
# compute port is changed.
with self.subnet(cidr='10.0.0.0/24', enable_dhcp=True) as subnet:
port_data = {'fixed_ips': [{'subnet_id': subnet['subnet']['id'],
'ip_address': '10.0.0.3'}]}
new_ip = '10.0.0.4'
update_data = {'port': {'fixed_ips': [
{'subnet_id': subnet['subnet']['id'], 'ip_address': new_ip}]}}
assert_data = {'ip_address': new_ip}
self._verify_dhcp_binding(subnet, port_data, update_data,
assert_data)
def test_dhcp_binding_with_update_port_mac(self):
# Test if DHCP binding is updated when the Mac of the associated
# compute port is changed.
with self.subnet(enable_dhcp=True) as subnet:
port_data = {'mac_address': '11:22:33:44:55:66'}
new_mac = '22:33:44:55:66:77'
update_data = {'port': {'mac_address': new_mac}}
assert_data = {'mac_address': new_mac}
self._verify_dhcp_binding(subnet, port_data, update_data,
assert_data)
def test_dhcp_binding_with_update_port_mac_ip(self):
# Test if DHCP binding is updated when the IP and Mac of the associated
# compute port are changed at the same time.
with self.subnet(cidr='10.0.0.0/24', enable_dhcp=True) as subnet:
port_data = {'mac_address': '11:22:33:44:55:66',
'fixed_ips': [{'subnet_id': subnet['subnet']['id'],
'ip_address': '10.0.0.3'}]}
new_mac = '22:33:44:55:66:77'
new_ip = '10.0.0.4'
update_data = {'port': {'mac_address': new_mac, 'fixed_ips': [
{'subnet_id': subnet['subnet']['id'], 'ip_address': new_ip}]}}
assert_data = {'mac_address': new_mac, 'ip_address': new_ip}
self._verify_dhcp_binding(subnet, port_data, update_data,
assert_data)
def test_dhcp_binding_with_update_port_name(self):
# Test if DHCP binding is not updated when the name of the associated
# compute port is changed.
with mock.patch.object(nsx_resources.LogicalDhcpServer,
'update_binding') as update_dhcp_binding:
with self.subnet(cidr='10.0.0.0/24', enable_dhcp=True) as subnet:
device_owner = constants.DEVICE_OWNER_COMPUTE_PREFIX + 'None'
device_id = uuidutils.generate_uuid()
with self.port(subnet=subnet, device_owner=device_owner,
device_id=device_id, name='abc') as port:
data = {'port': {'name': 'xyz'}}
self.plugin.update_port(
context.get_admin_context(), port['port']['id'], data)
update_dhcp_binding.assert_not_called()
def test_dhcp_binding_with_multiple_ips(self):
# Test create/update/delete DHCP binding with multiple IPs on a
# compute port.
with mock.patch.object(nsx_resources.LogicalDhcpServer,
'create_binding',
side_effect=[{"id": uuidutils.generate_uuid()},
{"id": uuidutils.generate_uuid()}]
) as create_dhcp_binding:
with mock.patch.object(nsx_resources.LogicalDhcpServer,
'update_binding'
) as update_dhcp_binding:
with mock.patch.object(nsx_resources.LogicalDhcpServer,
'delete_binding'
) as delete_dhcp_binding:
with self.subnet(cidr='10.0.0.0/24', enable_dhcp=True
) as subnet:
device_owner = (constants.DEVICE_OWNER_COMPUTE_PREFIX +
'None')
device_id = uuidutils.generate_uuid()
fixed_ips = [{'subnet_id': subnet['subnet']['id'],
'ip_address': '10.0.0.3'},
{'subnet_id': subnet['subnet']['id'],
'ip_address': '10.0.0.4'}]
with self.port(subnet=subnet,
device_owner=device_owner,
device_id=device_id,
fixed_ips=fixed_ips) as port:
self.assertEqual(create_dhcp_binding.call_count, 2)
new_fixed_ips = [
{'subnet_id': subnet['subnet']['id'],
'ip_address': '10.0.0.5'},
{'subnet_id': subnet['subnet']['id'],
'ip_address': '10.0.0.6'}]
self.plugin.update_port(
context.get_admin_context(),
port['port']['id'],
{'port': {'fixed_ips': new_fixed_ips}})
self.assertEqual(update_dhcp_binding.call_count, 2)
self.plugin.delete_port(
context.get_admin_context(),
port['port']['id'])
self.assertEqual(delete_dhcp_binding.call_count, 2)

View File

@ -31,6 +31,7 @@ NSX_CERT = '/opt/stack/certs/nsx.pem'
NSX_HTTP_TIMEOUT = 10
NSX_HTTP_READ_TIMEOUT = 180
NSX_TZ_NAME = 'default transport zone'
NSX_DHCP_PROFILE_ID = 'default dhcp profile'
V3_CLIENT_PKG = 'vmware_nsx.nsxlib.v3.client'
BRIDGE_FNS = ['create_resource', 'delete_resource',
@ -42,6 +43,9 @@ class NsxLibTestCase(unittest.TestCase):
@classmethod
def setup_conf_overrides(cls):
cfg.CONF.set_override('default_overlay_tz', NSX_TZ_NAME, 'nsx_v3')
cfg.CONF.set_override('native_dhcp_metadata', False, 'nsx_v3')
cfg.CONF.set_override('dhcp_profile_uuid',
NSX_DHCP_PROFILE_ID, 'nsx_v3')
cfg.CONF.set_override('nsx_api_user', NSX_USER, 'nsx_v3')
cfg.CONF.set_override('nsx_api_password', NSX_PASSWORD, 'nsx_v3')
cfg.CONF.set_override('nsx_api_managers', [NSX_MANAGER], 'nsx_v3')