Ansible Networking drivers for ML2
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

644 lines
28 KiB

# Copyright (c) 2018 OpenStack Foundation
# 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 os
from neutron.db import provisioning_blocks
from neutron.objects.network import Network
from neutron.objects.network import NetworkSegment
from neutron.objects.ports import Port
from neutron.objects.trunk import Trunk
from neutron.plugins.ml2.common import exceptions as ml2_exc
from neutron_lib.api.definitions import portbindings
from neutron_lib.api.definitions import provider_net
from neutron_lib.callbacks import resources
from neutron_lib.plugins.ml2 import api as ml2api
from oslo_config import cfg
from oslo_log import log as logging
from networking_ansible import config
from networking_ansible import constants as c
from networking_ansible import exceptions
from networking_ansible.ml2 import trunk_driver
from network_runner import api as net_runr_api
from network_runner.models.inventory import Inventory
from tooz import coordination
LOG = logging.getLogger(__name__)
CONF = config.CONF
class AnsibleMechanismDriver(ml2api.MechanismDriver):
"""ML2 Mechanism Driver for Ansible Networking
https://www.ansible.com/integrations/networks
"""
def initialize(self):
LOG.debug("Initializing Ansible ML2 driver")
# Get ML2 config
self.ml2config = config.Config()
# Build a network runner inventory object
# and instatiate network runner
_inv = Inventory()
_inv.deserialize({'all': {'hosts': self.ml2config.inventory}})
self.net_runr = net_runr_api.NetworkRunner(_inv)
# build the custom params and extra params dict.
# this holds kwargs per host to pass to network runner
self.kwargs = {}
for host_name in self.ml2config.inventory:
self.kwargs[host_name] = {}
for key, val in self.ml2config.inventory[host_name].items():
if key in c.EXTRA_PARAMS or \
key.startswith(c.CUSTOM_PARAM_PREFIX):
if key.startswith(c.CUSTOM_PARAM_PREFIX):
key = key[len(c.CUSTOM_PARAM_PREFIX):]
self.kwargs[host_name][key] = val
self.coordinator = coordination.get_coordinator(
cfg.CONF.ml2_ansible.coordination_uri,
'{}-{}'.format(CONF.host, os.getpid()))
# the heartbeat will have the default timeout of 30 seconds
# that can be changed per-driver. Both Redis and etcd drivers
# use 30 second timeouts.
self.coordinator.start(start_heart=True)
LOG.debug("Ansible ML2 coordination started via uri %s",
cfg.CONF.ml2_ansible.coordination_uri)
self.trunk_driver = trunk_driver.NetAnsibleTrunkDriver.create(self)
def create_network_postcommit(self, context):
"""Create a network.
:param context: NetworkContext instance describing the new
network.
Called after the transaction commits. Call can block, though
will block the entire process so care should be taken to not
drastically affect performance. Raising an exception will
cause the deletion of the resource.
"""
# assuming all hosts
# TODO(radez): can we filter by physnets?
# TODO(michchap): we should be able to do each switch in parallel
# if it becomes a performance issue. switch topology might also
# open up options for filtering.
for host_name in self.ml2config.inventory:
host = self.ml2config.inventory[host_name]
if host.get('manage_vlans', True):
network = context.current
network_id = network['id']
provider_type = network[provider_net.NETWORK_TYPE]
segmentation_id = network[provider_net.SEGMENTATION_ID]
lock = self.coordinator.get_lock(host_name)
with lock:
if provider_type == 'vlan' and segmentation_id:
# re-request network info in case it's stale
net = Network.get_object(context._plugin_context,
id=network_id)
LOG.debug('network create object: {}'.format(net))
# network was since deleted by user and we can discard
# this request
if not net:
return
# check the vlan for this request is still associated
# with this network. We don't currently allow updating
# the segment on a network - it's disallowed at the
# neutron level for provider networks - but that could
# change in the future
s_ids = [s.segmentation_id for s in net.segments]
if segmentation_id not in s_ids:
return
# Create VLAN on the switch
try:
self.net_runr.create_vlan(
host_name,
segmentation_id,
**self.kwargs[host_name])
LOG.info('Network {net_id}, segmentation '
'{seg} has been added on '
'ansible host {host}'.format(
net_id=network['id'],
seg=segmentation_id,
host=host_name))
except Exception as e:
# TODO(radez) I don't think there is a message
# returned from ansible runner's
# exceptions
LOG.error('Failed to create network {net_id} '
'on ansible host: {host}, '
'reason: {err}'.format(
net_id=network_id,
host=host_name,
err=e))
raise exceptions.NetworkingAnsibleMechException(e)
def delete_network_postcommit(self, context):
"""Delete a network.
:param context: NetworkContext instance describing the current
state of the network, prior to the call to delete it.
Called after the transaction commits. Call can block, though
will block the entire process so care should be taken to not
drastically affect performance. Runtime errors are not
expected, and will not prevent the resource from being
deleted.
"""
# assuming all hosts
# TODO(radez): can we filter by physnets?
LOG.debug('ansnet:network delete')
for host_name in self.ml2config.inventory:
host = self.ml2config.inventory[host_name]
network = context.current
provider_type = network[provider_net.NETWORK_TYPE]
segmentation_id = network[provider_net.SEGMENTATION_ID]
physnet = network[provider_net.PHYSICAL_NETWORK]
if provider_type == 'vlan' and segmentation_id:
if host.get('manage_vlans', True):
lock = self.coordinator.get_lock(host_name)
with lock:
# Find out if this segment is active.
# We need to find out if this segment is being used
# by another network before deleting it from the switch
# since reordering could mean that a vlan is recycled
# by the time this request is satisfied. Getting
# the current network is not enough
db = context._plugin_context
segments = NetworkSegment.get_objects(
db, segmentation_id=segmentation_id)
for segment in segments:
if segment.segmentation_id == segmentation_id and \
segment.physical_network == physnet and \
segment.network_type == 'vlan':
LOG.debug('Not deleting segment {} from {}'
'because it was recreated'.format(
segmentation_id, physnet))
return
# Delete VLAN on the switch
try:
self.net_runr.delete_vlan(
host_name,
segmentation_id,
**self.kwargs[host_name])
LOG.info('Network {net_id} has been deleted on '
'ansible host {host}'.format(
net_id=network['id'],
host=host_name))
except Exception as e:
LOG.error('Failed to delete network {net} '
'on ansible host: {host}, '
'reason: {err}'.format(net=network['id'],
host=host_name,
err=e))
raise exceptions.NetworkingAnsibleMechException(e)
def update_port_postcommit(self, context):
"""Update a port.
:param context: PortContext instance describing the new
state of the port, as well as the original state prior
to the update_port call.
Called after the transaction completes. Call can block, though
will block the entire process so care should be taken to not
drastically affect performance. Raising an exception will
result in the deletion of the resource.
update_port_postcommit is called for all changes to the port
state. It is up to the mechanism driver to ignore state or
state changes that it does not know or care about.
"""
if self._is_port_bound(context.current):
port = context.current
provisioning_blocks.provisioning_complete(
context._plugin_context, port['id'], resources.PORT,
c.NETWORKING_ENTITY)
elif self._is_port_bound(context.original):
port = context.original
network = context.network.current
switch_name, switch_port, segmentation_id = \
self._link_info_from_port(context.original, network)
LOG.debug('Ensuring Updated port {switch_port} on network '
'{network} {switch_name} to vlan: '
'{segmentation_id}'.format(
switch_port=switch_port,
network=network,
switch_name=switch_name,
segmentation_id=segmentation_id))
self.ensure_port(port, context._plugin_context,
switch_name, switch_port,
network[provider_net.PHYSICAL_NETWORK], context)
def delete_port_postcommit(self, context):
"""Delete a port.
:param context: PortContext instance describing the current
state of the port, prior to the call to delete it.
Called after the transaction completes. Call can block, though
will block the entire process so care should be taken to not
drastically affect performance. Runtime errors are not
expected, and will not prevent the resource from being
deleted.
"""
port = context.current
network = context.network.current
if self._is_port_bound(context.current):
switch_name, switch_port, segmentation_id = \
self._link_info_from_port(port, network)
LOG.debug('Ensuring Deleted port {switch_port} on '
'{switch_name} to vlan: {segmentation_id}'.format(
switch_port=switch_port,
switch_name=switch_name,
segmentation_id=segmentation_id))
self.ensure_port(port, context._plugin_context,
switch_name, switch_port,
network[provider_net.PHYSICAL_NETWORK], context)
def bind_port(self, context):
"""Attempt to bind a port.
:param context: PortContext instance describing the port
This method is called outside any transaction to attempt to
establish a port binding using this mechanism driver. Bindings
may be created at each of multiple levels of a hierarchical
network, and are established from the top level downward. At
each level, the mechanism driver determines whether it can
bind to any of the network segments in the
context.segments_to_bind property, based on the value of the
context.host property, any relevant port or network
attributes, and its own knowledge of the network topology. At
the top level, context.segments_to_bind contains the static
segments of the port's network. At each lower level of
binding, it contains static or dynamic segments supplied by
the driver that bound at the level above. If the driver is
able to complete the binding of the port to any segment in
context.segments_to_bind, it must call context.set_binding
with the binding details. If it can partially bind the port,
it must call context.continue_binding with the network
segments to be used to bind at the next lower level.
If the binding results are committed after bind_port returns,
they will be seen by all mechanism drivers as
update_port_precommit and update_port_postcommit calls. But if
some other thread or process concurrently binds or updates the
port, these binding results will not be committed, and
update_port_precommit and update_port_postcommit will not be
called on the mechanism drivers with these results. Because
binding results can be discarded rather than committed,
drivers should avoid making persistent state changes in
bind_port, or else must ensure that such state changes are
eventually cleaned up.
Implementing this method explicitly declares the mechanism
driver as having the intention to bind ports. This is inspected
by the QoS service to identify the available QoS rules you
can use with ports.
"""
port = context.current
network = context.network.current
# Run this before extracting lli, lli_from_port runs the same condition
# and will raise before this is checked
if not self._is_port_supported(port):
LOG.debug('Port {} has vnic_type set to {} which is not correct '
'to work with networking-ansible driver.'.format(
port['id'],
port[portbindings.VNIC_TYPE]))
return
switch_name, switch_port, segmentation_id = \
self._link_info_from_port(port, network)
LOG.debug('Plugging in port {switch_port} on '
'{switch_name} to vlan: {segmentation_id}'.format(
switch_port=switch_port,
switch_name=switch_name,
segmentation_id=segmentation_id))
provisioning_blocks.add_provisioning_component(
context._plugin_context, port['id'], resources.PORT,
c.NETWORKING_ENTITY)
self.ensure_port(port, context._plugin_context,
switch_name, switch_port,
network[provider_net.PHYSICAL_NETWORK], context)
def _link_info_from_port(self, port, network=None):
network = network or {}
local_link_info = self._get_port_lli(port)
if not local_link_info:
msg = 'local_link_information is missing in port {port_id} ' \
'binding:profile'.format(port_id=port['id'])
LOG.debug(msg)
raise exceptions.LocalLinkInfoMissingException(msg)
switch_mac = local_link_info[0].get('switch_id', '').upper()
switch_name = local_link_info[0].get('switch_info')
switch_port = local_link_info[0].get('port_id')
# fill in the switch name if mac exists but name is not defined
# this provides support for introspection when the switch's mac is
# also provided in the ML2 conf for ansible-networking
if not switch_name and switch_mac in self.ml2config.mac_map:
switch_name = self.ml2config.mac_map[switch_mac]
segmentation_id = network.get(provider_net.SEGMENTATION_ID, '')
return switch_name, switch_port, segmentation_id
def ensure_subports(self, port_id, db):
# set the correct state on port in the case where it has subports.
port = Port.get_object(db, id=port_id)
# If the parent port has been deleted then that delete will handle
# removing the trunked vlans on the switch using the mac
if not port:
LOG.debug('Discarding attempt to ensure subports on a port'
'that has been deleted')
return
# get switch from port bindings
switch_name, switch_port, segmentation_id = \
self._link_info_from_port(port, None)
# lock switch
lock = self.coordinator.get_lock(switch_name)
with lock:
# get updated port from db
updated_port = Port.get_object(db, id=port_id)
if updated_port:
self._set_port_state(updated_port, db)
return
else:
# port delete operation will take care of deletion
LOG.debug('Discarding attempt to ensure subports on a port {} '
'that was deleted after lock '
'acquisition'.format(port_id))
return
def ensure_port(self, port, db, switch_name,
switch_port, physnet, port_context):
LOG.debug('Ensuring state of port {port_id} '
'with mac addr {mac} '
'on switch {switch_name} '
'on port {switch_port} '
'on physnet {physnet} '
'using db context {db}'.format(port_id=port['id'],
mac=port['mac_address'],
switch_name=switch_name,
switch_port=switch_port,
physnet=physnet,
db=db))
if not self.net_runr.has_host(switch_name):
raise ml2_exc.MechanismDriverError('NetAnsible: couldnt find '
'switch_name {} in network '
'runner inventory while '
'configuring port id '
'{}'.format(switch_name,
port['id']))
# get dlock for the switch we're working with
lock = self.coordinator.get_lock(switch_name)
with lock:
# port = get the port from the db
updated_port = Port.get_object(db, id=port['id'])
# if it exists and is bound to a port
if self._get_port_lli(updated_port):
if self._set_port_state(updated_port, db):
if port_context and port_context.segments_to_bind:
segments = port_context.segments_to_bind
port_context.set_binding(segments[0][ml2api.ID],
portbindings.VIF_TYPE_OTHER,
{})
return
else:
# if the port doesn't exist, we have a mac+switch, we can look
# up whether the port needs to be deleted on the switch
if self._is_deleted_port_in_use(physnet,
port['mac_address'], db):
LOG.debug('Port {port_id} was deleted, but its switch port'
' {sp} is now in use by another port, discarding'
' request to delete'.format(port_id=port['id'],
sp=switch_port))
return
else:
self._delete_switch_port(switch_name, switch_port)
def _set_port_state(self, port, db):
if not port:
# error
raise ml2_exc.MechanismDriverError('Null port passed to '
'set_port_state')
# get switch name and port from port bindings
switch_name, switch_port, tmp = \
self._link_info_from_port(port, None)
if not switch_name or not switch_port:
# error/raise
raise ml2_exc.MechanismDriverError('NetAnsible: couldnt find '
'switch_name {} or switch port '
'{} to set port '
'state'.format(switch_name,
switch_port))
if not self.net_runr.has_host(switch_name):
raise ml2_exc.MechanismDriverError('NetAnsible: couldnt find '
'switch_name {} in '
'inventory'.format(switch_name))
network = Network.get_object(db, id=port.network_id)
if not network:
raise ml2_exc.MechanismDriverError('NetAnsible: couldnt find '
'network for port '
'{}'.format(port.id))
trunk = Trunk.get_object(db, port_id=port.id)
segmentation_id = network.segments[0].segmentation_id
# Assign port to network
try:
if trunk:
sub_ports = trunk.sub_ports
trunked_vlans = [sp.segmentation_id for sp in sub_ports]
self.net_runr.conf_trunk_port(switch_name,
switch_port,
segmentation_id,
trunked_vlans,
**self.kwargs[switch_name])
else:
self.net_runr.conf_access_port(
switch_name,
switch_port,
segmentation_id,
**self.kwargs[switch_name])
LOG.info('Port {neutron_port} has been plugged into '
'switch port {sp} on device {switch_name}'.format(
neutron_port=port.id,
sp=switch_port,
switch_name=switch_name))
return True
except Exception as e:
LOG.error('Failed to plug port {neutron_port} into '
'switch port: {sp} on device: {sw} '
'reason: {exc}'.format(
neutron_port=port.id,
sp=switch_port,
sw=switch_name,
exc=e))
raise exceptions.NetworkingAnsibleMechException(e)
def _delete_switch_port(self, switch_name, switch_port):
# we want to delete the physical port on the switch
# provided since it's no longer in use
LOG.debug('Unplugging port {switch_port} '
'on {switch_name}'.format(switch_port=switch_port,
switch_name=switch_name))
try:
self.net_runr.delete_port(switch_name,
switch_port,
**self.kwargs[switch_name])
LOG.info('Unplugged port {switch_port} '
'on {switch_name}'.format(switch_port=switch_port,
switch_name=switch_name))
except Exception as e:
LOG.error('Failed to uplug port {switch_port} '
'on {switch_name} '
'reason: {exc}'.format(
switch_port=switch_port,
switch_name=switch_name,
exc=e))
raise exceptions.NetworkingAnsibleMechException(e)
def _is_deleted_port_in_use(self, physnet, mac, db):
# Go through all ports with this mac addr and find which
# network segment they are on, which will contain physnet
# and net type
#
# if a port with this mac on the same physical provider
# is attached to this port then don't delete the interface.
# Don't need to check the segment since delete will remove all
# segments, and bind also deletes before adding
#
# These checks are required because a second tenant could
# set their mac address to the ironic port one and attempt
# meddle with the port delete. This wouldn't do anything
# bad today because bind deletes the interface and recreates
# it on the physical switch, but that's an implementation
# detail we shouldn't rely on.
# get port by mac
mports = Port.get_objects(db, mac_address=mac)
if len(mports) > 1:
# This isn't technically a problem, but it may indicate something
# fishy is going on
LOG.warn('multiple ports matching ironic '
'port mac: {mac}'.format(mac=mac))
for port in mports:
if port.bindings:
lli = self._get_port_lli(port)
if lli:
net = Network.get_object(db, id=port.network_id)
if net:
for seg in net.segments:
if seg.physical_network == physnet and \
seg.network_type == 'vlan':
return True
return False
@staticmethod
def _is_port_supported(port):
"""Return whether a port is supported by this driver.
:param port: The port to check
:returns: Whether the port is supported
Ports supported by this driver have a VNIC type of 'baremetal'.
"""
vnic_type = port[portbindings.VNIC_TYPE]
return vnic_type == portbindings.VNIC_BAREMETAL
@staticmethod
def _is_port_bound(port):
"""Return whether a port is bound by this driver.
Ports bound by this driver have their VIF type set to 'other'.
:param port: The port to check
:returns: Whether the port is bound
"""
if not AnsibleMechanismDriver._is_port_supported(port):
return False
vif_type = port[portbindings.VIF_TYPE]
return vif_type == portbindings.VIF_TYPE_OTHER
@staticmethod
def _get_port_lli(port):
"""Return the local link info for a port
:param port: The port to get link info from
:returns: The link info or None
"""
if not port:
return None
# Validate port and local link info
if isinstance(port, Port):
local_link_info = port.bindings[0].profile.get(c.LLI)
else:
if not AnsibleMechanismDriver._is_port_supported(port):
return None
local_link_info = port['binding:profile'].get(c.LLI)
return local_link_info