@ -13,13 +13,22 @@
# License for the specific language governing permissions and limitations
# under the License.
import netaddr
from oslo_db import exception as db_exc
from oslo_log import log as logging
from oslo_utils import excutils
from sqlalchemy import and_
from neutron . api . v2 import attributes
from neutron . common import constants
from neutron . common import exceptions as n_exc
from neutron . common import ipv6_utils
from neutron . db import ipam_backend_mixin
from neutron . db import models_v2
from neutron . i18n import _LE
from neutron . ipam import driver
from neutron . ipam import exceptions as ipam_exc
from neutron . ipam import requests as ipam_req
LOG = logging . getLogger ( __name__ )
@ -110,7 +119,6 @@ class IpamPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
ip_address , ip_subnet = self . _ipam_allocate_single_ip (
context , ipam_driver , port , ip_list )
allocated . append ( { ' ip_address ' : ip_address ,
' subnet_cidr ' : ip_subnet [ ' subnet_cidr ' ] ,
' subnet_id ' : ip_subnet [ ' subnet_id ' ] } )
except Exception :
with excutils . save_and_reraise_exception ( ) :
@ -127,3 +135,317 @@ class IpamPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
" external system for %s " ) , addresses )
return allocated
def _ipam_update_allocation_pools ( self , context , ipam_driver , subnet ) :
self . _validate_allocation_pools ( subnet [ ' allocation_pools ' ] ,
subnet [ ' cidr ' ] )
factory = ipam_driver . get_subnet_request_factory ( )
subnet_request = factory . get_request ( context , subnet , None )
ipam_driver . update_subnet ( subnet_request )
def delete_subnet ( self , context , subnet_id ) :
ipam_driver = driver . Pool . get_instance ( None , context )
ipam_driver . remove_subnet ( subnet_id )
def allocate_ips_for_port_and_store ( self , context , port , port_id ) :
network_id = port [ ' port ' ] [ ' network_id ' ]
ips = [ ]
try :
ips = self . _allocate_ips_for_port ( context , port )
for ip in ips :
ip_address = ip [ ' ip_address ' ]
subnet_id = ip [ ' subnet_id ' ]
IpamPluggableBackend . _store_ip_allocation (
context , ip_address , network_id ,
subnet_id , port_id )
except Exception :
with excutils . save_and_reraise_exception ( ) :
if ips :
LOG . debug ( " An exception occurred during port creation. "
" Reverting IP allocation " )
ipam_driver = driver . Pool . get_instance ( None , context )
self . _ipam_deallocate_ips ( context , ipam_driver ,
port [ ' port ' ] , ips ,
revert_on_fail = False )
def _allocate_ips_for_port ( self , context , port ) :
""" Allocate IP addresses for the port. IPAM version.
If port [ ' fixed_ips ' ] is set to ' ATTR_NOT_SPECIFIED ' , allocate IP
addresses for the port . If port [ ' fixed_ips ' ] contains an IP address or
a subnet_id then allocate an IP address accordingly .
"""
p = port [ ' port ' ]
ips = [ ]
v6_stateless = [ ]
net_id_filter = { ' network_id ' : [ p [ ' network_id ' ] ] }
subnets = self . _get_subnets ( context , filters = net_id_filter )
is_router_port = (
p [ ' device_owner ' ] in constants . ROUTER_INTERFACE_OWNERS_SNAT )
fixed_configured = p [ ' fixed_ips ' ] is not attributes . ATTR_NOT_SPECIFIED
if fixed_configured :
ips = self . _test_fixed_ips_for_port ( context ,
p [ " network_id " ] ,
p [ ' fixed_ips ' ] ,
p [ ' device_owner ' ] )
# For ports that are not router ports, implicitly include all
# auto-address subnets for address association.
if not is_router_port :
v6_stateless + = [ subnet for subnet in subnets
if ipv6_utils . is_auto_address_subnet ( subnet ) ]
else :
# Split into v4, v6 stateless and v6 stateful subnets
v4 = [ ]
v6_stateful = [ ]
for subnet in subnets :
if subnet [ ' ip_version ' ] == 4 :
v4 . append ( subnet )
else :
if ipv6_utils . is_auto_address_subnet ( subnet ) :
if not is_router_port :
v6_stateless . append ( subnet )
else :
v6_stateful . append ( subnet )
version_subnets = [ v4 , v6_stateful ]
for subnets in version_subnets :
if subnets :
ips . append ( [ { ' subnet_id ' : s [ ' id ' ] }
for s in subnets ] )
for subnet in v6_stateless :
# IP addresses for IPv6 SLAAC and DHCPv6-stateless subnets
# are implicitly included.
ips . append ( { ' subnet_id ' : subnet [ ' id ' ] ,
' subnet_cidr ' : subnet [ ' cidr ' ] ,
' eui64_address ' : True ,
' mac ' : p [ ' mac_address ' ] } )
ipam_driver = driver . Pool . get_instance ( None , context )
return self . _ipam_allocate_ips ( context , ipam_driver , p , ips )
def _test_fixed_ips_for_port ( self , context , network_id , fixed_ips ,
device_owner ) :
""" Test fixed IPs for port.
Check that configured subnets are valid prior to allocating any
IPs . Include the subnet_id in the result if only an IP address is
configured .
: raises : InvalidInput , IpAddressInUse , InvalidIpForNetwork ,
InvalidIpForSubnet
"""
fixed_ip_list = [ ]
for fixed in fixed_ips :
subnet = self . _get_subnet_for_fixed_ip ( context , fixed , network_id )
is_auto_addr_subnet = ipv6_utils . is_auto_address_subnet ( subnet )
if ' ip_address ' in fixed :
if ( is_auto_addr_subnet and device_owner not in
constants . ROUTER_INTERFACE_OWNERS ) :
msg = ( _ ( " IPv6 address %(address)s can not be directly "
" assigned to a port on subnet %(id)s since the "
" subnet is configured for automatic addresses " ) %
{ ' address ' : fixed [ ' ip_address ' ] ,
' id ' : subnet [ ' id ' ] } )
raise n_exc . InvalidInput ( error_message = msg )
fixed_ip_list . append ( { ' subnet_id ' : subnet [ ' id ' ] ,
' ip_address ' : fixed [ ' ip_address ' ] } )
else :
# A scan for auto-address subnets on the network is done
# separately so that all such subnets (not just those
# listed explicitly here by subnet ID) are associated
# with the port.
if ( device_owner in constants . ROUTER_INTERFACE_OWNERS_SNAT or
not is_auto_addr_subnet ) :
fixed_ip_list . append ( { ' subnet_id ' : subnet [ ' id ' ] } )
self . _validate_max_ips_per_port ( fixed_ip_list )
return fixed_ip_list
def _update_ips_for_port ( self , context , port ,
original_ips , new_ips , mac ) :
""" Add or remove IPs from the port. IPAM version """
added = [ ]
removed = [ ]
changes = self . _get_changed_ips_for_port (
context , original_ips , new_ips , port [ ' device_owner ' ] )
# Check if the IP's to add are OK
to_add = self . _test_fixed_ips_for_port (
context , port [ ' network_id ' ] , changes . add ,
port [ ' device_owner ' ] )
ipam_driver = driver . Pool . get_instance ( None , context )
if changes . remove :
removed = self . _ipam_deallocate_ips ( context , ipam_driver , port ,
changes . remove )
if to_add :
added = self . _ipam_allocate_ips ( context , ipam_driver ,
changes , to_add )
return self . Changes ( add = added ,
original = changes . original ,
remove = removed )
def save_allocation_pools ( self , context , subnet , allocation_pools ) :
for pool in allocation_pools :
first_ip = str ( netaddr . IPAddress ( pool . first , pool . version ) )
last_ip = str ( netaddr . IPAddress ( pool . last , pool . version ) )
ip_pool = models_v2 . IPAllocationPool ( subnet = subnet ,
first_ip = first_ip ,
last_ip = last_ip )
context . session . add ( ip_pool )
def update_port_with_ips ( self , context , db_port , new_port , new_mac ) :
changes = self . Changes ( add = [ ] , original = [ ] , remove = [ ] )
if ' fixed_ips ' in new_port :
original = self . _make_port_dict ( db_port ,
process_extensions = False )
changes = self . _update_ips_for_port ( context ,
db_port ,
original [ " fixed_ips " ] ,
new_port [ ' fixed_ips ' ] ,
new_mac )
try :
# Check if the IPs need to be updated
network_id = db_port [ ' network_id ' ]
for ip in changes . add :
self . _store_ip_allocation (
context , ip [ ' ip_address ' ] , network_id ,
ip [ ' subnet_id ' ] , db_port . id )
for ip in changes . remove :
self . _delete_ip_allocation ( context , network_id ,
ip [ ' subnet_id ' ] , ip [ ' ip_address ' ] )
self . _update_db_port ( context , db_port , new_port , network_id ,
new_mac )
except Exception :
with excutils . save_and_reraise_exception ( ) :
if ' fixed_ips ' in new_port :
LOG . debug ( " An exception occurred during port update. " )
ipam_driver = driver . Pool . get_instance ( None , context )
if changes . add :
LOG . debug ( " Reverting IP allocation. " )
self . _ipam_deallocate_ips ( context , ipam_driver ,
db_port , changes . add ,
revert_on_fail = False )
if changes . remove :
LOG . debug ( " Reverting IP deallocation. " )
self . _ipam_allocate_ips ( context , ipam_driver ,
db_port , changes . remove ,
revert_on_fail = False )
return changes
def delete_port ( self , context , id ) :
# Get fixed_ips list before port deletion
port = self . _get_port ( context , id )
ipam_driver = driver . Pool . get_instance ( None , context )
super ( IpamPluggableBackend , self ) . delete_port ( context , id )
# Deallocating ips via IPAM after port is deleted locally.
# So no need to do rollback actions on remote server
# in case of fail to delete port locally
self . _ipam_deallocate_ips ( context , ipam_driver , port ,
port [ ' fixed_ips ' ] )
def update_db_subnet ( self , context , id , s , old_pools ) :
ipam_driver = driver . Pool . get_instance ( None , context )
if " allocation_pools " in s :
self . _ipam_update_allocation_pools ( context , ipam_driver , s )
try :
subnet , changes = super ( IpamPluggableBackend ,
self ) . update_db_subnet ( context , id ,
s , old_pools )
except Exception :
with excutils . save_and_reraise_exception ( ) :
if " allocation_pools " in s and old_pools :
LOG . error (
_LE ( " An exception occurred during subnet update. "
" Reverting allocation pool changes " ) )
s [ ' allocation_pools ' ] = old_pools
self . _ipam_update_allocation_pools ( context , ipam_driver , s )
return subnet , changes
def add_auto_addrs_on_network_ports ( self , context , subnet , ipam_subnet ) :
""" For an auto-address subnet, add addrs for ports on the net. """
with context . session . begin ( subtransactions = True ) :
network_id = subnet [ ' network_id ' ]
port_qry = context . session . query ( models_v2 . Port )
ports = port_qry . filter (
and_ ( models_v2 . Port . network_id == network_id ,
~ models_v2 . Port . device_owner . in_ (
constants . ROUTER_INTERFACE_OWNERS_SNAT ) ) )
for port in ports :
ip_request = ipam_req . AutomaticAddressRequest (
prefix = subnet [ ' cidr ' ] ,
mac = port [ ' mac_address ' ] )
ip_address = ipam_subnet . allocate ( ip_request )
allocated = models_v2 . IPAllocation ( network_id = network_id ,
port_id = port [ ' id ' ] ,
ip_address = ip_address ,
subnet_id = subnet [ ' id ' ] )
try :
# Do the insertion of each IP allocation entry within
# the context of a nested transaction, so that the entry
# is rolled back independently of other entries whenever
# the corresponding port has been deleted.
with context . session . begin_nested ( ) :
context . session . add ( allocated )
except db_exc . DBReferenceError :
LOG . debug ( " Port %s was deleted while updating it with an "
" IPv6 auto-address. Ignoring. " , port [ ' id ' ] )
LOG . debug ( " Reverting IP allocation for %s " , ip_address )
# Do not fail if reverting allocation was unsuccessful
try :
ipam_subnet . deallocate ( ip_address )
except Exception :
LOG . debug ( " Reverting IP allocation failed for %s " ,
ip_address )
def allocate_subnet ( self , context , network , subnet , subnetpool_id ) :
subnetpool = None
if subnetpool_id :
subnetpool = self . _get_subnetpool ( context , subnetpool_id )
self . _validate_ip_version_with_subnetpool ( subnet , subnetpool )
# gateway_ip and allocation pools should be validated or generated
# only for specific request
if subnet [ ' cidr ' ] is not attributes . ATTR_NOT_SPECIFIED :
subnet [ ' gateway_ip ' ] = self . _gateway_ip_str ( subnet ,
subnet [ ' cidr ' ] )
subnet [ ' allocation_pools ' ] = self . _prepare_allocation_pools (
subnet [ ' allocation_pools ' ] ,
subnet [ ' cidr ' ] ,
subnet [ ' gateway_ip ' ] )
ipam_driver = driver . Pool . get_instance ( subnetpool , context )
subnet_factory = ipam_driver . get_subnet_request_factory ( )
subnet_request = subnet_factory . get_request ( context , subnet ,
subnetpool )
ipam_subnet = ipam_driver . allocate_subnet ( subnet_request )
# get updated details with actually allocated subnet
subnet_request = ipam_subnet . get_details ( )
try :
subnet = self . _save_subnet ( context ,
network ,
self . _make_subnet_args (
subnet_request ,
subnet ,
subnetpool_id ) ,
subnet [ ' dns_nameservers ' ] ,
subnet [ ' host_routes ' ] ,
subnet_request )
except Exception :
# Note(pbondar): Third-party ipam servers can't rely
# on transaction rollback, so explicit rollback call needed.
# IPAM part rolled back in exception handling
# and subnet part is rolled back by transaction rollback.
with excutils . save_and_reraise_exception ( ) :
LOG . debug ( " An exception occurred during subnet creation. "
" Reverting subnet allocation. " )
self . delete_subnet ( context , subnet_request . subnet_id )
return subnet , ipam_subnet