From bb1fcc4e0dbfa2e575570d75d209c5308be41f6d Mon Sep 17 00:00:00 2001 From: Saravanan KR Date: Tue, 5 Jul 2016 14:47:00 +0530 Subject: Add support for OVS DPDK Bridge and Port Add support in os-net-config for DPDK ports and OVS user bridges, and implement parameters which will be set by the TripleO Heat Templates when using TripleO. Implements: blueprint tripleo-ovs-dpdk Change-Id: Id4a23ced28b92a642c180a35c55080e5f4e2e05d --- etc/os-net-config/samples/ovs_dpdk.json | 20 ++++++ etc/os-net-config/samples/ovs_dpdk.yaml | 21 ++++++ os_net_config/__init__.py | 20 ++++++ os_net_config/impl_ifcfg.py | 54 +++++++++++++- os_net_config/objects.py | 122 ++++++++++++++++++++++++++++++++ os_net_config/tests/test_cli.py | 17 +++++ os_net_config/tests/test_impl_ifcfg.py | 30 ++++++++ os_net_config/utils.py | 42 +++++++++++ 8 files changed, 325 insertions(+), 1 deletion(-) create mode 100644 etc/os-net-config/samples/ovs_dpdk.json create mode 100644 etc/os-net-config/samples/ovs_dpdk.yaml diff --git a/etc/os-net-config/samples/ovs_dpdk.json b/etc/os-net-config/samples/ovs_dpdk.json new file mode 100644 index 0000000..dc6706d --- /dev/null +++ b/etc/os-net-config/samples/ovs_dpdk.json @@ -0,0 +1,20 @@ +{ "network_config": [ + { + "type": "ovs_user_bridge", + "name": "br-link", + "members": [ + { + "type": "ovs_dpdk_port", + "name": "dpdk0", + "driver": "igb_uio", + "members": [ + { + "type": "interface", + "name": "eth1", + } + ] + } + ] + } + ] +} diff --git a/etc/os-net-config/samples/ovs_dpdk.yaml b/etc/os-net-config/samples/ovs_dpdk.yaml new file mode 100644 index 0000000..18fee31 --- /dev/null +++ b/etc/os-net-config/samples/ovs_dpdk.yaml @@ -0,0 +1,21 @@ +# ovs_user_bridge type refers to the OVSUserBridge OVS ifup type, which will +# have the datapath type set as 'netdev' for DPDK processing. +# ovs_dpdk_port type refers to the OVSDPDKPort OVS ifup type, which will +# add the port to the bridge with the interface type as 'dpdk'. + +network_config: + - + type: ovs_user_bridge + name: br-link + members: + - + type: ovs_dpdk_port + # dpdk0 name is generated by dpdk drivers after dpdk_nic_bind + name: dpdk0 + # driver is optional argument, default driver is 'vfio-pci' + driver: igb_uio + members: + - type: interface + # nic style number does not work as of now. real interface name has + # to be provided here. + name: eth1 diff --git a/os_net_config/__init__.py b/os_net_config/__init__.py index 2aafc23..d307980 100644 --- a/os_net_config/__init__.py +++ b/os_net_config/__init__.py @@ -57,6 +57,10 @@ class NetConfig(object): self.add_bridge(obj) for member in obj.members: self.add_object(member) + elif isinstance(obj, objects.OvsUserBridge): + self.add_ovs_user_bridge(obj) + for member in obj.members: + self.add_object(member) elif isinstance(obj, objects.LinuxBridge): self.add_linux_bridge(obj) for member in obj.members: @@ -87,6 +91,8 @@ class NetConfig(object): self.add_ovs_patch_port(obj) elif isinstance(obj, objects.IbInterface): self.add_ib_interface(obj) + elif isinstance(obj, objects.OvsDpdkPort): + self.add_ovs_dpdk_port(obj) def add_interface(self, interface): """Add an Interface object to the net config object. @@ -109,6 +115,13 @@ class NetConfig(object): """ raise NotImplemented("add_bridge is not implemented.") + def add_ovs_user_bridge(self, bridge): + """Add an OvsUserBridge object to the net config object. + + :param bridge: The OvsUserBridge object to add. + """ + raise NotImplemented("add_ovs_user_bridge is not implemented.") + def add_linux_bridge(self, bridge): """Add a LinuxBridge object to the net config object. @@ -172,6 +185,13 @@ class NetConfig(object): """ raise NotImplemented("add_ib_interface is not implemented.") + def add_ovs_dpdk_port(self, ovs_dpdk_port): + """Add a OvsDpdkPort object to the net config object. + + :param ovs_dpdk_port: The OvsDpdkPort object to add. + """ + raise NotImplemented("add_ovs_dpdk_port is not implemented.") + def apply(self, cleanup=False): """Apply the network configuration. diff --git a/os_net_config/impl_ifcfg.py b/os_net_config/impl_ifcfg.py index d49b57b..c3d45bf 100644 --- a/os_net_config/impl_ifcfg.py +++ b/os_net_config/impl_ifcfg.py @@ -158,6 +158,19 @@ class IfcfgNetConfig(os_net_config.NetConfig): if base_opt.ovs_options: data += "OVS_OPTIONS=\"%s\"\n" % base_opt.ovs_options ovs_extra.extend(base_opt.ovs_extra) + elif isinstance(base_opt, objects.OvsUserBridge): + data += "DEVICETYPE=ovs\n" + data += "TYPE=OVSUserBridge\n" + if base_opt.use_dhcp: + data += "OVSBOOTPROTO=dhcp\n" + if base_opt.members: + members = [member.name for member in base_opt.members] + self.member_names[base_opt.name] = members + if base_opt.use_dhcp: + data += ("OVSDHCPINTERFACES=\"%s\"\n" % " ".join(members)) + if base_opt.ovs_options: + data += "OVS_OPTIONS=\"%s\"\n" % base_opt.ovs_options + ovs_extra.extend(base_opt.ovs_extra) elif isinstance(base_opt, objects.OvsBond): if base_opt.primary_interface_name: primary_name = base_opt.primary_interface_name @@ -224,6 +237,11 @@ class IfcfgNetConfig(os_net_config.NetConfig): data += "TYPE=OVSPatchPort\n" data += "OVS_BRIDGE=%s\n" % base_opt.bridge_name data += "OVS_PATCH_PEER=%s\n" % base_opt.peer + elif isinstance(base_opt, objects.OvsDpdkPort): + ovs_extra.extend(base_opt.ovs_extra) + data += "DEVICETYPE=ovs\n" + data += "TYPE=OVSDPDKPort\n" + data += "OVS_BRIDGE=%s\n" % base_opt.bridge_name else: if base_opt.use_dhcp: data += "BOOTPROTO=dhcp\n" @@ -369,6 +387,18 @@ class IfcfgNetConfig(os_net_config.NetConfig): if bridge.routes: self._add_routes(bridge.name, bridge.routes) + def add_ovs_user_bridge(self, bridge): + """Add an OvsUserBridge object to the net config object. + + :param bridge: The OvsUserBridge object to add. + """ + logger.info('adding ovs user bridge: %s' % bridge.name) + data = self._add_common(bridge) + logger.debug('ovs user bridge data: %s' % data) + self.bridge_data[bridge.name] = data + if bridge.routes: + self._add_routes(bridge.name, bridge.routes) + def add_linux_bridge(self, bridge): """Add a LinuxBridge object to the net config object. @@ -476,6 +506,24 @@ class IfcfgNetConfig(os_net_config.NetConfig): % (ib_interface.hwname, ib_interface.name)) self.renamed_interfaces[ib_interface.hwname] = ib_interface.name + def add_ovs_dpdk_port(self, ovs_dpdk_port): + """Add a OvsDpdkPort object to the net config object. + + :param ovs_dpdk_port: The OvsDpdkPort object to add. + """ + logger.info('adding ovs dpdk port: %s' % ovs_dpdk_port.name) + + # DPDK Port will have only one member of type Interface, validation + # checks are added at the object creation stage. + ifname = ovs_dpdk_port.members[0].name + + # Bind the dpdk interface + utils.bind_dpdk_interfaces(ifname, ovs_dpdk_port.driver, self.noop) + + data = self._add_common(ovs_dpdk_port) + logger.debug('ovs dpdk port data: %s' % data) + self.interface_data[ovs_dpdk_port.name] = data + def generate_ivs_config(self, ivs_uplinks, ivs_interfaces): """Generate configuration content for ivs.""" @@ -648,7 +696,11 @@ class IfcfgNetConfig(os_net_config.NetConfig): utils.diff(br_route_path, route_data) or utils.diff(br_route6_path, route6_data)): restart_bridges.append(bridge_name) - restart_interfaces.extend(self.child_members(bridge_name)) + # Avoid duplicate interface being added to the restart list + children = self.child_members(bridge_name) + for child in children: + if child not in restart_interfaces: + restart_interfaces.append(child) update_files[bridge_path] = bridge_data update_files[br_route_path] = route_data update_files[br_route6_path] = route6_data diff --git a/os_net_config/objects.py b/os_net_config/objects.py index 9b5523f..725df38 100644 --- a/os_net_config/objects.py +++ b/os_net_config/objects.py @@ -38,6 +38,8 @@ def object_from_json(json): return Vlan.from_json(json) elif obj_type == "ovs_bridge": return OvsBridge.from_json(json) + elif obj_type == "ovs_user_bridge": + return OvsUserBridge.from_json(json) elif obj_type == "ovs_bond": return OvsBond.from_json(json) elif obj_type == "linux_bond": @@ -60,6 +62,8 @@ def object_from_json(json): return OvsPatchPort.from_json(json) elif obj_type == "ib_interface": return IbInterface.from_json(json) + elif obj_type == "ovs_dpdk_port": + return OvsDpdkPort.from_json(json) def _get_required_field(json, name, object_name): @@ -435,6 +439,65 @@ class OvsBridge(_BaseOpts): dhclient_args=dhclient_args, dns_servers=dns_servers) +class OvsUserBridge(_BaseOpts): + """Base class for OVS User bridges.""" + + def __init__(self, name, use_dhcp=False, use_dhcpv6=False, addresses=None, + routes=None, mtu=None, members=None, ovs_options=None, + ovs_extra=None, nic_mapping=None, persist_mapping=False, + defroute=True, dhclient_args=None, dns_servers=None): + super(OvsUserBridge, self).__init__(name, use_dhcp, use_dhcpv6, + addresses, routes, mtu, False, + nic_mapping, persist_mapping, + defroute, dhclient_args, + dns_servers) + self.members = members or [] + self.ovs_options = ovs_options + self.ovs_extra = ovs_extra or [] + for member in self.members: + member.bridge_name = name + if not isinstance(member, OvsTunnel) and \ + not isinstance(member, OvsDpdkPort): + member.ovs_port = True + if member.primary: + if self.primary_interface_name: + msg = 'Only one primary interface allowed per bridge.' + raise InvalidConfigException(msg) + if member.primary_interface_name: + self.primary_interface_name = member.primary_interface_name + else: + self.primary_interface_name = member.name + + @staticmethod + def from_json(json): + name = _get_required_field(json, 'name', 'OvsUserBridge') + (use_dhcp, use_dhcpv6, addresses, routes, mtu, nic_mapping, + persist_mapping, defroute, + dhclient_args, dns_servers) = _BaseOpts.base_opts_from_json( + json, include_primary=False) + ovs_options = json.get('ovs_options') + ovs_extra = json.get('ovs_extra', []) + members = [] + + # members + members_json = json.get('members') + if members_json: + if isinstance(members_json, list): + for member in members_json: + members.append(object_from_json(member)) + else: + msg = 'Members must be a list.' + raise InvalidConfigException(msg) + + return OvsUserBridge(name, use_dhcp=use_dhcp, use_dhcpv6=use_dhcpv6, + addresses=addresses, routes=routes, mtu=mtu, + members=members, ovs_options=ovs_options, + ovs_extra=ovs_extra, nic_mapping=nic_mapping, + persist_mapping=persist_mapping, + defroute=defroute, dhclient_args=dhclient_args, + dns_servers=dns_servers) + + class LinuxBridge(_BaseOpts): """Base class for Linux bridges.""" @@ -883,3 +946,62 @@ class IbInterface(_BaseOpts): name = _get_required_field(json, 'name', 'IbInterface') opts = _BaseOpts.base_opts_from_json(json) return IbInterface(name, *opts) + + +class OvsDpdkPort(_BaseOpts): + """Base class for OVS Dpdk Ports.""" + + def __init__(self, name, use_dhcp=False, use_dhcpv6=False, addresses=None, + routes=None, mtu=None, primary=False, nic_mapping=None, + persist_mapping=False, defroute=True, dhclient_args=None, + dns_servers=None, members=None, driver='vfio-pci', + ovs_options=None, ovs_extra=None): + + super(OvsDpdkPort, self).__init__(name, use_dhcp, use_dhcpv6, + addresses, routes, mtu, primary, + nic_mapping, persist_mapping, + defroute, dhclient_args, + dns_servers) + self.members = members or [] + self.ovs_options = ovs_options or [] + self.ovs_extra = ovs_extra or [] + self.driver = driver + + @staticmethod + def from_json(json): + name = _get_required_field(json, 'name', 'OvsDpdkPort') + # driver name by default will be 'vfio-pci' if not specified + driver = json.get('driver') + if not driver: + driver = 'vfio-pci' + + # members + members = [] + members_json = json.get('members') + if members_json: + if isinstance(members_json, list): + if len(members_json) == 1: + iface = object_from_json(members_json[0]) + if isinstance(iface, Interface): + # TODO(skramaja): Add checks for IP and route not to + # be set in the interface part of DPDK Port + members.append(iface) + else: + msg = 'OVS DPDK Port should have only interface member' + raise InvalidConfigException(msg) + else: + msg = 'OVS DPDK Port should have only one member' + raise InvalidConfigException(msg) + else: + msg = 'Members must be a list.' + raise InvalidConfigException(msg) + else: + msg = 'DPDK Port should have one member as Interface' + raise InvalidConfigException(msg) + + ovs_options = json.get('ovs_options', []) + ovs_options = ['options:%s' % opt for opt in ovs_options] + ovs_extra = json.get('ovs_extra', []) + opts = _BaseOpts.base_opts_from_json(json) + return OvsDpdkPort(name, *opts, members=members, driver=driver, + ovs_options=ovs_options, ovs_extra=ovs_extra) diff --git a/os_net_config/tests/test_cli.py b/os_net_config/tests/test_cli.py index e598dcb..939b12e 100644 --- a/os_net_config/tests/test_cli.py +++ b/os_net_config/tests/test_cli.py @@ -164,3 +164,20 @@ class TestCli(base.TestCase): for dev in sanity_devices: self.assertIn(dev, stdout_yaml) self.assertEqual(stdout_yaml, stdout_json) + + def test_ovs_dpdk_noop_output(self): + ivs_yaml = os.path.join(SAMPLE_BASE, 'ovs_dpdk.yaml') + ivs_json = os.path.join(SAMPLE_BASE, 'ovs_dpdk.json') + stdout_yaml, stderr = self.run_cli('ARG0 --provider=ifcfg --noop ' + '-c %s' % ivs_yaml) + self.assertEqual('', stderr) + stdout_json, stderr = self.run_cli('ARG0 --provider=ifcfg --noop ' + '-c %s' % ivs_json) + self.assertEqual('', stderr) + sanity_devices = ['DEVICE=br-link', + 'TYPE=OVSUserBridge', + 'DEVICE=dpdk0', + 'TYPE=OVSDPDKPort'] + for dev in sanity_devices: + self.assertIn(dev, stdout_yaml) + self.assertEqual(stdout_yaml, stdout_json) diff --git a/os_net_config/tests/test_impl_ifcfg.py b/os_net_config/tests/test_impl_ifcfg.py index 9b24717..ad27d83 100644 --- a/os_net_config/tests/test_impl_ifcfg.py +++ b/os_net_config/tests/test_impl_ifcfg.py @@ -758,6 +758,36 @@ DNS2=5.6.7.8 """ self.assertEqual(em1_config, self.get_interface_config('em1')) + def test_network_ovs_dpdk_bridge_and_port(self): + interface = objects.Interface(name='eth1') + dpdk_port = objects.OvsDpdkPort(name='dpdk0', members=[interface]) + bridge = objects.OvsUserBridge('br-link', members=[dpdk_port]) + self.provider.add_interface(interface) + self.provider.add_interface(dpdk_port) + self.provider.add_bridge(bridge) + br_link_config = """# This file is autogenerated by os-net-config +DEVICE=br-link +ONBOOT=yes +HOTPLUG=no +NM_CONTROLLED=no +PEERDNS=no +DEVICETYPE=ovs +TYPE=OVSUserBridge +""" + dpdk0_config = """# This file is autogenerated by os-net-config +DEVICE=dpdk0 +ONBOOT=yes +HOTPLUG=no +NM_CONTROLLED=no +PEERDNS=no +DEVICETYPE=ovs +TYPE=OVSDPDKPort +OVS_BRIDGE=br-link +""" + self.assertEqual(br_link_config, + self.provider.bridge_data['br-link']) + self.assertEqual(dpdk0_config, self.get_interface_config('dpdk0')) + class TestIfcfgNetConfigApply(base.TestCase): diff --git a/os_net_config/utils.py b/os_net_config/utils.py index 83aae63..7b85d91 100644 --- a/os_net_config/utils.py +++ b/os_net_config/utils.py @@ -19,11 +19,17 @@ import logging import os import re +from oslo_concurrency import processutils + logger = logging.getLogger(__name__) _SYS_CLASS_NET = '/sys/class/net' +class OvsDpdkBindException(ValueError): + pass + + def write_config(filename, data): with open(filename, 'w') as f: f.write(str(data)) @@ -118,3 +124,39 @@ def diff(filename, data): logger.debug("Diff data:\n%s" % data) # convert to string as JSON may have unicode in it return not file_data == data + + +def bind_dpdk_interfaces(ifname, driver, noop): + pci_addres = _get_pci_address(ifname, noop) + if not noop: + if pci_addres: + # modbprobe of the driver has to be done before binding. + # for reboots, puppet will add the modprobe to /etc/rc.modules + processutils.execute('modprobe', 'vfio-pci') + + out, err = processutils.execute('driverctl', 'set-override', + pci_addres, driver) + if err: + msg = "Failed to bind interface %s with dpdk" % ifname + raise OvsDpdkBindException(msg) + else: + processutils.execute('driverctl', 'load-override', pci_addres) + else: + logger.info('Interface %(name)s bound to DPDK driver %(driver)s ' + 'using driverctl command' % + {'name': ifname, 'driver': driver}) + + +def _get_pci_address(ifname, noop): + # TODO(skramaja): Validate if the given interface supports dpdk + if not noop: + # If ifname is already bound, then ethtool will not be able to list the + # device, in which case, binding is already done, proceed with scripts + out, err = processutils.execute('ethtool', '-i', ifname) + if not err: + for item in out.split('\n'): + if 'bus-info' in item: + return item.split(' ')[1] + else: + logger.info('Fetch the PCI address of the interface %s using ' + 'ethtool' % ifname) -- cgit 1.2.3-korg