3347cd311a
The standby controller would potentially fail to boot while attempting to PXE-boot when configured with a management LAG. This commit determines the interface with the lowest slave MAC address in an active_standby LAG configuration and assigns it as the primary interface. Story: 2002865 Task: 22814 Change-Id: I73f03cb9cb2fd632a1e64b4e7744e3be067318a4 Signed-off-by: Jack Ding <jack.ding@windriver.com>
1235 lines
44 KiB
Python
1235 lines
44 KiB
Python
#
|
|
# Copyright (c) 2017 Wind River Systems, Inc.
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
#
|
|
|
|
import collections
|
|
import copy
|
|
import six
|
|
import uuid
|
|
|
|
from netaddr import IPAddress
|
|
from netaddr import IPNetwork
|
|
|
|
from sysinv.common import constants
|
|
from sysinv.common import exception
|
|
from sysinv.common import utils
|
|
from sysinv.conductor import openstack
|
|
from sysinv.openstack.common import log
|
|
|
|
from . import base
|
|
|
|
|
|
LOG = log.getLogger(__name__)
|
|
|
|
PLATFORM_NETWORK_TYPES = [constants.NETWORK_TYPE_PXEBOOT,
|
|
constants.NETWORK_TYPE_MGMT,
|
|
constants.NETWORK_TYPE_INFRA,
|
|
constants.NETWORK_TYPE_OAM,
|
|
constants.NETWORK_TYPE_DATA_VRS, # For HP/Nuage
|
|
constants.NETWORK_TYPE_BM, # For internal use only
|
|
constants.NETWORK_TYPE_CONTROL]
|
|
|
|
DATA_NETWORK_TYPES = [constants.NETWORK_TYPE_DATA]
|
|
|
|
PCI_NETWORK_TYPES = [constants.NETWORK_TYPE_PCI_SRIOV,
|
|
constants.NETWORK_TYPE_PCI_PASSTHROUGH]
|
|
|
|
ACTIVE_STANDBY_AE_MODES = ['active_backup', 'active-backup', 'active_standby']
|
|
BALANCED_AE_MODES = ['balanced', 'balanced-xor']
|
|
LACP_AE_MODES = ['802.3ad']
|
|
|
|
DRIVER_MLX_CX3 = 'mlx4_core'
|
|
DRIVER_MLX_CX4 = 'mlx5_core'
|
|
|
|
MELLANOX_DRIVERS = [DRIVER_MLX_CX3,
|
|
DRIVER_MLX_CX4]
|
|
|
|
LOOPBACK_IFNAME = 'lo'
|
|
LOOPBACK_METHOD = 'loopback'
|
|
|
|
NETWORK_CONFIG_RESOURCE = 'platform::interfaces::network_config'
|
|
ROUTE_CONFIG_RESOURCE = 'platform::interfaces::route_config'
|
|
ADDRESS_CONFIG_RESOURCE = 'platform::addresses::address_config'
|
|
|
|
|
|
class InterfacePuppet(base.BasePuppet):
|
|
"""Class to encapsulate puppet operations for interface configuration"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(InterfacePuppet, self).__init__(*args, **kwargs)
|
|
self._openstack = None
|
|
|
|
@property
|
|
def openstack(self):
|
|
if not self._openstack:
|
|
self._openstack = openstack.OpenStackOperator(self.dbapi)
|
|
return self._openstack
|
|
|
|
def get_host_config(self, host):
|
|
"""
|
|
Generate the hiera data for the puppet network config and route config
|
|
resources for the host.
|
|
"""
|
|
|
|
# Normalize some of the host info into formats that are easier to
|
|
# use when parsing the interface list.
|
|
context = self._create_interface_context(host)
|
|
|
|
if host.personality == constants.CONTROLLER:
|
|
# Insert a fake BMC interface because BMC information is only
|
|
# stored on the host and in the global config. This makes it
|
|
# easier to setup the BMC interface from the interface handling
|
|
# code. Hopefully we can add real interfaces in the DB some day
|
|
# and remove this code.
|
|
self._create_bmc_interface(host, context)
|
|
|
|
# interface configuration is organized into sets of network_config,
|
|
# route_config and address_config resource hashes (dict)
|
|
config = {
|
|
NETWORK_CONFIG_RESOURCE: {},
|
|
ROUTE_CONFIG_RESOURCE: {},
|
|
ADDRESS_CONFIG_RESOURCE: {},
|
|
}
|
|
|
|
system = self._get_system()
|
|
# For AIO-SX subcloud, mgmt n/w will be on a separate
|
|
# physical interface instead of the loopback interface.
|
|
if system.system_mode != constants.SYSTEM_MODE_SIMPLEX or \
|
|
self._distributed_cloud_role() == \
|
|
constants.DISTRIBUTED_CLOUD_ROLE_SUBCLOUD:
|
|
# Setup the loopback interface first
|
|
generate_loopback_config(config)
|
|
|
|
# Generate the actual interface config resources
|
|
generate_interface_configs(context, config)
|
|
|
|
# Generate the actual interface config resources
|
|
generate_address_configs(context, config)
|
|
|
|
# Generate driver specific configuration
|
|
generate_driver_config(context, config)
|
|
|
|
# Generate the dhcp client configuration
|
|
generate_dhcp_config(context, config)
|
|
|
|
# Update the global context with generated interface context
|
|
self.context.update(context)
|
|
|
|
return config
|
|
|
|
def _create_interface_context(self, host):
|
|
context = {
|
|
'hostname': host.hostname,
|
|
'personality': host.personality,
|
|
'subfunctions': host.subfunctions,
|
|
'system_uuid': host.isystem_uuid,
|
|
'ports': self._get_port_interface_id_index(host),
|
|
'interfaces': self._get_interface_name_index(host),
|
|
'devices': self._get_port_pciaddr_index(host),
|
|
'addresses': self._get_address_interface_name_index(host),
|
|
'routes': self._get_routes_interface_name_index(host),
|
|
'networks': self._get_network_type_index(),
|
|
'gateways': self._get_gateway_index(),
|
|
'floatingips': self._get_floating_ip_index(),
|
|
'providernets': self._get_provider_networks(host),
|
|
}
|
|
return context
|
|
|
|
def _create_bmc_interface(self, host, context):
|
|
"""
|
|
Creates a fake BMC interface and inserts it into the context interface
|
|
list. It also creates a fake BMC address and inserts it into the
|
|
context address list. This is required because these two entities
|
|
exist only as attributes on the host and in local context variables.
|
|
Rather than have different code to generate manifest entries based on
|
|
these other data structures it is easier to create fake context entries
|
|
and re-use the existing code base.
|
|
"""
|
|
try:
|
|
network = self.dbapi.network_get_by_type(
|
|
constants.NETWORK_TYPE_BM)
|
|
except exception.NetworkTypeNotFound:
|
|
# No BMC network configured
|
|
return
|
|
|
|
lower_iface = _find_bmc_lower_interface(context)
|
|
if not lower_iface:
|
|
# No mgmt or pxeboot?
|
|
return
|
|
|
|
addr = self._get_address_by_name(host.hostname,
|
|
constants.NETWORK_TYPE_BM)
|
|
|
|
iface = {
|
|
'uuid': str(uuid.uuid4()),
|
|
'ifname': 'bmc0',
|
|
'iftype': constants.INTERFACE_TYPE_VLAN,
|
|
'networktype': constants.NETWORK_TYPE_BM,
|
|
'imtu': network.mtu,
|
|
'vlan_id': network.vlan_id,
|
|
'uses': [lower_iface['ifname']],
|
|
'used_by': []
|
|
}
|
|
|
|
lower_iface['used_by'] = ['bmc0']
|
|
address = {
|
|
'ifname': iface['ifname'],
|
|
'family': addr.family,
|
|
'prefix': addr.prefix,
|
|
'address': addr.address,
|
|
'networktype': iface['networktype']
|
|
}
|
|
|
|
context['interfaces'].update({iface['ifname']: iface})
|
|
context['addresses'].update({iface['ifname']: [address]})
|
|
|
|
def _find_host_interface(self, host, networktype):
|
|
"""
|
|
Search the host interface list looking for an interface with a given
|
|
primary network type.
|
|
"""
|
|
for iface in self.dbapi.iinterface_get_by_ihost(host.id):
|
|
if networktype == utils.get_primary_network_type(iface):
|
|
return iface
|
|
|
|
def _get_port_interface_id_index(self, host):
|
|
"""
|
|
Builds a dictionary of ports indexed by interface id.
|
|
"""
|
|
ports = {}
|
|
for port in self.dbapi.ethernet_port_get_by_host(host.id):
|
|
ports[port.interface_id] = port
|
|
return ports
|
|
|
|
def _get_interface_name_index(self, host):
|
|
"""
|
|
Builds a dictionary of interfaces indexed by interface name.
|
|
"""
|
|
interfaces = {}
|
|
for iface in self.dbapi.iinterface_get_by_ihost(host.id):
|
|
interfaces[iface.ifname] = iface
|
|
return interfaces
|
|
|
|
def _get_port_pciaddr_index(self, host):
|
|
"""
|
|
Builds a dictionary of port lists indexed by PCI address.
|
|
"""
|
|
devices = collections.defaultdict(list)
|
|
for port in self.dbapi.ethernet_port_get_by_host(host.id):
|
|
devices[port.pciaddr].append(port)
|
|
return devices
|
|
|
|
def _get_address_interface_name_index(self, host):
|
|
"""
|
|
Builds a dictionary of address lists indexed by interface name.
|
|
"""
|
|
addresses = collections.defaultdict(list)
|
|
for address in self.dbapi.addresses_get_by_host(host.id):
|
|
addresses[address.ifname].append(address)
|
|
return addresses
|
|
|
|
def _get_routes_interface_name_index(self, host):
|
|
"""
|
|
Builds a dictionary of route lists indexed by interface name.
|
|
"""
|
|
routes = collections.defaultdict(list)
|
|
for route in self.dbapi.routes_get_by_host(host.id):
|
|
routes[route.ifname].append(route)
|
|
|
|
results = collections.defaultdict(list)
|
|
for ifname, entries in six.iteritems(routes):
|
|
entries = sorted(entries, key=lambda r: r['prefix'], reverse=True)
|
|
results[ifname] = entries
|
|
return results
|
|
|
|
def _get_network_type_index(self):
|
|
networks = {}
|
|
for network in self.dbapi.networks_get_all():
|
|
networks[network['type']] = network
|
|
return networks
|
|
|
|
def _get_gateway_index(self):
|
|
"""
|
|
Builds a dictionary of gateway IP addresses indexed by network type.
|
|
"""
|
|
gateways = {}
|
|
try:
|
|
mgmt_address = self._get_address_by_name(
|
|
constants.CONTROLLER_GATEWAY, constants.NETWORK_TYPE_MGMT)
|
|
gateways.update({
|
|
constants.NETWORK_TYPE_MGMT: mgmt_address.address})
|
|
except exception.AddressNotFoundByName:
|
|
pass
|
|
|
|
try:
|
|
oam_address = self._get_address_by_name(
|
|
constants.CONTROLLER_GATEWAY, constants.NETWORK_TYPE_OAM)
|
|
gateways.update({
|
|
constants.NETWORK_TYPE_OAM: oam_address.address})
|
|
except exception.AddressNotFoundByName:
|
|
pass
|
|
|
|
return gateways
|
|
|
|
def _get_floating_ip_index(self):
|
|
"""
|
|
Builds a dictionary of floating ip addresses indexed by network type.
|
|
"""
|
|
mgmt_address = self._get_address_by_name(
|
|
constants.CONTROLLER_HOSTNAME, constants.NETWORK_TYPE_MGMT)
|
|
|
|
mgmt_floating_ip = (str(mgmt_address.address) + '/' +
|
|
str(mgmt_address.prefix))
|
|
|
|
floating_ips = {
|
|
constants.NETWORK_TYPE_MGMT: mgmt_floating_ip
|
|
}
|
|
|
|
try:
|
|
pxeboot_address = self._get_address_by_name(
|
|
constants.CONTROLLER_HOSTNAME, constants.NETWORK_TYPE_PXEBOOT)
|
|
|
|
pxeboot_floating_ip = (str(pxeboot_address.address) + '/' +
|
|
str(pxeboot_address.prefix))
|
|
|
|
floating_ips.update({
|
|
constants.NETWORK_TYPE_PXEBOOT: pxeboot_floating_ip,
|
|
})
|
|
except exception.AddressNotFoundByName:
|
|
pass
|
|
|
|
system = self._get_system()
|
|
if system.system_mode != constants.SYSTEM_MODE_SIMPLEX:
|
|
oam_address = self._get_address_by_name(
|
|
constants.CONTROLLER_HOSTNAME, constants.NETWORK_TYPE_OAM)
|
|
|
|
oam_floating_ip = (str(oam_address.address) + '/' +
|
|
str(oam_address.prefix))
|
|
|
|
floating_ips.update({
|
|
constants.NETWORK_TYPE_OAM: oam_floating_ip
|
|
})
|
|
|
|
return floating_ips
|
|
|
|
def _get_provider_networks(self, host):
|
|
# TODO(alegacy): this will not work as intended for upgrades of AIO-SX
|
|
# and -DX. The call to get_providernetworksdict will return an empty
|
|
# dictionary because the neutron endpoint is not available yet. Since
|
|
# we do not currently support SDN/OVS over upgrades we will need to
|
|
# deal with this in a later commit.
|
|
pnets = {}
|
|
if (self.openstack and
|
|
constants.COMPUTE in utils.get_personalities(host)):
|
|
pnets = self.openstack.get_providernetworksdict(quiet=True)
|
|
return pnets
|
|
|
|
|
|
def is_platform_network_type(iface):
|
|
networktype = utils.get_primary_network_type(iface)
|
|
return bool(networktype in PLATFORM_NETWORK_TYPES)
|
|
|
|
|
|
def is_data_network_type(iface):
|
|
networktypelist = utils.get_network_type_list(iface)
|
|
return bool(any(n in DATA_NETWORK_TYPES for n in networktypelist))
|
|
|
|
|
|
def _find_bmc_lower_interface(context):
|
|
"""
|
|
Search the profile interface list looking for either a pxeboot or mgmt
|
|
interface that can be used to attach a BMC VLAN interface. If a pxeboot
|
|
interface exists then it is preferred since we do not want to create a VLAN
|
|
over another VLAN interface.
|
|
"""
|
|
selected_iface = None
|
|
for ifname, iface in six.iteritems(context['interfaces']):
|
|
networktype = utils.get_primary_network_type(iface)
|
|
if networktype == constants.NETWORK_TYPE_PXEBOOT:
|
|
return iface
|
|
elif networktype == constants.NETWORK_TYPE_MGMT:
|
|
selected_iface = iface
|
|
return selected_iface
|
|
|
|
|
|
def is_controller(context):
|
|
"""
|
|
Determine we are creating a manifest for a controller node; regardless of
|
|
whether it has a compute subfunction or not.
|
|
"""
|
|
return bool(context['personality'] == constants.CONTROLLER)
|
|
|
|
|
|
def is_compute_subfunction(context):
|
|
"""
|
|
Determine if we are creating a manifest for a compute node or a compute
|
|
subfunction.
|
|
"""
|
|
if context['personality'] == constants.COMPUTE:
|
|
return True
|
|
if constants.COMPUTE in context['subfunctions']:
|
|
return True
|
|
return False
|
|
|
|
|
|
def is_pci_interface(iface):
|
|
"""
|
|
Determine if the interface is one of the PCI device types.
|
|
"""
|
|
networktype = utils.get_primary_network_type(iface)
|
|
return bool(networktype in PCI_NETWORK_TYPES)
|
|
|
|
|
|
def is_platform_interface(context, iface):
|
|
"""
|
|
Determine whether the interface needs to be configured in the linux kernel
|
|
as opposed to interfaces that exist purely in the vswitch. This includes
|
|
interfaces that are themselves platform interfaces or interfaces that have
|
|
platform interfaces above them. Both of these groups of interfaces require
|
|
a linux interface that will be used for platform purposes (i.e., pxeboot,
|
|
mgmt, infra, oam).
|
|
"""
|
|
if '_kernel' in iface: # check cached result
|
|
return iface['_kernel']
|
|
else:
|
|
kernel = False
|
|
if is_platform_network_type(iface):
|
|
kernel = True
|
|
else:
|
|
upper_ifnames = iface['used_by'] or []
|
|
for upper_ifname in upper_ifnames:
|
|
upper_iface = context['interfaces'][upper_ifname]
|
|
if is_platform_interface(context, upper_iface):
|
|
kernel = True
|
|
break
|
|
iface['_kernel'] = kernel # cache the result
|
|
return iface['_kernel']
|
|
|
|
|
|
def is_data_interface(context, iface):
|
|
"""
|
|
Determine whether the interface needs to be configured in the vswitch.
|
|
This includes interfaces that are themselves data interfaces or interfaces
|
|
that have data interfaces above them. Both of these groups of interfaces
|
|
require vswitch configuration data.
|
|
"""
|
|
if '_data' in iface: # check cached result
|
|
return iface['_data']
|
|
else:
|
|
data = False
|
|
if is_data_network_type(iface):
|
|
data = True
|
|
else:
|
|
upper_ifnames = iface['used_by'] or []
|
|
for upper_ifname in upper_ifnames:
|
|
upper_iface = context['interfaces'][upper_ifname]
|
|
if is_data_interface(context, upper_iface):
|
|
data = True
|
|
break
|
|
iface['_data'] = data # cache the result
|
|
return iface['_data']
|
|
|
|
|
|
def is_dpdk_compatible(context, iface):
|
|
"""
|
|
Determine whether an interface can be supported in vswitch as a native DPDK
|
|
interface. Since whether an interface is supported or not by the DPDK
|
|
means whether the DPDK has a hardware device driver for the underlying
|
|
physical device this also implies that all non-hardware related interfaces
|
|
are automatically supported in the DPDK. For this reason we report True
|
|
for VLAN and AE interfaces but check the DPDK support status for any
|
|
ethernet interfaces.
|
|
"""
|
|
if '_dpdksupport' in iface: # check the cached result
|
|
return iface['_dpdksupport']
|
|
elif iface['iftype'] == constants.INTERFACE_TYPE_ETHERNET:
|
|
port = get_interface_port(context, iface)
|
|
dpdksupport = port.get('dpdksupport', False)
|
|
else:
|
|
dpdksupport = True
|
|
iface['_dpdksupport'] = dpdksupport # cache the result
|
|
return iface['_dpdksupport']
|
|
|
|
|
|
def is_a_mellanox_device(context, iface):
|
|
"""
|
|
Determine if the underlying device is a Mellanox device.
|
|
"""
|
|
if iface['iftype'] != constants.INTERFACE_TYPE_ETHERNET:
|
|
# We only care about configuring specific settings for related ethernet
|
|
# devices.
|
|
return False
|
|
port = get_interface_port(context, iface)
|
|
if port['driver'] in MELLANOX_DRIVERS:
|
|
return True
|
|
return False
|
|
|
|
|
|
def is_a_mellanox_cx3_device(context, iface):
|
|
"""
|
|
Determine if the underlying device is a Mellanox CX3 device.
|
|
"""
|
|
if iface['iftype'] != constants.INTERFACE_TYPE_ETHERNET:
|
|
# We only care about configuring specific settings for related ethernet
|
|
# devices.
|
|
return False
|
|
port = get_interface_port(context, iface)
|
|
if port['driver'] == DRIVER_MLX_CX3:
|
|
return True
|
|
return False
|
|
|
|
|
|
def get_master_interface(context, iface):
|
|
"""
|
|
Get the interface name of the given interface's master (if any). The
|
|
master interface is the AE interface for any Ethernet interfaces.
|
|
"""
|
|
if '_master' not in iface: # check the cached result
|
|
master = None
|
|
if iface['iftype'] == constants.INTERFACE_TYPE_ETHERNET:
|
|
upper_ifnames = iface['used_by'] or []
|
|
for upper_ifname in upper_ifnames:
|
|
upper_iface = context['interfaces'][upper_ifname]
|
|
if upper_iface['iftype'] == constants.INTERFACE_TYPE_AE:
|
|
master = upper_iface['ifname']
|
|
break
|
|
iface['_master'] = master # cache the result
|
|
return iface['_master']
|
|
|
|
|
|
def is_slave_interface(context, iface):
|
|
"""
|
|
Determine if this interface is a slave interface. A slave interface is an
|
|
interface that is part of an AE interface.
|
|
"""
|
|
if '_slave' not in iface: # check the cached result
|
|
master = get_master_interface(context, iface)
|
|
iface['_slave'] = bool(master) # cache the result
|
|
return iface['_slave']
|
|
|
|
|
|
def get_interface_mtu(context, iface):
|
|
"""
|
|
Determine the MTU value to use for a given interface. We trust that sysinv
|
|
has selected the correct value.
|
|
"""
|
|
return iface['imtu']
|
|
|
|
|
|
def get_interface_providernets(iface):
|
|
"""
|
|
Return the provider networks of the supplied interface as a list.
|
|
"""
|
|
providernetworks = iface['providernetworks']
|
|
if not providernetworks:
|
|
return []
|
|
return [x.strip() for x in providernetworks.split(',')]
|
|
|
|
|
|
def get_interface_port(context, iface):
|
|
"""
|
|
Determine the port of the underlying device.
|
|
"""
|
|
assert iface['iftype'] == constants.INTERFACE_TYPE_ETHERNET
|
|
return context['ports'][iface['id']]
|
|
|
|
|
|
def get_interface_port_name(context, iface):
|
|
"""
|
|
Determine the port name of the underlying device.
|
|
"""
|
|
assert iface['iftype'] == constants.INTERFACE_TYPE_ETHERNET
|
|
port = get_interface_port(context, iface)
|
|
if port:
|
|
return port['name']
|
|
|
|
|
|
def get_lower_interface(context, iface):
|
|
"""
|
|
Return the interface object that is used to implement a VLAN interface.
|
|
"""
|
|
assert iface['iftype'] == constants.INTERFACE_TYPE_VLAN
|
|
lower_ifname = iface['uses'][0]
|
|
return context['interfaces'][lower_ifname]
|
|
|
|
|
|
def get_lower_interface_os_ifname(context, iface):
|
|
"""
|
|
Return the kernel interface name of the lower interface used to implement a
|
|
VLAN interface.
|
|
"""
|
|
lower_iface = get_lower_interface(context, iface)
|
|
return get_interface_os_ifname(context, lower_iface)
|
|
|
|
|
|
def get_interface_os_ifname(context, iface):
|
|
"""
|
|
Determine the interface name used in the linux kernel for the given
|
|
interface. Ethernet interfaces uses the original linux device name while
|
|
AE devices can use the user-defined named. VLAN interface must derive
|
|
their names based on their lower interface name.
|
|
"""
|
|
if '_os_ifname' in iface: # check cached result
|
|
return iface['_os_ifname']
|
|
else:
|
|
os_ifname = iface['ifname']
|
|
if iface['iftype'] == constants.INTERFACE_TYPE_ETHERNET:
|
|
os_ifname = get_interface_port_name(context, iface)
|
|
elif iface['iftype'] == constants.INTERFACE_TYPE_VLAN:
|
|
lower_os_ifname = get_lower_interface_os_ifname(context, iface)
|
|
os_ifname = lower_os_ifname + "." + str(iface['vlan_id'])
|
|
elif iface['iftype'] == constants.INTERFACE_TYPE_AE:
|
|
os_ifname = iface['ifname']
|
|
iface['_os_ifname'] = os_ifname # cache the result
|
|
return iface['_os_ifname']
|
|
|
|
|
|
def get_interface_routes(context, iface):
|
|
"""
|
|
Determine the list of routes that are applicable to a given interface (if
|
|
any).
|
|
"""
|
|
return context['routes'][iface['ifname']]
|
|
|
|
|
|
def get_network_speed(context, networktype):
|
|
if 'networks' in context:
|
|
network = context['networks'].get(networktype, None)
|
|
if network:
|
|
return network['link_capacity']
|
|
return 0
|
|
|
|
|
|
def _set_address_netmask(address):
|
|
"""
|
|
The netmask is not supplied by sysinv but is required by the puppet
|
|
resource class.
|
|
"""
|
|
network = IPNetwork(address['address'] + '/' + str(address['prefix']))
|
|
if network.version == 6:
|
|
address['netmask'] = str(network.prefixlen)
|
|
else:
|
|
address['netmask'] = str(network.netmask)
|
|
return address
|
|
|
|
|
|
def get_interface_primary_address(context, iface):
|
|
"""
|
|
Determine the primary IP address on an interface (if any). If multiple
|
|
addresses exist then the first address is returned.
|
|
"""
|
|
addresses = context['addresses'].get(iface['ifname'], [])
|
|
if len(addresses) > 0:
|
|
return _set_address_netmask(addresses[0])
|
|
|
|
|
|
def get_interface_address_family(context, iface):
|
|
"""
|
|
Determine the IP family/version of the interface primary address. If there
|
|
is no address then the IPv4 family identifier is returned so that an
|
|
appropriate default is always present in interface configurations.
|
|
"""
|
|
address = get_interface_primary_address(context, iface)
|
|
if not address:
|
|
return 'inet' # default to ipv4
|
|
elif IPAddress(address['address']).version == 4:
|
|
return 'inet'
|
|
else:
|
|
return 'inet6'
|
|
|
|
|
|
def get_interface_gateway_address(context, iface):
|
|
"""
|
|
Determine if the interface has a default gateway.
|
|
"""
|
|
networktype = utils.get_primary_network_type(iface)
|
|
return context['gateways'].get(networktype, None)
|
|
|
|
|
|
def get_interface_address_method(context, iface):
|
|
"""
|
|
Determine what type of interface to configure for each network type.
|
|
"""
|
|
networktype = utils.get_primary_network_type(iface)
|
|
if not networktype:
|
|
# Interfaces that are configured purely as a dependency from other
|
|
# interfaces (i.e., vlan lower interface, bridge member, bond slave)
|
|
# should be left as manual config
|
|
return 'manual'
|
|
elif networktype in DATA_NETWORK_TYPES:
|
|
# All data interfaces configured in the kernel because they are not
|
|
# natively supported in vswitch or need to be shared with the kernel
|
|
# because of a platform VLAN should be left as manual config
|
|
return 'manual'
|
|
elif networktype == constants.NETWORK_TYPE_CONTROL:
|
|
return 'static'
|
|
elif networktype == constants.NETWORK_TYPE_DATA_VRS:
|
|
# All HP/Nuage interfaces have their own IP address defined statically
|
|
return 'static'
|
|
elif networktype == constants.NETWORK_TYPE_BM:
|
|
return 'static'
|
|
elif networktype in PCI_NETWORK_TYPES:
|
|
return 'manual'
|
|
else:
|
|
if is_controller(context):
|
|
# All other interface types that exist on a controller are setup
|
|
# statically since the controller themselves run the DHCP server.
|
|
return 'static'
|
|
elif networktype == constants.NETWORK_TYPE_PXEBOOT:
|
|
# All pxeboot interfaces that exist on non-controller nodes are set
|
|
# to manual as they are not needed/used once the install is done.
|
|
# They exist only in support of the vlan mgmt interface above it.
|
|
return 'manual'
|
|
else:
|
|
# All other types get their addresses from the controller
|
|
return 'dhcp'
|
|
|
|
|
|
def get_interface_traffic_classifier(context, iface):
|
|
"""
|
|
Get the interface traffic classifier command line (if any)
|
|
"""
|
|
networktype = utils.get_primary_network_type(iface)
|
|
if networktype in [constants.NETWORK_TYPE_MGMT,
|
|
constants.NETWORK_TYPE_INFRA]:
|
|
networkspeed = get_network_speed(context, networktype)
|
|
return '/usr/local/bin/cgcs_tc_setup.sh %s %s %s > /dev/null' % (
|
|
get_interface_os_ifname(context, iface),
|
|
networktype,
|
|
networkspeed)
|
|
return None
|
|
|
|
|
|
def get_bridge_interface_name(context, iface):
|
|
"""
|
|
If the given interface is a bridge member then retrieve the bridge
|
|
interface name otherwise return None.
|
|
"""
|
|
if '_bridge' in iface: # check the cached result
|
|
return iface['_bridge']
|
|
else:
|
|
bridge = None
|
|
if (iface['iftype'] == constants.INTERFACE_TYPE_ETHERNET and
|
|
is_data_interface(context, iface) and
|
|
not is_dpdk_compatible(context, iface)):
|
|
bridge = 'br-' + get_interface_os_ifname(context, iface)
|
|
iface['_bridge'] = bridge # cache the result
|
|
return iface['_bridge']
|
|
|
|
|
|
def is_bridged_interface(context, iface):
|
|
"""
|
|
Determine if this interface is a member of a bridge. A interface is a
|
|
member of a bridge if the interface is a data interface that is not
|
|
accelerated (i.e., a slow data interface).
|
|
"""
|
|
if '_bridged' in iface: # check the cached result
|
|
return iface['_bridged']
|
|
else:
|
|
bridge = get_bridge_interface_name(context, iface)
|
|
iface['_bridged'] = bool(bridge) # cache the result
|
|
return iface['_bridged']
|
|
|
|
|
|
def needs_interface_config(context, iface):
|
|
"""
|
|
Determine whether an interface needs to be configured in the linux kernel.
|
|
This is true if the interface is a platform interface, is required by a
|
|
platform interface (i.e., an AE member, a VLAN lower interface), or is an
|
|
unaccelerated data interface.
|
|
"""
|
|
if is_platform_interface(context, iface):
|
|
return True
|
|
elif not is_compute_subfunction(context):
|
|
return False
|
|
elif is_data_interface(context, iface):
|
|
if not is_dpdk_compatible(context, iface):
|
|
# vswitch interfaces for devices that are not natively supported by
|
|
# the DPDK are created as regular Linux devices and then bridged in
|
|
# to vswitch in order for it to be able to use it indirectly.
|
|
return True
|
|
if is_a_mellanox_device(context, iface):
|
|
# Check for Mellanox data interfaces. We must set the MTU sizes of
|
|
# Mellanox data interfaces in case it is not the default. Normally
|
|
# data interfaces are owned by DPDK, they are not managed through
|
|
# Linux but in the Mellanox case, the interfaces are still visible
|
|
# in Linux so in case one needs to set jumbo frames, it has to be
|
|
# set in Linux as well. We only do this for combined nodes or
|
|
# non-controller nodes.
|
|
return True
|
|
elif is_pci_interface(iface):
|
|
return True
|
|
return False
|
|
|
|
|
|
def get_basic_network_config(ifname, ensure='present',
|
|
method='manual', onboot='true',
|
|
hotplug='false', family='inet',
|
|
mtu=None):
|
|
"""
|
|
Builds a basic network config dictionary with all of the fields required to
|
|
format a basic network_config puppet resource.
|
|
"""
|
|
config = {'ifname': ifname,
|
|
'ensure': ensure,
|
|
'family': family,
|
|
'method': method,
|
|
'hotplug': hotplug,
|
|
'onboot': onboot,
|
|
'options': {}}
|
|
if mtu:
|
|
config['mtu'] = str(mtu)
|
|
return config
|
|
|
|
|
|
def get_bridge_network_config(context, iface):
|
|
"""
|
|
Builds a network config dictionary for bridge interface resource.
|
|
"""
|
|
os_ifname = get_interface_os_ifname(context, iface)
|
|
os_ifname = 'br-' + os_ifname
|
|
method = get_interface_address_method(context, iface)
|
|
family = get_interface_address_family(context, iface)
|
|
config = get_basic_network_config(
|
|
os_ifname, method=method, family=family)
|
|
config['options']['TYPE'] = 'Bridge'
|
|
return config
|
|
|
|
|
|
def get_vlan_network_config(context, iface, config):
|
|
"""
|
|
Augments a basic config dictionary with the attributes specific to a VLAN
|
|
interface.
|
|
"""
|
|
options = {'VLAN': 'yes',
|
|
'pre_up': '/sbin/modprobe -q 8021q'}
|
|
config['options'].update(options)
|
|
return config
|
|
|
|
|
|
def get_bond_interface_options(iface, primary_iface):
|
|
"""
|
|
Get the interface config attribute for bonding options
|
|
"""
|
|
ae_mode = iface['aemode']
|
|
tx_hash_policy = iface['txhashpolicy']
|
|
options = None
|
|
if ae_mode in ACTIVE_STANDBY_AE_MODES:
|
|
# Requires the active device in an active_standby LAG
|
|
# configuration to be determined based on the lowest MAC address
|
|
options = 'mode=active-backup miimon=100 primary={}'.format(primary_iface['ifname'])
|
|
else:
|
|
options = 'xmit_hash_policy=%s miimon=100' % tx_hash_policy
|
|
if ae_mode in BALANCED_AE_MODES:
|
|
options = 'mode=balance-xor ' + options
|
|
elif ae_mode in LACP_AE_MODES:
|
|
options = 'mode=802.3ad lacp_rate=fast ' + options
|
|
return options
|
|
|
|
|
|
def get_bond_network_config(context, iface, config):
|
|
"""
|
|
Augments a basic config dictionary with the attributes specific to a bond
|
|
interface.
|
|
"""
|
|
options = {'MACADDR': iface['imac'].rstrip()}
|
|
primary_iface = get_primary_bond_interface(context, iface)
|
|
bonding_options = get_bond_interface_options(iface, primary_iface)
|
|
if bonding_options:
|
|
options['BONDING_OPTS'] = bonding_options
|
|
options['up'] = 'sleep 10'
|
|
config['options'].update(options)
|
|
return config
|
|
|
|
|
|
def get_primary_bond_interface(context, iface):
|
|
"""
|
|
Return the slave interface with the lowest MAC address
|
|
"""
|
|
slaves = get_bond_interface_slaves(context, iface)
|
|
sorted_slaves = sorted(slaves, key=slave_sort_key)
|
|
primary_iface = sorted_slaves[0]
|
|
return primary_iface
|
|
|
|
|
|
def get_bond_interface_slaves(context, iface):
|
|
"""
|
|
Return the slave interface objects for the corresponding
|
|
bond interface
|
|
"""
|
|
slaves = iface['uses']
|
|
ifaces = []
|
|
for ifname, iface in six.iteritems(context['interfaces']):
|
|
if ifname in slaves:
|
|
ifaces.append(iface)
|
|
return ifaces
|
|
|
|
|
|
def slave_sort_key(iface):
|
|
"""
|
|
Sort interfaces by lowest MAC address
|
|
"""
|
|
return int(iface['imac'].replace(':', ''), 16)
|
|
|
|
|
|
def get_ethernet_network_config(context, iface, config):
|
|
"""
|
|
Augments a basic config dictionary with the attributes specific to an
|
|
ethernet interface.
|
|
"""
|
|
networktype = utils.get_primary_network_type(iface)
|
|
options = {}
|
|
# Increased to accommodate devices that require more time to
|
|
# complete link auto-negotiation
|
|
options['LINKDELAY'] = '20'
|
|
if is_bridged_interface(context, iface):
|
|
options['BRIDGE'] = get_bridge_interface_name(context, iface)
|
|
elif is_slave_interface(context, iface):
|
|
options['SLAVE'] = 'yes'
|
|
options['MASTER'] = get_master_interface(context, iface)
|
|
options['PROMISC'] = 'yes'
|
|
elif networktype == constants.NETWORK_TYPE_PCI_SRIOV:
|
|
if not is_a_mellanox_cx3_device(context, iface):
|
|
# CX3 device can only use kernel module options to enable vfs
|
|
# others share the same pci-sriov sysfs enabling mechanism
|
|
sriovfs_path = ("/sys/class/net/%s/device/sriov_numvfs" %
|
|
get_interface_port_name(context, iface))
|
|
options['pre_up'] = "echo 0 > %s; echo %s > %s" % (
|
|
sriovfs_path, iface['sriov_numvfs'], sriovfs_path)
|
|
elif networktype == constants.NETWORK_TYPE_PCI_PASSTHROUGH:
|
|
sriovfs_path = ("/sys/class/net/%s/device/sriov_numvfs" %
|
|
get_interface_port_name(context, iface))
|
|
options['pre_up'] = "if [ -f %s ]; then echo 0 > %s; fi" % (
|
|
sriovfs_path, sriovfs_path)
|
|
|
|
config['options'].update(options)
|
|
return config
|
|
|
|
|
|
def get_route_config(route, ifname):
|
|
"""
|
|
Builds a basic route config dictionary with all of the fields required to
|
|
format a basic network_route puppet resource.
|
|
"""
|
|
if route['prefix']:
|
|
name = '%s/%s' % (route['network'], route['prefix'])
|
|
else:
|
|
name = 'default'
|
|
netmask = IPNetwork(route['network'] + "/" + str(route['prefix'])).netmask
|
|
config = {
|
|
'name': name,
|
|
'ensure': 'present',
|
|
'gateway': route['gateway'],
|
|
'interface': ifname,
|
|
'netmask': str(netmask) if route['prefix'] else '0.0.0.0',
|
|
'network': route['network'] if route['prefix'] else 'default',
|
|
'options': 'metric ' + str(route['metric'])
|
|
|
|
}
|
|
return config
|
|
|
|
|
|
def get_common_network_config(context, iface, config):
|
|
"""
|
|
Augments a basic config dictionary with the attributes specific to an upper
|
|
layer interface (i.e., an interface that is used to terminate IP traffic).
|
|
"""
|
|
traffic_classifier = get_interface_traffic_classifier(context, iface)
|
|
if traffic_classifier:
|
|
config['options']['post_up'] = traffic_classifier
|
|
|
|
method = get_interface_address_method(context, iface)
|
|
if method == 'static':
|
|
address = get_interface_primary_address(context, iface)
|
|
if address is None:
|
|
networktype = utils.get_primary_network_type(iface)
|
|
# control interfaces are not required to have an IP address
|
|
if networktype == constants.NETWORK_TYPE_CONTROL:
|
|
return config
|
|
LOG.error("Interface %s has no primary address" % iface['ifname'])
|
|
assert address is not None
|
|
config['ipaddress'] = address['address']
|
|
config['netmask'] = address['netmask']
|
|
|
|
gateway = get_interface_gateway_address(context, iface)
|
|
if gateway:
|
|
config['gateway'] = gateway
|
|
return config
|
|
|
|
|
|
def get_interface_network_config(context, iface):
|
|
"""
|
|
Builds a network_config resource dictionary for a given interface
|
|
"""
|
|
# Create a basic network config resource
|
|
os_ifname = get_interface_os_ifname(context, iface)
|
|
method = get_interface_address_method(context, iface)
|
|
family = get_interface_address_family(context, iface)
|
|
mtu = get_interface_mtu(context, iface)
|
|
config = get_basic_network_config(
|
|
os_ifname, method=method, family=family, mtu=mtu)
|
|
|
|
# Add options common to all top level interfaces
|
|
config = get_common_network_config(context, iface, config)
|
|
|
|
# Add type specific options
|
|
if iface['iftype'] == constants.INTERFACE_TYPE_VLAN:
|
|
config = get_vlan_network_config(context, iface, config)
|
|
elif iface['iftype'] == constants.INTERFACE_TYPE_AE:
|
|
config = get_bond_network_config(context, iface, config)
|
|
else:
|
|
config = get_ethernet_network_config(context, iface, config)
|
|
|
|
return config
|
|
|
|
|
|
def get_bridged_network_config(context, iface):
|
|
"""
|
|
Builds a pair of network_config resource dictionaries. One resource
|
|
represents the actual bridge interface that must be created when bridging a
|
|
physical interface to an avp-provider interface. The second interface is
|
|
the avp-provider network_config resource. It is assumed that the physical
|
|
interface network_config resource has already been created by the caller.
|
|
|
|
This is the hierarchy:
|
|
|
|
"eth0" -> "br-eth0" <- "eth0-avp"
|
|
|
|
This function creates "eth0-avp" and "br-eth0".
|
|
"""
|
|
# Create a config identical to the physical ethernet interface and change
|
|
# the name to the avp-provider interface name.
|
|
avp_config = get_interface_network_config(context, iface)
|
|
avp_config['ifname'] += '-avp'
|
|
|
|
# Create a bridge config that ties both interfaces together
|
|
bridge_config = get_bridge_network_config(context, iface)
|
|
|
|
return avp_config, bridge_config
|
|
|
|
|
|
def generate_network_config(context, config, iface):
|
|
"""
|
|
Produce the puppet network config resources necessary to configure the
|
|
given interface. In some cases this will emit a single network_config
|
|
resource, while in other cases it will emit multiple resources to create a
|
|
bridge, or to add additional route resources.
|
|
"""
|
|
network_config = get_interface_network_config(context, iface)
|
|
|
|
config[NETWORK_CONFIG_RESOURCE].update({
|
|
network_config['ifname']: format_network_config(network_config)
|
|
})
|
|
|
|
# Add additional configs for special interfaces
|
|
if is_bridged_interface(context, iface):
|
|
avp_config, bridge_config = get_bridged_network_config(context, iface)
|
|
config[NETWORK_CONFIG_RESOURCE].update({
|
|
avp_config['ifname']: format_network_config(avp_config),
|
|
bridge_config['ifname']: format_network_config(bridge_config),
|
|
})
|
|
|
|
# Add complementary puppet resource definitions (if needed)
|
|
for route in get_interface_routes(context, iface):
|
|
route_config = get_route_config(route, network_config['ifname'])
|
|
config[ROUTE_CONFIG_RESOURCE].update({
|
|
route_config['name']: route_config
|
|
})
|
|
|
|
|
|
def find_interface_by_type(context, networktype):
|
|
"""
|
|
Lookup an interface based on networktype. This is only intended for
|
|
platform interfaces that have only 1 such interface per node (i.e., oam,
|
|
mgmt, infra, pxeboot, bmc).
|
|
"""
|
|
for ifname, iface in six.iteritems(context['interfaces']):
|
|
if networktype == utils.get_primary_network_type(iface):
|
|
return iface
|
|
|
|
|
|
def find_address_by_type(context, networktype):
|
|
"""
|
|
Lookup an address based on networktype. This is only intended for for
|
|
types that only have 1 such address per node. For example, for SDN we
|
|
only expect/support a single data IP address per node because the SDN
|
|
controller cannot support more than 1.
|
|
"""
|
|
for ifname, addresses in six.iteritems(context['addresses']):
|
|
for address in addresses:
|
|
if address['networktype'] == networktype:
|
|
return address['address'], address['prefix']
|
|
return None, None
|
|
|
|
|
|
def find_sriov_interfaces_by_driver(context, driver):
|
|
"""
|
|
Lookup all interfaces based on port driver.
|
|
To be noted that this is only used for IFTYPE_ETHERNET
|
|
"""
|
|
ifaces = []
|
|
for ifname, iface in six.iteritems(context['interfaces']):
|
|
if iface['iftype'] != constants.INTERFACE_TYPE_ETHERNET:
|
|
continue
|
|
port = get_interface_port(context, iface)
|
|
networktype = utils.get_primary_network_type(iface)
|
|
if (port['driver'] == driver and
|
|
networktype == constants.NETWORK_TYPE_PCI_SRIOV):
|
|
ifaces.append(iface)
|
|
return ifaces
|
|
|
|
|
|
def count_interfaces_by_type(context, networktypes):
|
|
"""
|
|
Count the number of interfaces with a matching network type.
|
|
"""
|
|
for ifname, iface in six.iteritems(context['interfaces']):
|
|
networktypelist = utils.get_network_type_list(iface)
|
|
if any(n in networktypelist for n in networktypes):
|
|
return iface
|
|
|
|
|
|
def interface_sort_key(iface):
|
|
"""
|
|
Sort interfaces by interface type placing ethernet interfaces ahead of
|
|
aggregated ethernet interfaces, and vlan interfaces last.
|
|
"""
|
|
if iface['iftype'] == constants.INTERFACE_TYPE_ETHERNET:
|
|
return 0, iface['ifname']
|
|
elif iface['iftype'] == constants.INTERFACE_TYPE_AE:
|
|
return 1, iface['ifname']
|
|
else: # if iface['iftype'] == constants.INTERFACE_TYPE_VLAN:
|
|
return 2, iface['ifname']
|
|
|
|
|
|
def generate_interface_configs(context, config):
|
|
"""
|
|
Generate the puppet resource for each of the interface and route config
|
|
resources.
|
|
"""
|
|
for iface in sorted(context['interfaces'].values(),
|
|
key=interface_sort_key):
|
|
if needs_interface_config(context, iface):
|
|
generate_network_config(context, config, iface)
|
|
|
|
|
|
def get_address_config(context, iface, address):
|
|
ifname = get_interface_os_ifname(context, iface)
|
|
return {
|
|
'ifname': ifname,
|
|
'address': address,
|
|
}
|
|
|
|
|
|
def generate_address_configs(context, config):
|
|
"""
|
|
Generate the puppet resource for each of the floating IP addresses
|
|
"""
|
|
for networktype, address in six.iteritems(context['floatingips']):
|
|
iface = find_interface_by_type(context, networktype)
|
|
if iface:
|
|
address_config = get_address_config(context, iface, address)
|
|
config[ADDRESS_CONFIG_RESOURCE].update({
|
|
networktype: address_config
|
|
})
|
|
elif networktype == constants.NETWORK_TYPE_PXEBOOT:
|
|
# Fallback PXE boot address against mananagement interface
|
|
iface = find_interface_by_type(context,
|
|
constants.NETWORK_TYPE_MGMT)
|
|
if iface:
|
|
address_config = get_address_config(context, iface, address)
|
|
config[ADDRESS_CONFIG_RESOURCE].update({
|
|
networktype: address_config
|
|
})
|
|
|
|
|
|
def build_mlx4_num_vfs_options(context):
|
|
"""
|
|
Generate the manifest fragment that will create mlx4_core
|
|
modprobe conf file in which VF is set and reload the mlx4_core
|
|
kernel module
|
|
"""
|
|
ifaces = find_sriov_interfaces_by_driver(context, DRIVER_MLX_CX3)
|
|
if not ifaces:
|
|
return ""
|
|
|
|
num_vfs_options = ""
|
|
for iface in ifaces:
|
|
port = get_interface_port(context, iface)
|
|
# For CX3 SR-IOV configuration, we only configure VFs on the 1st port
|
|
# Since two ports share the same PCI address, if the first port has
|
|
# been configured, we need to skip the second port
|
|
if port['pciaddr'] in num_vfs_options:
|
|
continue
|
|
|
|
if not num_vfs_options:
|
|
num_vfs_options = "%s-%d;0;0" % (port['pciaddr'],
|
|
iface['sriov_numvfs'])
|
|
else:
|
|
num_vfs_options += ",%s-%d;0;0" % (port['pciaddr'],
|
|
iface['sriov_numvfs'])
|
|
|
|
return num_vfs_options
|
|
|
|
|
|
def generate_mlx4_core_options(context, config):
|
|
"""
|
|
Generate the config options that will create mlx4_core modprobe
|
|
conf file in which VF is set and execute mlx4_core_conf.sh in which
|
|
/var/run/.mlx4_cx3_reboot_required is created to indicate a reboot
|
|
is needed for goenable and /etc/modprobe.d/mlx4_sriov.conf is injected
|
|
into initramfs, this way mlx4_core options can be applied after reboot
|
|
"""
|
|
num_vfs_options = build_mlx4_num_vfs_options(context)
|
|
if not num_vfs_options:
|
|
return
|
|
|
|
mlx4_core_options = "port_type_array=2,2 num_vfs=%s" % num_vfs_options
|
|
config['platform::networking::mlx4_core_options'] = mlx4_core_options
|
|
|
|
|
|
def generate_driver_config(context, config):
|
|
"""
|
|
Generate custom configuration for driver specific parameters.
|
|
"""
|
|
if is_compute_subfunction(context):
|
|
generate_mlx4_core_options(context, config)
|
|
|
|
|
|
def generate_loopback_config(config):
|
|
"""
|
|
Generate the loopback network config resource so that the loopback
|
|
interface is automatically enabled on reboots.
|
|
"""
|
|
network_config = get_basic_network_config(LOOPBACK_IFNAME,
|
|
method=LOOPBACK_METHOD)
|
|
config[NETWORK_CONFIG_RESOURCE].update({
|
|
LOOPBACK_IFNAME: format_network_config(network_config)
|
|
})
|
|
|
|
|
|
def format_network_config(config):
|
|
"""
|
|
Converts a network_config resource dictionary to the equivalent puppet
|
|
resource definition parameters.
|
|
"""
|
|
network_config = copy.copy(config)
|
|
del network_config['ifname']
|
|
return network_config
|
|
|
|
|
|
def generate_dhcp_config(context, config):
|
|
"""
|
|
Generate the DHCP client configuration.
|
|
"""
|
|
if not is_controller(context):
|
|
infra_interface = find_interface_by_type(
|
|
context, constants.NETWORK_TYPE_INFRA)
|
|
if infra_interface:
|
|
infra_cid = utils.get_dhcp_cid(context['hostname'],
|
|
constants.NETWORK_TYPE_INFRA,
|
|
infra_interface['imac'])
|
|
config['platform::dhclient::params::infra_client_id'] = infra_cid
|