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:
parent
b2467e9889
commit
5dc1c19037
@ -127,6 +127,7 @@ function neutron_plugin_configure_service {
|
|||||||
_nsxv3_ini_set insecure $NSX_INSECURE
|
_nsxv3_ini_set insecure $NSX_INSECURE
|
||||||
_nsxv3_ini_set ca_file $NSX_CA_FILE
|
_nsxv3_ini_set ca_file $NSX_CA_FILE
|
||||||
_nsxv3_ini_set default_bridge_cluster $DEFAULT_BRIDGE_CLUSTER_UUID
|
_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 {
|
function neutron_plugin_setup_interface_driver {
|
||||||
|
@ -11,3 +11,4 @@ DEFAULT_OVERLAY_TZ_UUID=<FILL_IN>
|
|||||||
EDGE_CLUSTER_UUID=<FILL_IN>
|
EDGE_CLUSTER_UUID=<FILL_IN>
|
||||||
NSX_MANAGER=<FILL_IN>
|
NSX_MANAGER=<FILL_IN>
|
||||||
NSX_CONTROLLERS=<FILL_IN>
|
NSX_CONTROLLERS=<FILL_IN>
|
||||||
|
DHCP_PROFILE_UUID=<FILL_IN>
|
||||||
|
@ -347,6 +347,26 @@ nsx_v3_opts = [
|
|||||||
help=_("If true, an internal metadata network will be created "
|
help=_("If true, an internal metadata network will be created "
|
||||||
"for a router only when the router is attached to a "
|
"for a router only when the router is attached to a "
|
||||||
"DHCP-disabled subnet.")),
|
"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',
|
cfg.BoolOpt('log_security_groups_blocked_traffic',
|
||||||
default=False,
|
default=False,
|
||||||
help=_("(Optional) Indicates whether distributed-firewall "
|
help=_("(Optional) Indicates whether distributed-firewall "
|
||||||
|
@ -22,6 +22,7 @@ ADMIN_STATUSES = [ADMIN_STATE_UP, ADMIN_STATE_DOWN]
|
|||||||
# Port attachment types
|
# Port attachment types
|
||||||
ATTACHMENT_VIF = "VIF"
|
ATTACHMENT_VIF = "VIF"
|
||||||
ATTACHMENT_LR = "LOGICALROUTER"
|
ATTACHMENT_LR = "LOGICALROUTER"
|
||||||
|
ATTACHMENT_DHCP = "DHCP_SERVICE"
|
||||||
ATTACHMENT_CIF = "CIF"
|
ATTACHMENT_CIF = "CIF"
|
||||||
CIF_RESOURCE_TYPE = "CifAttachmentContext"
|
CIF_RESOURCE_TYPE = "CifAttachmentContext"
|
||||||
|
|
||||||
@ -54,3 +55,6 @@ VIF_TYPE_DVS = 'dvs'
|
|||||||
|
|
||||||
# NSXv3 L2 Gateway constants
|
# NSXv3 L2 Gateway constants
|
||||||
BRIDGE_ENDPOINT = "BRIDGEENDPOINT"
|
BRIDGE_ENDPOINT = "BRIDGEENDPOINT"
|
||||||
|
|
||||||
|
# NSX service type
|
||||||
|
SERVICE_DHCP = "dhcp"
|
||||||
|
@ -114,6 +114,63 @@ def add_neutron_nsx_security_group_mapping(session, neutron_id, nsx_id):
|
|||||||
return mapping
|
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):
|
def get_nsx_switch_ids(session, neutron_id):
|
||||||
# This function returns a list of NSX switch identifiers because of
|
# This function returns a list of NSX switch identifiers because of
|
||||||
# the possibility of chained logical switches
|
# the possibility of chained logical switches
|
||||||
|
@ -1 +1 @@
|
|||||||
c288bb6a7252
|
c644ec62c585
|
||||||
|
@ -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'))
|
@ -26,6 +26,8 @@ from sqlalchemy import sql
|
|||||||
from neutron.db import model_base
|
from neutron.db import model_base
|
||||||
from neutron.db import models_v2
|
from neutron.db import models_v2
|
||||||
|
|
||||||
|
from vmware_nsx.common import nsx_constants
|
||||||
|
|
||||||
|
|
||||||
class TzNetworkBinding(model_base.BASEV2):
|
class TzNetworkBinding(model_base.BASEV2):
|
||||||
"""Represents a binding of a virtual network with a transport zone.
|
"""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))
|
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):
|
class MultiProviderNetworks(model_base.BASEV2):
|
||||||
"""Networks provisioned through multiprovider extension."""
|
"""Networks provisioned through multiprovider extension."""
|
||||||
|
|
||||||
|
62
vmware_nsx/nsxlib/v3/native_dhcp.py
Normal file
62
vmware_nsx/nsxlib/v3/native_dhcp.py
Normal 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}
|
@ -425,3 +425,88 @@ class LogicalRouterPort(AbstractRESTResource):
|
|||||||
raise nsx_exc.ResourceNotFound(
|
raise nsx_exc.ResourceNotFound(
|
||||||
manager=client._get_nsx_managers_from_conf(),
|
manager=client._get_nsx_managers_from_conf(),
|
||||||
operation="get router link port")
|
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)
|
||||||
|
@ -62,6 +62,7 @@ from neutron_lib.api import validators
|
|||||||
from neutron_lib import constants as const
|
from neutron_lib import constants as const
|
||||||
from neutron_lib import exceptions as n_exc
|
from neutron_lib import exceptions as n_exc
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
from oslo_db import exception as db_exc
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
from oslo_utils import excutils
|
from oslo_utils import excutils
|
||||||
from oslo_utils import importutils
|
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 client as nsx_client
|
||||||
from vmware_nsx.nsxlib.v3 import cluster as nsx_cluster
|
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 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 resources as nsx_resources
|
||||||
from vmware_nsx.nsxlib.v3 import router
|
from vmware_nsx.nsxlib.v3 import router
|
||||||
from vmware_nsx.nsxlib.v3 import security
|
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.cfg_group = 'nsx_v3' # group name for nsx_v3 section in nsx.ini
|
||||||
self.tier0_groups_dict = {}
|
self.tier0_groups_dict = {}
|
||||||
self._setup_dhcp()
|
self._init_dhcp()
|
||||||
self._start_rpc_notifiers()
|
|
||||||
|
|
||||||
self._port_client = nsx_resources.LogicalPort(self._nsx_client)
|
self._port_client = nsx_resources.LogicalPort(self._nsx_client)
|
||||||
self.nsgroup_manager, self.default_section = (
|
self.nsgroup_manager, self.default_section = (
|
||||||
@ -336,6 +337,31 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
|
|||||||
with locking.LockManager.get_lock('nsxv3_nsgroup_manager_init'):
|
with locking.LockManager.get_lock('nsxv3_nsgroup_manager_init'):
|
||||||
return security.init_nsgroup_manager_and_default_section_rules()
|
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):
|
def _setup_rpc(self):
|
||||||
self.endpoints = [dhcp_rpc.DhcpRpcCallback(),
|
self.endpoints = [dhcp_rpc.DhcpRpcCallback(),
|
||||||
agents_db.AgentExtRpcCallback(),
|
agents_db.AgentExtRpcCallback(),
|
||||||
@ -574,7 +600,6 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
|
|||||||
context.session,
|
context.session,
|
||||||
neutron_net_id,
|
neutron_net_id,
|
||||||
nsx_net_id)
|
nsx_net_id)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
with excutils.save_and_reraise_exception():
|
with excutils.save_and_reraise_exception():
|
||||||
# Undo creation on the backend
|
# Undo creation on the backend
|
||||||
@ -715,17 +740,224 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
|
|||||||
|
|
||||||
return updated_net
|
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):
|
def create_subnet(self, context, subnet):
|
||||||
# TODO(berlin): public external subnet announcement
|
# 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):
|
def delete_subnet(self, context, subnet_id):
|
||||||
# TODO(berlin): cancel public external subnet announcement
|
# 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):
|
def update_subnet(self, 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(
|
updated_subnet = super(NsxV3Plugin, self).update_subnet(
|
||||||
context, subnet_id, 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 cfg.CONF.nsx_v3.metadata_on_demand:
|
||||||
# If enable_dhcp is changed on a subnet attached to a router,
|
# If enable_dhcp is changed on a subnet attached to a router,
|
||||||
# update internal metadata network accordingly.
|
# update internal metadata network accordingly.
|
||||||
@ -994,6 +1226,205 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
|
|||||||
LOG.warning(err_msg)
|
LOG.warning(err_msg)
|
||||||
raise n_exc.InvalidInput(error_message=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):
|
def create_port(self, context, port, l2gw_port_check=False):
|
||||||
port_data = port['port']
|
port_data = port['port']
|
||||||
dhcp_opts = port_data.get(ext_edo.EXTRADHCPOPTS, [])
|
dhcp_opts = port_data.get(ext_edo.EXTRADHCPOPTS, [])
|
||||||
@ -1062,6 +1493,11 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
|
|||||||
# latest db model for the extension functions
|
# latest db model for the extension functions
|
||||||
port_model = self._get_port(context, port_data['id'])
|
port_model = self._get_port(context, port_data['id'])
|
||||||
self._apply_dict_extend_functions('ports', port_data, port_model)
|
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)
|
nsx_rpc.handle_port_metadata_access(self, context, neutron_db)
|
||||||
return port_data
|
return port_data
|
||||||
|
|
||||||
@ -1098,11 +1534,13 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
|
|||||||
security.update_lport_with_security_groups(
|
security.update_lport_with_security_groups(
|
||||||
context, nsx_port_id, port.get(ext_sg.SECURITYGROUPS, []), [])
|
context, nsx_port_id, port.get(ext_sg.SECURITYGROUPS, []), [])
|
||||||
self.disassociate_floatingips(context, port_id)
|
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,
|
nsx_rpc.handle_port_metadata_access(self, context, port,
|
||||||
is_delete=True)
|
is_delete=True)
|
||||||
ret_val = super(NsxV3Plugin, self).delete_port(context, port_id)
|
super(NsxV3Plugin, self).delete_port(context, port_id)
|
||||||
|
|
||||||
return ret_val
|
|
||||||
|
|
||||||
def _update_port_preprocess_security(
|
def _update_port_preprocess_security(
|
||||||
self, context, port, id, updated_port):
|
self, context, port, id, updated_port):
|
||||||
@ -1356,6 +1794,10 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
|
|||||||
context, id, {'port': original_port},
|
context, id, {'port': original_port},
|
||||||
updated_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
|
return updated_port
|
||||||
|
|
||||||
def _extend_get_port_dict_binding(self, context, port):
|
def _extend_get_port_dict_binding(self, context, port):
|
||||||
|
109
vmware_nsx/shell/admin/plugins/nsxv3/resources/dhcp_binding.py
Normal file
109
vmware_nsx/shell/admin/plugins/nsxv3/resources/dhcp_binding.py
Normal 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)
|
50
vmware_nsx/shell/admin/plugins/nsxv3/resources/utils.py
Normal file
50
vmware_nsx/shell/admin/plugins/nsxv3/resources/utils.py
Normal 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
|
@ -89,6 +89,9 @@ nsxv3_resources = {
|
|||||||
[Operations.LIST_MISMATCHES.value]),
|
[Operations.LIST_MISMATCHES.value]),
|
||||||
constants.ROUTERS: Resource(constants.ROUTERS,
|
constants.ROUTERS: Resource(constants.ROUTERS,
|
||||||
[Operations.LIST_MISMATCHES.value]),
|
[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
|
# Add supported NSX-V resources in this dictionary
|
||||||
|
@ -67,6 +67,12 @@ def make_fake_switch(switch_uuid=None, tz_uuid=None, name=FAKE_NAME):
|
|||||||
return fake_switch
|
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):
|
def get_resource(resource):
|
||||||
return {'id': resource.split('/')[-1]}
|
return {'id': resource.split('/')[-1]}
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
import mock
|
import mock
|
||||||
|
import netaddr
|
||||||
import six
|
import six
|
||||||
from webob import exc
|
from webob import exc
|
||||||
|
|
||||||
@ -44,9 +45,14 @@ from oslo_config import cfg
|
|||||||
from oslo_serialization import jsonutils
|
from oslo_serialization import jsonutils
|
||||||
from oslo_utils import uuidutils
|
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.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 client as nsx_client
|
||||||
from vmware_nsx.nsxlib.v3 import cluster as nsx_cluster
|
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.plugins.nsx_v3 import plugin as nsx_plugin
|
||||||
from vmware_nsx.tests import unit as vmware
|
from vmware_nsx.tests import unit as vmware
|
||||||
from vmware_nsx.tests.unit.extensions import test_metadata
|
from vmware_nsx.tests.unit.extensions import test_metadata
|
||||||
@ -761,3 +767,352 @@ class TestNsxV3Utils(NsxV3PluginTestCaseMixin):
|
|||||||
{'scope': 'os-api-version',
|
{'scope': 'os-api-version',
|
||||||
'tag': version.version_info.release_string()}]
|
'tag': version.version_info.release_string()}]
|
||||||
self.assertEqual(sorted(expected), sorted(tags))
|
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)
|
||||||
|
@ -31,6 +31,7 @@ NSX_CERT = '/opt/stack/certs/nsx.pem'
|
|||||||
NSX_HTTP_TIMEOUT = 10
|
NSX_HTTP_TIMEOUT = 10
|
||||||
NSX_HTTP_READ_TIMEOUT = 180
|
NSX_HTTP_READ_TIMEOUT = 180
|
||||||
NSX_TZ_NAME = 'default transport zone'
|
NSX_TZ_NAME = 'default transport zone'
|
||||||
|
NSX_DHCP_PROFILE_ID = 'default dhcp profile'
|
||||||
|
|
||||||
V3_CLIENT_PKG = 'vmware_nsx.nsxlib.v3.client'
|
V3_CLIENT_PKG = 'vmware_nsx.nsxlib.v3.client'
|
||||||
BRIDGE_FNS = ['create_resource', 'delete_resource',
|
BRIDGE_FNS = ['create_resource', 'delete_resource',
|
||||||
@ -42,6 +43,9 @@ class NsxLibTestCase(unittest.TestCase):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def setup_conf_overrides(cls):
|
def setup_conf_overrides(cls):
|
||||||
cfg.CONF.set_override('default_overlay_tz', NSX_TZ_NAME, 'nsx_v3')
|
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_user', NSX_USER, 'nsx_v3')
|
||||||
cfg.CONF.set_override('nsx_api_password', NSX_PASSWORD, 'nsx_v3')
|
cfg.CONF.set_override('nsx_api_password', NSX_PASSWORD, 'nsx_v3')
|
||||||
cfg.CONF.set_override('nsx_api_managers', [NSX_MANAGER], 'nsx_v3')
|
cfg.CONF.set_override('nsx_api_managers', [NSX_MANAGER], 'nsx_v3')
|
||||||
|
Loading…
Reference in New Issue
Block a user