config/sysinv/sysinv/sysinv/sysinv/puppet/ovs.py
Thales Elero Cervi a5050c48fa Customize sysinv dpdk_elf_file for OVS-DPDK
As part of Debian migration, the sysinv procedure to check DPDK
compatibility for each host interface was also updated in order to make
it customizable in case one would like to use other virtual switch than
the delivered OVS with DPDK support [1].

For other virtual switches, that might or not rely on DPDK, the ELF
target that sysinv uses to verify interfaces compatibility must be
customizable and the query_pci_id script is already able to use custom
values [2].

This change adds the required logic on sysinv ovs puppet module such
that it is able to customize the hiera data with OVS-DPDK correct ELF
file. This is not strictly necessary, since it is configuring sysinv
with query_pci_id default ELF value, but would be an example for anyone
that wants to use a different virtual switch in the future and needs to
update the sysinv configuration likewise.

[1] https://review.opendev.org/c/starlingx/config/+/872979
[2] https://opendev.org/starlingx/config/src/branch/master/sysinv/sysinv/sysinv/scripts/query_pci_id#L34

Test Plan:
PASS - Build sysinv packages
PASS - Build a custom stx ISO with the new packages
PASS - Bootstrap AIO-SX virtual system (vswitch_type=none)
       and ensure the hiera data was not modified neither
       sysinv.conf was updated
PASS - Bootstrap AIO-SX virtual system (vswitch_type=ovs-dpdk)*
       and ensure the hiera data was modified correctly and
       sysinv.conf was updated accordingly
* A successful complete installation with ovs-dpdk is still blocked by
a bug that will be solved soon:
https://bugs.launchpad.net/starlingx/+bug/2008124

Story: 2010317
Task: 46389

Depends-On: https://review.opendev.org/c/starlingx/stx-puppet/+/887102

Signed-off-by: Thales Elero Cervi <thaleselero.cervi@windriver.com>
Change-Id: I4e33fb86199b3a0de7015aab44a66ef84138fca3
2023-06-29 20:16:43 -03:00

415 lines
14 KiB
Python

