Add netconf-openconfig device driver
Add the initial part of a device driver using Netconf and OpenConfig models. Implements network create/delete/update and port create/delete/update. Also bump paramiko lower-constrain to 2.3.2, see: https://github.com/paramiko/paramiko/issues/1108 Story: 2009961 Task: 44996 Depends-On: https://review.opendev.org//837105 Change-Id: Ifc89923d7f6bbfba25feb2218b80fea9e27b9c4achanges/24/835324/24
parent
30b6008c17
commit
3b99ee3bc7
|
@ -13,13 +13,16 @@
|
|||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
import networking_baremetal.drivers.netconf.openconfig as netconf_openconfig
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
_opts = [
|
||||
cfg.ListOpt('enabled_devices',
|
||||
default=[],
|
||||
sample_default=['device.example.com'],
|
||||
sample_default=['generic.example.com',
|
||||
'netconf-openconfig.example.com'],
|
||||
help=('Enabled devices for which the plugin should manage'
|
||||
'configuration. Driver specific configuration for each '
|
||||
'device must be added in separate sections.')),
|
||||
|
@ -58,7 +61,10 @@ for device in CONF.networking_baremetal.enabled_devices:
|
|||
|
||||
def list_opts():
|
||||
return [('networking_baremetal', _opts),
|
||||
('device.example.com', _device_opts)]
|
||||
('generic.example.com', _device_opts),
|
||||
('netconf-openconfig.example.com',
|
||||
_device_opts + netconf_openconfig._DEVICE_OPTS
|
||||
+ netconf_openconfig._NCCLIENT_OPTS)]
|
||||
|
||||
|
||||
def get_devices():
|
||||
|
|
|
@ -0,0 +1,669 @@
|
|||
# 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 re
|
||||
from urllib.parse import parse_qs as urlparse_qs
|
||||
from urllib.parse import urlparse
|
||||
import uuid
|
||||
from xml.etree.ElementTree import fromstring as etree_fromstring
|
||||
|
||||
from ncclient import manager
|
||||
from ncclient.operations.rpc import RPCError
|
||||
from ncclient.transport.errors import AuthenticationError
|
||||
from ncclient.transport.errors import SessionCloseError
|
||||
from ncclient.transport.errors import SSHError
|
||||
from neutron_lib.api.definitions import portbindings
|
||||
from neutron_lib.api.definitions import provider_net
|
||||
from neutron_lib import constants as n_const
|
||||
from neutron_lib import exceptions as n_exec
|
||||
from neutron_lib.plugins.ml2 import api
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
import tenacity
|
||||
|
||||
from networking_baremetal import common
|
||||
from networking_baremetal import constants
|
||||
from networking_baremetal.constants import NetconfEditConfigOperation as nc_op
|
||||
from networking_baremetal.drivers import base
|
||||
from networking_baremetal import exceptions
|
||||
from networking_baremetal.openconfig.interfaces import interfaces
|
||||
from networking_baremetal.openconfig.network_instance import network_instance
|
||||
from networking_baremetal.openconfig.vlan import vlan
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
LOCK_DENIED_TAG = 'lock-denied' # [RFC 4741]
|
||||
CANDIDATE = 'candidate'
|
||||
RUNNING = 'running'
|
||||
|
||||
# Options for the device, maps to the local_link_information in the
|
||||
# port binding profile.
|
||||
_DEVICE_OPTS = [
|
||||
cfg.StrOpt('network_instance',
|
||||
default='default',
|
||||
advanced=True,
|
||||
help=('The L2, L3, or L2+L3 forwarding instance to use when '
|
||||
'defining VLANs on the device.')),
|
||||
cfg.DictOpt('port_id_re_sub',
|
||||
default={},
|
||||
sample_default={'pattern': 'Ethernet', 'repl': 'eth'},
|
||||
help=('Regular expression pattern and replacement string. '
|
||||
'Some devices do not use the port description from '
|
||||
'LLDP in Netconf configuration. If the regular '
|
||||
'expression pattern and replacement string is set the '
|
||||
'port_id will be modified before passing configuration '
|
||||
'to the device.')),
|
||||
cfg.ListOpt('disabled_properties',
|
||||
item_type=cfg.types.String(
|
||||
choices=['port_mtu']),
|
||||
default=[],
|
||||
help=('A list of properties that should not be used, '
|
||||
'currently only "port_mtu" is valid'))
|
||||
]
|
||||
|
||||
# Configuration option for Netconf client connection
|
||||
_NCCLIENT_OPTS = [
|
||||
cfg.StrOpt('host',
|
||||
help=('The hostname or IP address to use for connecting to the '
|
||||
'netconf device.'),
|
||||
sample_default='device.example.com'),
|
||||
cfg.StrOpt('username',
|
||||
help='The username to use for SSH authentication.',
|
||||
sample_default='netconf'),
|
||||
cfg.IntOpt('port', default=830,
|
||||
help=('The port to use for connection to the netconf '
|
||||
'device.')),
|
||||
cfg.StrOpt('password',
|
||||
help=('The password used if using password authentication, or '
|
||||
'the passphrase to use for unlocking keys that require '
|
||||
'it. (To disable attempting key authentication '
|
||||
'altogether, set options *allow_agent* and '
|
||||
'*look_for_keys* to `False`.'),
|
||||
sample_default='secret'),
|
||||
cfg.StrOpt('key_filename',
|
||||
help='Private key filename',
|
||||
default='~/.ssh/id_rsa'),
|
||||
cfg.BoolOpt('hostkey_verify',
|
||||
default=True,
|
||||
help=('Enables hostkey verification from '
|
||||
'~/.ssh/known_hosts')),
|
||||
cfg.DictOpt('device_params',
|
||||
default={'name': 'default'},
|
||||
help=('ncclient device handler parameters, see ncclient '
|
||||
'documentation for supported device handlers.')),
|
||||
cfg.BoolOpt('allow_agent',
|
||||
default=True,
|
||||
help='Enables querying SSH agent (if found) for keys.'),
|
||||
cfg.BoolOpt('look_for_keys',
|
||||
default=True,
|
||||
help=('Enables looking in the usual locations for ssh keys '
|
||||
'(e.g. :file:`~/.ssh/id_*`)')),
|
||||
]
|
||||
|
||||
|
||||
class NetconfLockDenied(n_exec.NeutronException):
|
||||
message = ('Access to the requested lock is denied because the'
|
||||
'lock is currently held by another entity.')
|
||||
|
||||
|
||||
class NetconfOpenConfigClient(base.BaseDeviceClient):
|
||||
|
||||
def __init__(self, device):
|
||||
super().__init__(device)
|
||||
self.device = device
|
||||
self.capabilities = set()
|
||||
|
||||
# Reduce the log level for ncclient, it is very chatty by default
|
||||
netconf_logger = logging.getLogger('ncclient')
|
||||
netconf_logger.setLevel(logging.WARNING)
|
||||
|
||||
@staticmethod
|
||||
def _get_lock_session_id(err_info):
|
||||
"""Parse lock-denied error [RFC6241]
|
||||
|
||||
error-tag: lock-denied
|
||||
error-type: protocol
|
||||
error-severity: error
|
||||
error-info: <session-id> : session ID of session holding the
|
||||
requested lock, or zero to indicate a non-NETCONF
|
||||
entity holds the lock
|
||||
Description: Access to the requested lock is denied because the
|
||||
lock is currently held by another entity.
|
||||
"""
|
||||
root = etree_fromstring(err_info)
|
||||
session_id = root.find(
|
||||
"./{urn:ietf:params:xml:ns:netconf:base:1.0}session-id").text
|
||||
|
||||
return session_id
|
||||
|
||||
@staticmethod
|
||||
def process_capabilities(server_capabilities):
|
||||
capabilities = set()
|
||||
for capability in server_capabilities:
|
||||
for k, v in constants.IANA_NETCONF_CAPABILITIES.items():
|
||||
if v in capability:
|
||||
capabilities.add(k)
|
||||
if capability.startswith('http://openconfig.net/yang'):
|
||||
openconfig_module = urlparse_qs(
|
||||
urlparse(capability).query).get('module').pop()
|
||||
capabilities.add(openconfig_module)
|
||||
|
||||
return capabilities
|
||||
|
||||
def get_capabilities(self):
|
||||
# https://github.com/ncclient/ncclient/issues/525
|
||||
_ignore_close_issue_525 = False
|
||||
args = self.get_client_args()
|
||||
try:
|
||||
with manager.connect(**args) as nc_client:
|
||||
server_capabilities = nc_client.server_capabilities
|
||||
_ignore_close_issue_525 = True
|
||||
except SessionCloseError as e:
|
||||
if not _ignore_close_issue_525:
|
||||
raise e
|
||||
except (SSHError, AuthenticationError) as e:
|
||||
raise exceptions.DeviceConnectionError(device=self.device, err=e)
|
||||
|
||||
return self.process_capabilities(server_capabilities)
|
||||
|
||||
def get_client_args(self):
|
||||
"""Get client connection arguments from configuration
|
||||
|
||||
:param device: Device identifier
|
||||
"""
|
||||
args = dict(
|
||||
host=CONF[self.device].host,
|
||||
port=CONF[self.device].port,
|
||||
username=CONF[self.device].username,
|
||||
hostkey_verify=CONF[self.device].hostkey_verify,
|
||||
device_params=CONF[self.device].device_params,
|
||||
keepalive=True,
|
||||
allow_agent=CONF[self.device].allow_agent,
|
||||
look_for_keys=CONF[self.device].look_for_keys,
|
||||
)
|
||||
if CONF[self.device].key_filename:
|
||||
args['key_filename'] = CONF[self.device].key_filename
|
||||
if CONF[self.device].password:
|
||||
args['password'] = CONF[self.device].password
|
||||
|
||||
return args
|
||||
|
||||
def get(self):
|
||||
"""Get current configuration/staate from device"""
|
||||
pass
|
||||
|
||||
@tenacity.retry(
|
||||
reraise=True,
|
||||
retry=tenacity.retry_if_exception_type(NetconfLockDenied),
|
||||
wait=tenacity.wait_random_exponential(multiplier=1, min=2, max=10),
|
||||
stop=tenacity.stop_after_attempt(5))
|
||||
def get_lock_and_configure(self, client, source, config):
|
||||
try:
|
||||
with client.locked(source):
|
||||
xml_config = common.config_to_xml(config)
|
||||
LOG.info(
|
||||
'Sending configuration to Netconf device %(dev)s: '
|
||||
'%(conf)s',
|
||||
{'dev': self.device, 'conf': xml_config})
|
||||
if source == CANDIDATE:
|
||||
# Revert the candidate configuration to the current
|
||||
# running configuration. Any uncommitted changes are
|
||||
# discarded.
|
||||
client.discard_changes()
|
||||
# Edit the candidate configuration
|
||||
client.edit_config(target=source, config=xml_config)
|
||||
# Validate the candidate configuration
|
||||
if (':validate' in self.capabilities
|
||||
or ':validate:1.1' in self.capabilities):
|
||||
client.validate(source='candidate')
|
||||
# Commit the candidate config, 30 seconds timeout
|
||||
if (':confirmed-commit' in self.capabilities
|
||||
or ':confirmed-commit:1.1' in self.capabilities):
|
||||
client.commit(confirmed=True, timeout=str(30))
|
||||
# Confirm the commit, if this commit does not
|
||||
# succeed the device will revert the config after
|
||||
# 30 seconds.
|
||||
client.commit()
|
||||
elif source == RUNNING:
|
||||
client.edit_config(target=source, config=xml_config)
|
||||
# TODO(hjensas): persist config.
|
||||
except RPCError as err:
|
||||
if err.tag == LOCK_DENIED_TAG:
|
||||
# If the candidate config is modified, some vendors do not
|
||||
# permit a new session to take a lock. This is per the RFC,
|
||||
# in this case a lock-denied error where session-id == 0 is
|
||||
# returned, because no session is actually holding the
|
||||
# lock we can discard changes which will release the lock.
|
||||
if (source == CANDIDATE
|
||||
and self._get_lock_session_id(err.info) == '0'):
|
||||
client.discard_changes()
|
||||
raise NetconfLockDenied()
|
||||
else:
|
||||
LOG.error('Netconf XML: %s', common.config_to_xml(config))
|
||||
raise err
|
||||
|
||||
def edit_config(self, config):
|
||||
"""Edit configuration on the device
|
||||
|
||||
:param config: Configuration, or list of configurations
|
||||
"""
|
||||
|
||||
# https://github.com/ncclient/ncclient/issues/525
|
||||
_ignore_close_issue_525 = False
|
||||
|
||||
if not isinstance(config, list):
|
||||
config = [config]
|
||||
|
||||
try:
|
||||
with manager.connect(**self.get_client_args()) as client:
|
||||
self.capabilities = self.process_capabilities(
|
||||
client.server_capabilities)
|
||||
if ':candidate' in self.capabilities:
|
||||
self.get_lock_and_configure(client, CANDIDATE, config)
|
||||
_ignore_close_issue_525 = True
|
||||
elif ':writable-running' in self.capabilities:
|
||||
self.get_lock_and_configure(client, RUNNING, config)
|
||||
_ignore_close_issue_525 = True
|
||||
except SessionCloseError as e:
|
||||
if not _ignore_close_issue_525:
|
||||
raise e
|
||||
|
||||
|
||||
class NetconfOpenConfigDriver(base.BaseDeviceDriver):
|
||||
|
||||
SUPPORTED_BOND_MODES = set().union(constants.NON_SWITCH_BOND_MODES)
|
||||
|
||||
def __init__(self, device):
|
||||
super().__init__(device)
|
||||
self.client = NetconfOpenConfigClient(device)
|
||||
self.device = device
|
||||
|
||||
def validate(self):
|
||||
try:
|
||||
LOG.info('Device %(device)s was loaded. Device capabilities: '
|
||||
'%(caps)s', {'device': self.device,
|
||||
'caps': self.client.get_capabilities()})
|
||||
except exceptions.DeviceConnectionError as e:
|
||||
raise exceptions.DriverValidationError(device=self.device, err=e)
|
||||
|
||||
def load_config(self):
|
||||
"""Register driver specific configuration"""
|
||||
|
||||
CONF.register_opts(_DEVICE_OPTS, group=self.device)
|
||||
CONF.register_opts(_NCCLIENT_OPTS, group=self.device)
|
||||
|
||||
def create_network(self, context):
|
||||
"""Create network on device
|
||||
|
||||
:param context: NetworkContext instance describing the new
|
||||
network.
|
||||
"""
|
||||
network = context.current
|
||||
segmentation_id = network[provider_net.SEGMENTATION_ID]
|
||||
|
||||
net_instances = network_instance.NetworkInstances()
|
||||
net_instance = net_instances.add(CONF[self.device].network_instance)
|
||||
_vlan = net_instance.vlans.add(segmentation_id)
|
||||
# Devices has limitations for vlan names, use the hex variant of the
|
||||
# network UUID which is shorter.
|
||||
_vlan.config.name = self._uuid_as_hex(network[api.ID])
|
||||
_vlan.config.status = constants.VLAN_ACTIVE
|
||||
self.client.edit_config(net_instances)
|
||||
|
||||
def update_network(self, context):
|
||||
"""Update network on device
|
||||
|
||||
:param context: NetworkContext instance describing the new
|
||||
network.
|
||||
"""
|
||||
network = context.current
|
||||
network_orig = context.original
|
||||
segmentation_id = network[provider_net.SEGMENTATION_ID]
|
||||
segmentation_id_orig = network_orig[provider_net.SEGMENTATION_ID]
|
||||
admin_state = network['admin_state_up']
|
||||
admin_state_orig = network_orig['admin_state_up']
|
||||
|
||||
add_net_instances = network_instance.NetworkInstances()
|
||||
add_net_instance = add_net_instances.add(
|
||||
CONF[self.device].network_instance)
|
||||
del_net_instances = None
|
||||
need_update = False
|
||||
|
||||
if segmentation_id:
|
||||
_vlan = add_net_instance.vlans.add(segmentation_id)
|
||||
# Devices has limitations for vlan names, use the hex variant of
|
||||
# the network UUID which is shorter.
|
||||
_vlan.config.name = self._uuid_as_hex(network[api.ID])
|
||||
if network['admin_state_up']:
|
||||
_vlan.config.status = constants.VLAN_ACTIVE
|
||||
else:
|
||||
_vlan.config.status = constants.VLAN_SUSPENDED
|
||||
if admin_state != admin_state_orig:
|
||||
need_update = True
|
||||
if segmentation_id_orig and segmentation_id != segmentation_id_orig:
|
||||
need_update = True
|
||||
del_net_instances = network_instance.NetworkInstances()
|
||||
del_net_instance = del_net_instances.add(
|
||||
CONF[self.device].network_instance)
|
||||
vlan_orig = del_net_instance.vlans.remove(segmentation_id_orig)
|
||||
# Not all devices support removing a VLAN, in that case lets
|
||||
# make sure the VLAN is suspended and set a name to indicate the
|
||||
# network was deleted.
|
||||
vlan_orig.config.name = f'neutron-DELETED-{segmentation_id_orig}'
|
||||
vlan_orig.config.status = constants.VLAN_SUSPENDED
|
||||
|
||||
if not need_update:
|
||||
return
|
||||
|
||||
# If the segmentation ID changed, delete the old VLAN first to avoid
|
||||
# vlan name conflict.
|
||||
if del_net_instances is not None:
|
||||
self.client.edit_config(del_net_instances)
|
||||
|
||||
self.client.edit_config(add_net_instances)
|
||||
|
||||
def delete_network(self, context):
|
||||
"""Delete network on device
|
||||
|
||||
:param context: NetworkContext instance describing the new
|
||||
network.
|
||||
"""
|
||||
network = context.current
|
||||
segmentation_id = network[provider_net.SEGMENTATION_ID]
|
||||
|
||||
net_instances = network_instance.NetworkInstances()
|
||||
net_instance = net_instances.add(CONF[self.device].network_instance)
|
||||
_vlan = net_instance.vlans.remove(segmentation_id)
|
||||
# Not all devices support removing a VLAN, in that case lets
|
||||
# make sure the VLAN is suspended and set a name to indicate the
|
||||
# network was deleted.
|
||||
_vlan.config.name = f'neutron-DELETED-{segmentation_id}'
|
||||
_vlan.config.status = constants.VLAN_SUSPENDED
|
||||
self.client.edit_config(net_instances)
|
||||
|
||||
def create_port(self, context, segment, links):
|
||||
"""Create/Configure port on device
|
||||
|
||||
:param context: PortContext instance describing the new
|
||||
state of the port, as well as the original state prior
|
||||
to the update_port call.
|
||||
:param segment: segment dictionary describing segment to bind
|
||||
:param links: Local link information filtered for the device.
|
||||
"""
|
||||
port = context.current
|
||||
binding_profile = port[portbindings.PROFILE]
|
||||
local_link_information = binding_profile.get(
|
||||
constants.LOCAL_LINK_INFO)
|
||||
local_group_information = binding_profile.get(
|
||||
constants.LOCAL_GROUP_INFO, {})
|
||||
bond_mode = local_group_information.get('bond_mode')
|
||||
|
||||
if segment[api.NETWORK_TYPE] != n_const.TYPE_VLAN:
|
||||
switched_vlan = None
|
||||
else:
|
||||
switched_vlan = vlan.VlanSwitchedVlan()
|
||||
switched_vlan.config.operation = nc_op.REPLACE
|
||||
switched_vlan.config.interface_mode = constants.VLAN_MODE_ACCESS
|
||||
switched_vlan.config.access_vlan = segment[api.SEGMENTATION_ID]
|
||||
|
||||
if not bond_mode or bond_mode in constants.NON_SWITCH_BOND_MODES:
|
||||
self.create_non_bond(context, switched_vlan, links)
|
||||
elif bond_mode in constants.LACP_BOND_MODES:
|
||||
if len(local_link_information) == len(links):
|
||||
self.create_lacp_aggregate(context, switched_vlan, links)
|
||||
else:
|
||||
# Some links is on a different device,
|
||||
# MLAG aggregate must be pre-configured.
|
||||
self.create_pre_conf_aggregate(context, switched_vlan, links)
|
||||
elif bond_mode in constants.PRE_CONF_ONLY_BOND_MODES:
|
||||
self.create_pre_conf_aggregate(context, switched_vlan, links)
|
||||
|
||||
def create_non_bond(self, context, switched_vlan, links):
|
||||
"""Create/Configure ports on device
|
||||
|
||||
:param context: PortContext instance describing the new
|
||||
state of the port, as well as the original state prior
|
||||
to the update_port call.
|
||||
:param switched_vlan: switched_vlan OpenConfig object
|
||||
:param links: Local link information filtered for the device.
|
||||
"""
|
||||
port = context.current
|
||||
network = context.network.current
|
||||
|
||||
ifaces = interfaces.Interfaces()
|
||||
for link in links:
|
||||
link_port_id = link.get(constants.PORT_ID)
|
||||
link_port_id = self._port_id_resub(link_port_id)
|
||||
|
||||
iface = ifaces.add(link_port_id)
|
||||
iface.config.enabled = port['admin_state_up']
|
||||
if 'port_mtu' not in CONF[self.device].disabled_properties:
|
||||
iface.config.mtu = network[api.MTU]
|
||||
iface.config.description = f'neutron-{port[api.ID]}'
|
||||
if switched_vlan is not None:
|
||||
iface.ethernet.switched_vlan = switched_vlan
|
||||
else:
|
||||
del iface.ethernet
|
||||
|
||||
self.client.edit_config(ifaces)
|
||||
|
||||
def create_lacp_aggregate(self, context, switched_vlan, links):
|
||||
"""Create/Configure LACP aggregate on device
|
||||
|
||||
:param context: PortContext instance describing the new
|
||||
state of the port, as well as the original state prior
|
||||
to the update_port call.
|
||||
:param switched_vlan: switched_vlan OpenConfig object
|
||||
:param links: Local link information filtered for the device.
|
||||
"""
|
||||
pass
|
||||
|
||||
def create_pre_conf_aggregate(self, context, switched_vlan, links):
|
||||
"""Create/Configure pre-configured aggregate on device
|
||||
|
||||
:param context: PortContext instance describing the new
|
||||
state of the port, as well as the original state prior
|
||||
to the update_port call.
|
||||
:param switched_vlan: switched_vlan OpenConfig object
|
||||
:param links: Local link information filtered for the device.
|
||||
"""
|
||||
pass
|
||||
|
||||
def update_port(self, context, links):
|
||||
"""Update port on device
|
||||
|
||||
:param context: PortContext instance describing the new
|
||||
state of the port, as well as the original state prior
|
||||
to the update_port call.
|
||||
:param links: Local link information filtered for the device.
|
||||
"""
|
||||
if (not self.admin_state_changed(context)
|
||||
and not self.network_mtu_changed(context)):
|
||||
return
|
||||
|
||||
port = context.current
|
||||
binding_profile = port[portbindings.PROFILE]
|
||||
local_link_information = binding_profile.get(
|
||||
constants.LOCAL_LINK_INFO)
|
||||
local_group_information = binding_profile.get(
|
||||
constants.LOCAL_GROUP_INFO, {})
|
||||
bond_mode = local_group_information.get('bond_mode')
|
||||
|
||||
if not bond_mode or bond_mode in constants.NON_SWITCH_BOND_MODES:
|
||||
self.update_non_bond(context, links)
|
||||
elif bond_mode in constants.LACP_BOND_MODES:
|
||||
if len(local_link_information) == len(links):
|
||||
self.update_lacp_aggregate(context, links)
|
||||
else:
|
||||
# Some links is on a different device,
|
||||
# MLAG aggregate must be pre-configured.
|
||||
self.update_pre_conf_aggregate(context, links)
|
||||
elif bond_mode in constants.PRE_CONF_ONLY_BOND_MODES:
|
||||
self.update_pre_conf_aggregate(context, links)
|
||||
|
||||
def update_non_bond(self, context, links):
|
||||
"""Update port on device
|
||||
|
||||
:param context: PortContext instance describing the new
|
||||
state of the port, as well as the original state prior
|
||||
to the update_port call.
|
||||
:param links: Local link information filtered for the device.
|
||||
"""
|
||||
network = context.network.current
|
||||
ifaces = interfaces.Interfaces()
|
||||
port = context.current
|
||||
|
||||
for link in links:
|
||||
link_port_id = link.get(constants.PORT_ID)
|
||||
link_port_id = self._port_id_resub(link_port_id)
|
||||
|
||||
iface = ifaces.add(link_port_id)
|
||||
iface.config.enabled = port['admin_state_up']
|
||||
if 'port_mtu' not in CONF[self.device].disabled_properties:
|
||||
iface.config.mtu = network[api.MTU]
|
||||
|
||||
del iface.ethernet
|
||||
|
||||
self.client.edit_config(ifaces)
|
||||
|
||||
def update_lacp_aggregate(self, context, links):
|
||||
"""Update LACP aggregate on device
|
||||
|
||||
:param context: PortContext instance describing the new
|
||||
state of the port, as well as the original state prior
|
||||
to the update_port call.
|
||||
:param links: Local link information filtered for the device.
|
||||
"""
|
||||
pass
|
||||
|
||||
def update_pre_conf_aggregate(self, context, links):
|
||||
"""Update pre-configured aggregate on device
|
||||
|
||||
:param context: PortContext instance describing the new
|
||||
state of the port, as well as the original state prior
|
||||
to the update_port call.
|
||||
:param links: Local link information filtered for the device.
|
||||
"""
|
||||
pass
|
||||
|
||||
def delete_port(self, context, links, current=True):
|
||||
"""Delete/Un-configure port on device
|
||||
|
||||
:param context: PortContext instance describing the new
|
||||
state of the port, as well as the original state prior
|
||||
to the update_port call.
|
||||
:param links: Local link information filtered for the device.
|
||||
:param current: Boolean, when true use context.current, when
|
||||
false use context.original
|
||||
"""
|
||||
port = context.current if current else context.original
|
||||
binding_profile = port[portbindings.PROFILE]
|
||||
local_link_information = binding_profile.get(
|
||||
constants.LOCAL_LINK_INFO)
|
||||
local_group_information = binding_profile.get(
|
||||
constants.LOCAL_GROUP_INFO, {})
|
||||
bond_mode = local_group_information.get('bond_mode')
|
||||
|
||||
if not bond_mode or bond_mode in constants.NON_SWITCH_BOND_MODES:
|
||||
self.delete_non_bond(context, links)
|
||||
elif bond_mode in constants.LACP_BOND_MODES:
|
||||
if len(local_link_information) == len(links):
|
||||
self.delete_lacp_aggregate(context, links)
|
||||
else:
|
||||
# Some links is on a different device,
|
||||
# MLAG aggregate must be pre-configured.
|
||||
self.delete_pre_conf_aggregate(links)
|
||||
elif bond_mode in constants.PRE_CONF_ONLY_BOND_MODES:
|
||||
self.delete_pre_conf_aggregate(links)
|
||||
|
||||
def delete_non_bond(self, context, links):
|
||||
"""Delete/Un-configure port on device
|
||||
|
||||
:param context: PortContext instance describing the new
|
||||
state of the port, as well as the original state prior
|
||||
to the update_port call.
|
||||
:param links: Local link information filtered for the device.
|
||||
"""
|
||||
network = context.network.current
|
||||
ifaces = interfaces.Interfaces()
|
||||
for link in links:
|
||||
link_port_id = link.get(constants.PORT_ID)
|
||||
link_port_id = self._port_id_resub(link_port_id)
|
||||
|
||||
iface = ifaces.add(link_port_id)
|
||||
iface.config.operation = nc_op.REMOVE
|
||||
# Not possible mark entire config for removal due to name leaf-ref
|
||||
# Set dummy values for properties to remove
|
||||
iface.config.description = ''
|
||||
iface.config.enabled = False
|
||||
if 'port_mtu' not in CONF[self.device].disabled_properties:
|
||||
iface.config.mtu = 0
|
||||
if network[provider_net.NETWORK_TYPE] == n_const.TYPE_VLAN:
|
||||
iface.ethernet.switched_vlan.config.operation = nc_op.REMOVE
|
||||
else:
|
||||
del iface.ethernet
|
||||
|
||||
self.client.edit_config(ifaces)
|
||||
|
||||
def delete_lacp_aggregate(self, context, links):
|
||||
"""Delete/Un-configure LACP aggregate on device
|
||||
|
||||
:param context: PortContext instance describing the new
|
||||
state of the port, as well as the original state prior
|
||||
to the update_port call.
|
||||
:param links: Local link information filtered for the device.
|
||||
"""
|
||||
pass
|
||||
|
||||
def delete_pre_conf_aggregate(self, links):
|
||||
"""Delete/Un-configure pre-configured aggregate on device
|
||||
|
||||
:param links: Local link information filtered for the device.
|
||||
"""
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _uuid_as_hex(_uuid):
|
||||
return uuid.UUID(_uuid).hex
|
||||
|
||||
def _port_id_resub(self, link_port_id):
|
||||
"""Replace pattern
|
||||
|
||||
Regular expression pattern and replacement string.
|
||||
Some devices don not use the port description from
|
||||
LLDP in Netconf configuration. If the regular expression
|
||||
pattern and replacement string is set the port_id will
|
||||
be modified before passing configuration to the device.
|
||||
|
||||
Replacing the leftmost non-overlapping occurrences of pattern
|
||||
in string by the replacement repl.
|
||||
"""
|
||||
if CONF[self.device].port_id_re_sub:
|
||||
pattern = CONF[self.device].port_id_re_sub.get('pattern')
|
||||
repl = CONF[self.device].port_id_re_sub.get('repl')
|
||||
link_port_id = re.sub(pattern, repl, link_port_id)
|
||||
|
||||
return link_port_id
|
||||
|
||||
@staticmethod
|
||||
def admin_state_changed(context):
|
||||
port = context.current
|
||||
port_orig = context.original
|
||||
return (port and port_orig
|
||||
and port['admin_state_up'] != port_orig['admin_state_up'])
|
||||
|
||||
@staticmethod
|
||||
def network_mtu_changed(context):
|
||||
network = context.network.current
|
||||
network_orig = context.network.original
|
||||
return (network and network_orig
|
||||
and network[api.MTU] != network_orig[api.MTU])
|
|
@ -0,0 +1,500 @@
|
|||
# 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 unittest import mock
|
||||
from xml.etree import ElementTree
|
||||
|
||||
from ncclient import manager
|
||||
from neutron.plugins.ml2 import driver_context
|
||||
from neutron_lib import constants as n_const
|
||||
from neutron_lib.plugins.ml2 import api
|
||||
from oslo_config import fixture as config_fixture
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
from networking_baremetal import config
|
||||
from networking_baremetal import constants
|
||||
from networking_baremetal.constants import NetconfEditConfigOperation as nc_op
|
||||
from networking_baremetal.drivers.netconf import openconfig
|
||||
from networking_baremetal.tests import base
|
||||
from networking_baremetal.tests.unit.plugins.ml2 import utils as ml2_utils
|
||||
|
||||
|
||||
class TestNetconfOpenConfigClient(base.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestNetconfOpenConfigClient, self).setUp()
|
||||
self.device = 'foo'
|
||||
self.conf = self.useFixture(config_fixture.Config())
|
||||
self.conf.register_opts(config._opts + config._device_opts,
|
||||
group='foo')
|
||||
self.conf.register_opts((openconfig._DEVICE_OPTS
|
||||
+ openconfig._NCCLIENT_OPTS), group='foo')
|
||||
self.conf.config(enabled_devices=['foo'],
|
||||
group='networking_baremetal')
|
||||
self.conf.config(driver='test-driver',
|
||||
switch_id='aa:bb:cc:dd:ee:ff',
|
||||
switch_info='foo',
|
||||
physical_networks=['fake_physical_network'],
|
||||
device_params={'name': 'default'},
|
||||
host='foo.example.com',
|
||||
key_filename='/test/test_key_file',
|
||||
username='foo_user',
|
||||
group='foo')
|
||||
|
||||
self.client = openconfig.NetconfOpenConfigClient(self.device)
|
||||
|
||||
def test_get_lock_session_id(self):
|
||||
err_info = (
|
||||
'<?xml version="1.0" encoding="UTF-8"?>'
|
||||
'<error-info xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">'
|
||||
'<session-id>{}</session-id>'
|
||||
'</error-info>')
|
||||
self.assertEqual('0', self.client._get_lock_session_id(
|
||||
err_info.format(0)))
|
||||
self.assertEqual('abc-123', self.client._get_lock_session_id(
|
||||
err_info.format('abc-123')))
|
||||
|
||||
def test_get_client_args(self):
|
||||
self.assertEqual(
|
||||
{'device_params': {'name': 'default'},
|
||||
'host': 'foo.example.com',
|
||||
'hostkey_verify': True,
|
||||
'keepalive': True,
|
||||
'key_filename': '/test/test_key_file',
|
||||
'port': 830,
|
||||
'username': 'foo_user',
|
||||
'allow_agent': True,
|
||||
'look_for_keys': True}, self.client.get_client_args())
|
||||
|
||||
@mock.patch.object(manager, 'connect', autospec=True)
|
||||
def test_get_capabilities(self, mock_manager):
|
||||
fake_caps = set(constants.IANA_NETCONF_CAPABILITIES.values())
|
||||
fake_caps.add('http://openconfig.net/yang/'
|
||||
'network-instance?'
|
||||
'module=openconfig-network-instance&'
|
||||
'revision=2021-07-22')
|
||||
fake_caps.add('http://openconfig.net/yang/'
|
||||
'interfaces?'
|
||||
'module=openconfig-interfaces&'
|
||||
'revision=2021-04-06')
|
||||
ncclient_mock = mock.Mock()
|
||||
ncclient_mock.server_capabilities = fake_caps
|
||||
mock_manager.return_value.__enter__.return_value = ncclient_mock
|
||||
self.assertEqual({
|
||||
':base:1.0', ':base:1.1', ':candidate', ':confirmed-commit',
|
||||
':confirmed-commit:1.1', ':rollback-on-error', ':startup',
|
||||
':validate', ':validate:1.1', ':writable-running',
|
||||
'openconfig-network-instance', 'openconfig-interfaces'},
|
||||
self.client.get_capabilities())
|
||||
|
||||
@mock.patch.object(manager, 'connect', autospec=True)
|
||||
@mock.patch.object(openconfig.NetconfOpenConfigClient,
|
||||
'get_lock_and_configure', autospec=True)
|
||||
def test_edit_config_writable_running(self, mock_lock_config,
|
||||
mock_manager):
|
||||
fake_config = mock.Mock()
|
||||
fake_config.to_xml_element.return_value = ElementTree.Element('fake')
|
||||
ncclient_mock = mock.Mock()
|
||||
fake_caps = {constants.IANA_NETCONF_CAPABILITIES[':writable-running']}
|
||||
ncclient_mock.server_capabilities = fake_caps
|
||||
mock_manager.return_value.__enter__.return_value = ncclient_mock
|
||||
self.client.edit_config(fake_config)
|
||||
mock_lock_config.assert_called_once_with(self.client, ncclient_mock,
|
||||
openconfig.RUNNING,
|
||||
[fake_config])
|
||||
|
||||
@mock.patch.object(manager, 'connect', autospec=True)
|
||||
@mock.patch.object(openconfig.NetconfOpenConfigClient,
|
||||
'get_lock_and_configure', autospec=True)
|
||||
def test_edit_config_candidate(self, mock_lock_config, mock_manager):
|
||||
fake_config = mock.Mock()
|
||||
fake_config.to_xml_element.return_value = ElementTree.Element('fake')
|
||||
ncclient_mock = mock.Mock()
|
||||
fake_caps = {constants.IANA_NETCONF_CAPABILITIES[':candidate']}
|
||||
ncclient_mock.server_capabilities = fake_caps
|
||||
mock_manager.return_value.__enter__.return_value = ncclient_mock
|
||||
self.client.edit_config(fake_config)
|
||||
mock_lock_config.assert_called_once_with(self.client, ncclient_mock,
|
||||
openconfig.CANDIDATE,
|
||||
[fake_config])
|
||||
|
||||
def test_get_lock_and_configure_confirmed_commit(self):
|
||||
self.client.capabilities = {':candidate', ':writable-running',
|
||||
':confirmed-commit'}
|
||||
fake_config = mock.Mock()
|
||||
fake_config.to_xml_element.return_value = ElementTree.Element('fake')
|
||||
mock_client = mock.MagicMock()
|
||||
self.client.get_lock_and_configure(mock_client, openconfig.CANDIDATE,
|
||||
[fake_config])
|
||||
mock_client.locked.assert_called_with(openconfig.CANDIDATE)
|
||||
mock_client.discard_changes.assert_called_once()
|
||||
mock_client.edit_config.assert_called_with(
|
||||
target=openconfig.CANDIDATE,
|
||||
config='<config><fake /></config>')
|
||||
mock_client.validate.assert_not_called()
|
||||
mock_client.commit.assert_has_calls([
|
||||
mock.call(confirmed=True, timeout=str(30)), mock.call()])
|
||||
|
||||
def test_get_lock_and_configure_validate(self):
|
||||
self.client.capabilities = {':candidate', ':writable-running',
|
||||
':validate'}
|
||||
fake_config = mock.Mock()
|
||||
fake_config.to_xml_element.return_value = ElementTree.Element('fake')
|
||||
mock_client = mock.MagicMock()
|
||||
self.client.get_lock_and_configure(mock_client, openconfig.CANDIDATE,
|
||||
[fake_config])
|
||||
mock_client.locked.assert_called_with(openconfig.CANDIDATE)
|
||||
mock_client.discard_changes.assert_called_once()
|
||||
mock_client.edit_config.assert_called_with(
|
||||
target=openconfig.CANDIDATE,
|
||||
config='<config><fake /></config>')
|
||||
mock_client.validate.assert_called_once_with(
|
||||
source=openconfig.CANDIDATE)
|
||||
mock_client.commit.assert_called_once_with()
|
||||
|
||||
def test_get_lock_and_configure_writeable_running(self):
|
||||
self.client.capabilities = {':writable-running'}
|
||||
fake_config = mock.Mock()
|
||||
fake_config.to_xml_element.return_value = ElementTree.Element('fake')
|
||||
mock_client = mock.MagicMock()
|
||||
self.client.get_lock_and_configure(mock_client, openconfig.RUNNING,
|
||||
[fake_config])
|
||||
mock_client.locked.assert_called_with(openconfig.RUNNING)
|
||||
mock_client.discard_changes.assert_not_called()
|
||||
mock_client.validate.assert_not_called()
|
||||
mock_client.commit.assert_not_called()
|
||||
mock_client.edit_config.assert_called_with(
|
||||
target=openconfig.RUNNING,
|
||||
config='<config><fake /></config>')
|
||||
|
||||
|
||||
class TestNetconfOpenConfigDriver(base.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestNetconfOpenConfigDriver, self).setUp()
|
||||
self.device = 'foo'
|
||||
self.conf = self.useFixture(config_fixture.Config())
|
||||
self.conf.register_opts(config._opts + config._device_opts,
|
||||
group='foo')
|
||||
self.conf.register_opts((openconfig._DEVICE_OPTS
|
||||
+ openconfig._NCCLIENT_OPTS), group='foo')
|
||||
self.conf.config(enabled_devices=['foo'],
|
||||
group='networking_baremetal')
|
||||
self.conf.config(driver='test-driver',
|
||||
switch_id='aa:bb:cc:dd:ee:ff',
|
||||
switch_info='foo',
|
||||
physical_networks=['fake_physical_network'],
|
||||
device_params={'name': 'default'},
|
||||
host='foo.example.com',
|
||||
key_filename='/test/test_key_file',
|
||||
username='foo_user',
|
||||
group='foo')
|
||||
mock_client = mock.patch.object(openconfig, 'NetconfOpenConfigClient',
|
||||
autospec=True)
|
||||
self.mock_client = mock_client.start()
|
||||
self.addCleanup(mock_client.stop)
|
||||
|
||||
self.driver = openconfig.NetconfOpenConfigDriver(self.device)
|
||||
self.mock_client.assert_called_once_with('foo')
|
||||
self.mock_client.reset_mock()
|
||||
|
||||
def test_validate(self):
|
||||
self.driver.validate()
|
||||
self.driver.client.get_capabilities.assert_called_once_with()
|
||||
|
||||
@mock.patch.object(openconfig, 'CONF', autospec=True)
|
||||
def test_load_config(self, mock_conf):
|
||||
self.driver.load_config()
|
||||
mock_conf.register_opts.assert_has_calls(
|
||||
[mock.call(openconfig._DEVICE_OPTS, group=self.driver.device),
|
||||
mock.call(openconfig._NCCLIENT_OPTS, group=self.driver.device)])
|
||||
|
||||
def test_create_network(self):
|
||||
m_nc = mock.create_autospec(driver_context.NetworkContext)
|
||||
m_nc.current = ml2_utils.get_test_network()
|
||||
self.driver.create_network(m_nc)
|
||||
net_instances = self.driver.client.edit_config.call_args[0][0]
|
||||
for net_instance in net_instances:
|
||||
self.assertEqual(net_instance.name, 'default')
|
||||
vlans = net_instance.vlans
|
||||
for vlan in vlans:
|
||||
self.assertEqual(vlan.config.operation, nc_op.MERGE.value)
|
||||
self.assertEqual(vlan.config.name,
|
||||
self.driver._uuid_as_hex(m_nc.current['id']))
|
||||
self.assertEqual(vlan.config.status, constants.VLAN_ACTIVE)
|
||||
|
||||
def test_update_network_no_changes(self):
|
||||
tenant_id = uuidutils.generate_uuid()
|
||||
network_id = uuidutils.generate_uuid()
|
||||
project_id = uuidutils.generate_uuid()
|
||||
m_nc = mock.create_autospec(driver_context.NetworkContext)
|
||||
m_nc.current = ml2_utils.get_test_network(
|
||||
id=network_id, tenant_id=tenant_id, project_id=project_id,
|
||||
network_type=n_const.TYPE_VLAN)
|
||||
m_nc.original = ml2_utils.get_test_network(
|
||||
id=network_id, tenant_id=tenant_id, project_id=project_id,
|
||||
network_type=n_const.TYPE_VLAN)
|
||||
self.assertEqual(m_nc.current, m_nc.original)
|
||||
self.driver.update_network(m_nc)
|
||||
self.driver.client.edit_config.assert_not_called()
|
||||
|
||||
def test_update_network_change_vlan_id(self):
|
||||
tenant_id = uuidutils.generate_uuid()
|
||||
network_id = uuidutils.generate_uuid()
|
||||
project_id = uuidutils.generate_uuid()
|
||||
m_nc = mock.create_autospec(driver_context.NetworkContext)
|
||||
m_nc.current = ml2_utils.get_test_network(
|
||||
id=network_id, tenant_id=tenant_id, project_id=project_id,
|
||||
network_type=n_const.TYPE_VLAN, segmentation_id=10)
|
||||
m_nc.original = ml2_utils.get_test_network(
|
||||
id=network_id, tenant_id=tenant_id, project_id=project_id,
|
||||
network_type=n_const.TYPE_VLAN, segmentation_id=20)
|
||||
self.driver.update_network(m_nc)
|
||||
call_args_list = self.driver.client.edit_config.call_args_list
|
||||
del_net_instances = call_args_list[0][0][0]
|
||||
add_net_instances = call_args_list[1][0][0]
|
||||
self.driver.client.edit_config.assert_has_calls(
|
||||
[mock.call(del_net_instances), mock.call(add_net_instances)])
|
||||
for net_instance in del_net_instances:
|
||||
self.assertEqual(net_instance.name, 'default')
|
||||
for vlan in net_instance.vlans:
|
||||
self.assertEqual(vlan.operation, nc_op.REMOVE.value)
|
||||
self.assertEqual(vlan.vlan_id, 20)
|
||||
self.assertEqual(vlan.config.status, constants.VLAN_SUSPENDED)
|
||||
self.assertEqual(vlan.config.name, 'neutron-DELETED-20')
|
||||
for net_instance in add_net_instances:
|
||||
self.assertEqual(net_instance.name, 'default')
|
||||
for vlan in net_instance.vlans:
|
||||
self.assertEqual(vlan.operation, nc_op.MERGE.value)
|
||||
self.assertEqual(vlan.config.name,
|
||||
self.driver._uuid_as_hex(network_id))
|
||||
self.assertEqual(vlan.vlan_id, 10)
|
||||
|
||||
def test_update_network_change_admin_state(self):
|
||||
tenant_id = uuidutils.generate_uuid()
|
||||
network_id = uuidutils.generate_uuid()
|
||||
project_id = uuidutils.generate_uuid()
|
||||
m_nc = mock.create_autospec(driver_context.NetworkContext)
|
||||
m_nc.current = ml2_utils.get_test_network(
|
||||
id=network_id, tenant_id=tenant_id, project_id=project_id,
|
||||
network_type=n_const.TYPE_VLAN, segmentation_id=10,
|
||||
admin_state_up=False)
|
||||
m_nc.original = ml2_utils.get_test_network(
|
||||
id=network_id, tenant_id=tenant_id, project_id=project_id,
|
||||
network_type=n_const.TYPE_VLAN, segmentation_id=10,
|
||||
admin_state_up=True)
|
||||
self.driver.update_network(m_nc)
|
||||
call_args_list = self.driver.client.edit_config.call_args_list
|
||||
add_net_instances = call_args_list[0][0][0]
|
||||
self.driver.client.edit_config.assert_called_once_with(
|
||||
add_net_instances)
|
||||
for net_instance in add_net_instances:
|
||||
self.assertEqual(net_instance.name, 'default')
|
||||
for vlan in net_instance.vlans:
|
||||
self.assertEqual(vlan.operation, nc_op.MERGE.value)
|
||||
self.assertEqual(vlan.config.status, constants.VLAN_SUSPENDED)
|
||||
self.assertEqual(vlan.config.name,
|
||||
self.driver._uuid_as_hex(network_id))
|
||||
self.assertEqual(vlan.vlan_id, 10)
|
||||
|
||||
def test_delete_network(self):
|
||||
m_nc = mock.create_autospec(driver_context.NetworkContext)
|
||||
m_nc.current = ml2_utils.get_test_network(
|
||||
network_type=n_const.TYPE_VLAN, segmentation_id=15)
|
||||
self.driver.delete_network(m_nc)
|
||||
self.driver.client.edit_config.assert_called_once()
|
||||
call_args_list = self.driver.client.edit_config.call_args_list
|
||||
net_instances = call_args_list[0][0][0]
|
||||
for net_instance in net_instances:
|
||||
self.assertEqual(net_instance.name, 'default')
|
||||
for vlan in net_instance.vlans:
|
||||
self.assertEqual(vlan.operation, nc_op.REMOVE.value)
|
||||
self.assertEqual(vlan.vlan_id, 15)
|
||||
self.assertEqual(vlan.config.status, constants.VLAN_SUSPENDED)
|
||||
self.assertEqual(vlan.config.name, 'neutron-DELETED-15')
|
||||
|
||||
def test_create_port_vlan(self):
|
||||
tenant_id = uuidutils.generate_uuid()
|
||||
network_id = uuidutils.generate_uuid()
|
||||
project_id = uuidutils.generate_uuid()
|
||||
m_nc = mock.create_autospec(driver_context.NetworkContext)
|
||||
m_pc = mock.create_autospec(driver_context.PortContext)
|
||||
m_nc.current = ml2_utils.get_test_network(
|
||||
id=network_id, tenant_id=tenant_id, project_id=project_id,
|
||||
network_type=n_const.TYPE_VLAN, segmentation_id=15)
|
||||
m_pc.current = ml2_utils.get_test_port(
|
||||
network_id=network_id, tenant_id=tenant_id, project_id=project_id)
|
||||
m_pc.network = m_nc
|
||||
segment = {
|
||||
api.ID: uuidutils.generate_uuid(),
|
||||
api.PHYSICAL_NETWORK:
|
||||
m_nc.current['provider:physical_network'],
|
||||
api.NETWORK_TYPE: m_nc.current['provider:network_type'],
|
||||
api.SEGMENTATION_ID: m_nc.current['provider:segmentation_id']}
|
||||
links = m_pc.current['binding:profile'][constants.LOCAL_LINK_INFO]
|
||||
self.driver.create_port(m_pc, segment, links)
|
||||
self.driver.client.edit_config.assert_called_once()
|
||||
call_args_list = self.driver.client.edit_config.call_args_list
|
||||
ifaces = call_args_list[0][0][0]
|
||||
for iface in ifaces:
|
||||
self.assertEqual(iface.name, links[0]['port_id'])
|
||||
self.assertEqual(iface.config.enabled,
|
||||
m_pc.current['admin_state_up'])
|
||||
self.assertEqual(iface.config.mtu, m_nc.current[api.MTU])
|
||||
self.assertEqual(iface.config.description,
|
||||
f'neutron-{m_pc.current[api.ID]}')
|
||||
self.assertEqual(iface.ethernet.switched_vlan.config.operation,
|
||||
nc_op.REPLACE.value)
|
||||
self.assertEqual(
|
||||
iface.ethernet.switched_vlan.config.interface_mode,
|
||||
constants.VLAN_MODE_ACCESS)
|
||||
self.assertEqual(
|
||||
iface.ethernet.switched_vlan.config.access_vlan,
|
||||
segment[api.SEGMENTATION_ID])
|
||||
|
||||
def test_create_port_flat(self):
|
||||
tenant_id = uuidutils.generate_uuid()
|
||||
network_id = uuidutils.generate_uuid()
|
||||
project_id = uuidutils.generate_uuid()
|
||||
m_nc = mock.create_autospec(driver_context.NetworkContext)
|
||||
m_pc = mock.create_autospec(driver_context.PortContext)
|
||||
m_nc.current = ml2_utils.get_test_network(
|
||||
id=network_id, tenant_id=tenant_id, project_id=project_id,
|
||||
network_type=n_const.TYPE_FLAT)
|
||||
m_pc.current = ml2_utils.get_test_port(
|
||||
network_id=network_id, tenant_id=tenant_id, project_id=project_id)
|
||||
m_pc.network = m_nc
|
||||
segment = {
|
||||
api.ID: uuidutils.generate_uuid(),
|
||||
api.PHYSICAL_NETWORK:
|
||||
m_nc.current['provider:physical_network'],
|
||||
api.NETWORK_TYPE: m_nc.current['provider:network_type']}
|
||||
links = m_pc.current['binding:profile'][constants.LOCAL_LINK_INFO]
|
||||
self.driver.create_port(m_pc, segment, links)
|
||||
self.driver.client.edit_config.assert_called_once()
|
||||
call_args_list = self.driver.client.edit_config.call_args_list
|
||||
ifaces = call_args_list[0][0][0]
|
||||
for iface in ifaces:
|
||||
self.assertEqual(iface.name, links[0]['port_id'])
|
||||
self.assertEqual(iface.config.enabled,
|
||||
m_pc.current['admin_state_up'])
|
||||
self.assertEqual(iface.config.mtu, m_nc.current[api.MTU])
|
||||
self.assertEqual(iface.config.description,
|
||||
f'neutron-{m_pc.current[api.ID]}')
|
||||
self.assertIsNone(iface.ethernet)
|
||||
|
||||
def test_update_port(self):
|
||||
tenant_id = uuidutils.generate_uuid()
|
||||
network_id = uuidutils.generate_uuid()
|
||||
project_id = uuidutils.generate_uuid()
|
||||
m_nc = mock.create_autospec(driver_context.NetworkContext)
|
||||
m_pc = mock.create_autospec(driver_context.PortContext)
|
||||
m_nc.current = ml2_utils.get_test_network(
|
||||
id=network_id, tenant_id=tenant_id, project_id=project_id,
|
||||
network_type=n_const.TYPE_VLAN, segmentation_id=15,
|
||||
mtu=9000)
|
||||
m_nc.original = ml2_utils.get_test_network(
|
||||
id=network_id, tenant_id=tenant_id, project_id=project_id,
|
||||
network_type=n_const.TYPE_VLAN, segmentation_id=15,
|
||||
mtu=1500)
|
||||
m_pc.current = ml2_utils.get_test_port(
|
||||
network_id=network_id, tenant_id=tenant_id, project_id=project_id,
|
||||
admin_state_up=False)
|
||||
m_pc.original = ml2_utils.get_test_port(
|
||||
network_id=network_id, tenant_id=tenant_id, project_id=project_id,
|
||||
admin_state_up=True)
|
||||
m_pc.network = m_nc
|
||||
links = m_pc.current['binding:profile'][constants.LOCAL_LINK_INFO]
|
||||
self.driver.update_port(m_pc, links)
|
||||
self.driver.client.edit_config.assert_called_once()
|
||||
call_args_list = self.driver.client.edit_config.call_args_list
|
||||
ifaces = call_args_list[0][0][0]
|
||||
for iface in ifaces:
|
||||
self.assertEqual(iface.name, links[0]['port_id'])
|
||||
self.assertEqual(iface.config.enabled,
|
||||
m_pc.current['admin_state_up'])
|
||||
self.assertEqual(iface.config.mtu, m_nc.current[api.MTU])
|
||||
self.assertIsNone(iface.ethernet)
|
||||
|
||||
def test_update_port_no_supported_attrib_changed(self):
|
||||
tenant_id = uuidutils.generate_uuid()
|
||||
network_id = uuidutils.generate_uuid()
|
||||
project_id = uuidutils.generate_uuid()
|
||||
m_nc = mock.create_autospec(driver_context.NetworkContext)
|
||||
m_pc = mock.create_autospec(driver_context.PortContext)
|
||||
m_nc.current = ml2_utils.get_test_network(
|
||||
id=network_id, tenant_id=tenant_id, project_id=project_id,
|
||||
network_type=n_const.TYPE_VLAN, segmentation_id=15)
|
||||
m_nc.original = ml2_utils.get_test_network(
|
||||
id=network_id, tenant_id=tenant_id, project_id=project_id,
|
||||
network_type=n_const.TYPE_VLAN, segmentation_id=15)
|
||||
m_pc.current = ml2_utils.get_test_port(
|
||||
network_id=network_id, tenant_id=tenant_id, project_id=project_id,
|
||||
name='current')
|
||||
m_pc.original = ml2_utils.get_test_port(
|
||||
network_id=network_id, tenant_id=tenant_id, project_id=project_id,
|
||||
name='original')
|
||||
m_pc.network = m_nc
|
||||
links = m_pc.current['binding:profile'][constants.LOCAL_LINK_INFO]
|
||||
self.driver.update_port(m_pc, links)
|
||||
self.driver.client.edit_config.assert_not_called()
|
||||
|
||||
def test_delete_port_vlan(self):
|
||||
tenant_id = uuidutils.generate_uuid()
|
||||
network_id = uuidutils.generate_uuid()
|
||||
project_id = uuidutils.generate_uuid()
|
||||
m_nc = mock.create_autospec(driver_context.NetworkContext)
|
||||
m_pc = mock.create_autospec(driver_context.PortContext)
|
||||
m_nc.current = ml2_utils.get_test_network(
|
||||
id=network_id, tenant_id=tenant_id, project_id=project_id,
|
||||
network_type=n_const.TYPE_VLAN, segmentation_id=15)
|
||||
m_pc.current = ml2_utils.get_test_port(
|
||||
network_id=network_id, tenant_id=tenant_id, project_id=project_id)
|
||||
m_pc.network = m_nc
|
||||
links = m_pc.current['binding:profile'][constants.LOCAL_LINK_INFO]
|
||||
self.driver.delete_port(m_pc, links)
|
||||
self.driver.client.edit_config.assert_called_once()
|
||||
call_args_list = self.driver.client.edit_config.call_args_list
|
||||
ifaces = call_args_list[0][0][0]
|
||||
for iface in ifaces:
|
||||
self.assertEqual(iface.name, links[0]['port_id'])
|
||||
self.assertEqual(iface.config.operation, nc_op.REMOVE.value)
|
||||
self.assertEqual(iface.config.description, '')
|
||||
self.assertFalse(iface.config.enabled)
|
||||
self.assertEqual(iface.config.mtu, 0)
|
||||
self.assertEqual(iface.ethernet.switched_vlan.config.operation,
|
||||
nc_op.REMOVE.value)
|
||||
|
||||
def test_delete_port_flat(self):
|
||||
tenant_id = uuidutils.generate_uuid()
|
||||
network_id = uuidutils.generate_uuid()
|
||||
project_id = uuidutils.generate_uuid()
|
||||
m_nc = mock.create_autospec(driver_context.NetworkContext)
|
||||
m_pc = mock.create_autospec(driver_context.PortContext)
|
||||
m_nc.current = ml2_utils.get_test_network(
|
||||
id=network_id, tenant_id=tenant_id, project_id=project_id,
|
||||
network_type=n_const.TYPE_FLAT)
|
||||
m_pc.current = ml2_utils.get_test_port(
|
||||
network_id=network_id, tenant_id=tenant_id, project_id=project_id)
|
||||
m_pc.network = m_nc
|
||||
links = m_pc.current['binding:profile'][constants.LOCAL_LINK_INFO]
|
||||
self.driver.delete_port(m_pc, links)
|
||||
self.driver.client.edit_config.assert_called_once()
|
||||
call_args_list = self.driver.client.edit_config.call_args_list
|
||||
ifaces = call_args_list[0][0][0]
|
||||
for iface in ifaces:
|
||||
self.assertEqual(iface.name, links[0]['port_id'])
|
||||
self.assertEqual(iface.config.operation, nc_op.REMOVE.value)
|
||||
self.assertEqual(iface.config.description, '')
|
||||
self.assertFalse(iface.config.enabled)
|
||||
self.assertEqual(iface.config.mtu, 0)
|
||||
self.assertIsNone(iface.ethernet)
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
features:
|
||||
- |
|
||||
Added an `OpenConfig <http://openconfig.net>`__ based device driver
|
||||
(driver name: ``netconf-openconfig``) using Network Configuration Protocol
|
||||
(**NETCONF**). Implements network create, delete and update functionality
|
||||
as well as port create, delete and update.
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
|
||||
ncclient>=0.6.9 # Apache-2.0
|
||||
neutron-lib>=1.28.0 # Apache-2.0
|
||||
oslo.config>=5.2.0 # Apache-2.0
|
||||
oslo.i18n>=3.15.3 # Apache-2.0
|
||||
|
|
|
@ -36,6 +36,9 @@ console_scripts =
|
|||
neutron.ml2.mechanism_drivers =
|
||||
baremetal = networking_baremetal.plugins.ml2.baremetal_mech:BaremetalMechanismDriver
|
||||
|
||||
networking_baremetal.drivers =
|
||||
netconf-openconfig = networking_baremetal.drivers.netconf.openconfig:NetconfOpenConfigDriver
|
||||
|
||||
[pbr]
|
||||
autodoc_index_modules = True
|
||||
api_doc_dir = contributor/api
|
||||
|
|
|
@ -8,3 +8,4 @@ python-subunit>=1.0.0 # Apache-2.0/BSD
|
|||
testtools>=2.2.0 # MIT
|
||||
stestr>=2.0.0 # Apache-2.0
|
||||
testscenarios>=0.4 # Apache-2.0/BSD
|
||||
ncclient>=0.6.9 # Apache-2.0
|
||||
|
|
3
tox.ini
3
tox.ini
|
@ -70,6 +70,9 @@ commands =
|
|||
sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
|
||||
|
||||
[testenv:genconfig]
|
||||
deps =
|
||||
-c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
commands =
|
||||
oslo-config-generator --config-file=tools/config/networking-baremetal-config-generator.conf
|
||||
|
||||
|
|
Loading…
Reference in New Issue