#
# Copyright (c) 2018-2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
from oslo_log import log
from sysinv.common import constants
from sysinv.common import utils
from sysinv.puppet import base
from sysinv.puppet import interface
LOG = log.getLogger(__name__)
OVS_DPDK_ELF = '/usr/sbin/ovs-vswitchd'
class OVSPuppet(base.BasePuppet):
"""Class to encapsulate puppet operations for vswitch configuration"""
def __init__(self, *args, **kwargs):
super(OVSPuppet, self).__init__(*args, **kwargs)
def get_system_config(self):
config = {}
if self._vswitch_type() == constants.VSWITCH_TYPE_OVS_DPDK:
config.update({
'sysinv::agent::dpdk_elf_file': OVS_DPDK_ELF,
})
return config
def get_host_config(self, host):
config = {}
if constants.WORKER in utils.get_personalities(host):
config.update(self._get_vswitch_enabled_config(host))
if self._vswitch_type() == constants.VSWITCH_TYPE_OVS_DPDK:
config.update(self._get_cpu_config(host))
config.update(self._get_memory_config(host))
config.update(self._get_port_config(host))
config.update(self._get_virtual_config(host))
config.update(self._get_lldp_config(host))
return config
def _get_port_config(self, host):
ovs_devices = {}
ovs_bridges = {}
ovs_ports = {}
ovs_addresses = {}
ovs_flows = {}
index = 0
for iface in sorted(self.context['interfaces'].values(),
key=interface.interface_sort_key):
if interface.is_data_network_type(iface):
# create a separate bridge for every configured data interface
brname = 'br-phy%d' % index
ovs_bridges[brname] = {}
# save the associated bridge for provider network mapping
iface['_ovs_bridge'] = brname
if iface['iftype'] == constants.INTERFACE_TYPE_ETHERNET:
port, devices = self._get_ethernet_port(
host, iface, brname, index)
elif iface['iftype'] == constants.INTERFACE_TYPE_AE:
port, devices = self._get_bond_port(
host, iface, brname, index)
elif iface['iftype'] == constants.INTERFACE_TYPE_VLAN:
port, devices = self._get_vlan_port(
host, iface, brname, index)
else:
raise Exception("unsupported interface type: %s" %
iface['iftype'])
ovs_ports.update({port['name']: port})
ovs_devices.update({d['pci_addr']: d for d in devices})
if iface['iftype'] == constants.INTERFACE_TYPE_ETHERNET:
ovs_ifname = port['interfaces'][0]['name']
lldp_port = self._get_lldp_port(
iface, brname, ovs_ifname=ovs_ifname)
ovs_ports.update({lldp_port['name']: lldp_port})
flow = self._get_lldp_flow(
brname, ovs_ifname, lldp_port['name'])
ovs_flows.update({port['name']: flow})
if iface['iftype'] == constants.INTERFACE_TYPE_AE:
slaves = interface.get_interface_slaves(
self.context, iface)
for member, slave in enumerate(slaves):
ovs_ifname = port['interfaces'][member]['name']
lldp_port = self._get_lldp_port(
slave, brname, ovs_ifname=ovs_ifname)
ovs_ports.update({lldp_port['name']: lldp_port})
flow = self._get_lldp_flow(
brname, ovs_ifname, lldp_port['name'])
ovs_flows.update({flow['name']: flow})
flow = self._get_lldp_flow(
brname, lldp_port['name'], ovs_ifname)
ovs_flows.update({flow['name']: flow})
index += 1
datanets = interface.get_interface_datanets(self.context, iface)
# setup tunnel address if assigned provider network is vxlan
if datanets and self._is_vxlan_datanet(datanets[0]):
address = interface.get_interface_primary_address(
self.context, iface)
if address:
ovs_addresses[brname] = {
'ifname': brname,
'address': address['address'],
'prefixlen': address['prefix'],
}
ovs_dict = {
'platform::vswitch::ovs::devices': ovs_devices,
'platform::vswitch::ovs::bridges': ovs_bridges,
'platform::vswitch::ovs::ports': ovs_ports,
'platform::vswitch::ovs::addresses': ovs_addresses,
'platform::vswitch::ovs::flows': ovs_flows,
}
LOG.debug("_get_port_config=%s" % ovs_dict)
return ovs_dict
def _get_ethernet_device(self, iface):
if interface.is_a_mellanox_device(self.context, iface):
# Mellanox devices are not bound to the DPDK driver
return None
port = interface.get_interface_port(self.context, iface)
pci_addr = self.quoted_str(port.pciaddr)
return {
'pci_addr': pci_addr
}
def _get_ethernet_interface(self, host, iface, ifname):
port = interface.get_interface_port(self.context, iface)
if interface.is_a_mellanox_device(self.context, iface):
# Mellanox devices use an ibverbs enumerated device name, therefore
# use the MAC address to identify the device.
device_name = "class=eth,mac=%s" % iface['imac']
else:
device_name = str(port.pciaddr)
rxq_count = len(self.context["_ovs_cpus"])
attributes = [
"options:dpdk-devargs=%s" % device_name,
"options:n_rxq=%d" % rxq_count,
"mtu_request=%d" % iface['imtu']
]
# TODO(mpeters): set other_config:pmd-rxq-affinity to pin receive
# queues to specific PMD cores
iftype = 'dpdk'
return {
'name': ifname,
'type': iftype,
'attributes': attributes,
}
def _get_ethernet_port(self, host, iface, bridge, index):
devices = []
interfaces = []
ifname = 'eth%d' % index
device = self._get_ethernet_device(iface)
if device:
devices.append(device)
interfaces.append(self._get_ethernet_interface(host, iface, ifname))
port = {
'name': ifname,
'bridge': bridge,
'interfaces': interfaces,
}
return port, devices
def _get_lldp_interface(self, ifname, peer_ifname):
attributes = []
iftype = 'internal'
attributes.append("other_config:lldp_phy_peer=%s" % peer_ifname)
return {
'name': ifname,
'type': iftype,
'attributes': attributes,
}
def _get_lldp_port(self, iface, lldp_brname, ovs_ifname=None):
interfaces = []
port = interface.get_interface_port(self.context, iface)
# Limit port name length to the maximum supported by ovs-ofctl to
# reference a port with a name rather than ofport number
# when creating flows.
port_name_len = constants.LLDP_OVS_PORT_NAME_LEN
uuid_len = port_name_len - len(constants.LLDP_OVS_PORT_PREFIX)
port_name = '{}{}'.format(constants.LLDP_OVS_PORT_PREFIX,
port.uuid[:uuid_len])
if ovs_ifname:
interfaces.append(self._get_lldp_interface(port_name, ovs_ifname))
else:
interfaces.append(self._get_lldp_interface(port_name, iface['name']))
port = {
'name': port_name,
'bridge': lldp_brname,
'interfaces': interfaces,
}
return port
def _get_lldp_flow(self, bridge, in_port, out_port):
actions = []
attributes = {
'idle_timeout': 0,
'hard_timeout': 0,
'in_port': in_port,
'dl_dst': constants.LLDP_MULTICAST_ADDRESS,
'dl_type': constants.LLDP_ETHER_TYPE
}
action = {
'type': 'output',
'value': out_port
}
actions.append(action)
flow = {
'name': '{}-{}-{}'.format(bridge, in_port, out_port),
'bridge': bridge,
'attributes': attributes,
'actions': actions
}
return flow
def _get_bond_port(self, host, iface, bridge, index):
devices = []
interfaces = []
attributes = []
ifname = 'bond%d' % index
# TODO(mpeters): OVS can support balance-tcp if interface txhashpolicy
# is set to layer3+4 (currently restricted at API for data interfaces)
ae_mode = iface['aemode']
if ae_mode in interface.ACTIVE_STANDBY_AE_MODES:
attributes.append("bond_mode=active-backup")
if ae_mode in interface.BALANCED_AE_MODES:
attributes.append("bond_mode=balance-slb")
elif ae_mode in interface.LACP_AE_MODES:
attributes.append("lacp=active")
attributes.append("bond_mode=balance-slb")
attributes.append("other_config:lacp-time=fast")
for member, lower_ifname in enumerate(iface['uses']):
lower_iface = self.context['interfaces'][lower_ifname]
member_ifname = '%s.%d' % (ifname, member)
device = self._get_ethernet_device(lower_iface)
if device:
devices.append(device)
interfaces.append(self._get_ethernet_interface(
host, lower_iface, member_ifname))
port = {
'type': 'bond',
'name': ifname,
'bridge': bridge,
'attributes': attributes,
'interfaces': interfaces,
}
return port, devices
def _get_vlan_port(self, host, iface, bridge, index):
devices = []
interfaces = []
ifname = 'vlan%d' % iface['vlan_id']
attributes = [
"tag=%d" % iface['vlan_id']
]
lower_iface = interface.get_lower_interface(self.context, iface)
device = self._get_ethernet_device(lower_iface)
if device:
devices.append(device)
interfaces.append(self._get_ethernet_interface(
host, lower_iface, ifname))
port = {
'name': ifname,
'bridge': bridge,
'attributes': attributes,
'interfaces': interfaces,
}
return port, devices
def _get_cpu_config(self, host):
platform_cpus = self._get_platform_cpu_list(host)
vswitch_cpus = self._get_vswitch_cpu_list(host)
host_cpus = platform_cpus[:1] + vswitch_cpus[:]
host_core_list = self.quoted_str(
','.join([str(c.cpu) for c in host_cpus]))
pmd_core_list = self.quoted_str(
','.join([str(c.cpu) for c in vswitch_cpus]))
# save the assigned CPUs for port assignment
self.context["_ovs_cpus"] = [c.cpu for c in vswitch_cpus]
return {
'vswitch::dpdk::host_core_list': host_core_list,
'vswitch::dpdk::pmd_core_list': pmd_core_list,
}
def _get_memory_config(self, host):
vswitch_memory = []
config = {}
vswitch_size = 0
host_memory = self.dbapi.imemory_get_by_ihost(host.id)
for memory in host_memory:
vswitch_size = memory.vswitch_hugepages_size_mib
vswitch_pages = memory.vswitch_hugepages_reqd \
if memory.vswitch_hugepages_reqd is not None \
else memory.vswitch_hugepages_nr
if vswitch_pages == 0:
vswitch_pages = memory.vswitch_hugepages_nr
vswitch_memory.append(str(vswitch_size * vswitch_pages))
dpdk_socket_mem = self.quoted_str(','.join(vswitch_memory))
config.update({
'vswitch::dpdk::socket_mem': dpdk_socket_mem
})
if vswitch_size == constants.MIB_2M:
config.update({
'platform::vswitch::params::hugepage_dir': '/mnt/huge-2048kB'
})
return config
def _get_virtual_config(self, host):
config = {}
if utils.is_virtual() or utils.is_virtual_worker(host):
config.update({
'platform::vswitch::params::iommu_enabled': False,
'platform::vswitch::params::hugepage_dir': '/mnt/huge-2048kB',
'openstack::neutron::params::tunnel_csum': True,
})
return config
def _get_vswitch_enabled_config(self, host):
host_labels = self.dbapi.label_get_by_host(host.id)
return {
'platform::vswitch::params::enabled':
utils.has_vswitch_enabled(host_labels, self.dbapi)
}
def _is_vxlan_datanet(self, datanet):
return datanet.get('network_type') == constants.DATANETWORK_TYPE_VXLAN
def _get_lldp_config(self, host):
driver_list = self.context['_lldp_drivers']
driver_list.append('ovs')
lldpd_options = []
# Disable broadcasting the kernel version
lldpd_kernel_option = {"option": "-k"}
lldpd_options.append(lldpd_kernel_option)
return {
'sysinv::agent::lldp_drivers': driver_list,
'platform::lldp::params::options': lldpd_options
}