diff options
-rw-r--r-- | etc/os-net-config/samples/interface.json | 8 | ||||
-rw-r--r-- | etc/os-net-config/samples/interface.yaml | 7 | ||||
-rw-r--r-- | etc/os-net-config/samples/nfvswitch.json | 2 | ||||
-rw-r--r-- | etc/os-net-config/samples/nfvswitch.yaml | 4 | ||||
-rw-r--r-- | os_net_config/impl_eni.py | 11 | ||||
-rw-r--r-- | os_net_config/impl_ifcfg.py | 42 | ||||
-rw-r--r-- | os_net_config/objects.py | 38 | ||||
-rw-r--r-- | os_net_config/tests/test_cli.py | 18 | ||||
-rw-r--r-- | os_net_config/tests/test_impl_eni.py | 18 | ||||
-rw-r--r-- | os_net_config/tests/test_impl_ifcfg.py | 52 | ||||
-rw-r--r-- | os_net_config/tests/test_objects.py | 78 | ||||
-rw-r--r-- | os_net_config/tests/test_utils.py | 168 | ||||
-rw-r--r-- | os_net_config/utils.py | 136 |
13 files changed, 464 insertions, 118 deletions
diff --git a/etc/os-net-config/samples/interface.json b/etc/os-net-config/samples/interface.json index 8a942b5..6eb8f62 100644 --- a/etc/os-net-config/samples/interface.json +++ b/etc/os-net-config/samples/interface.json @@ -13,6 +13,11 @@ "ip_netmask": "0.0.0.0/0", "next_hop": "192.0.2.254", "default": "true" + }, + { + "ip_netmask": "10.1.2.0/24", + "next_hop": "192.0.2.5", + "route_options": "metric 10" } ] }, @@ -20,7 +25,8 @@ "type": "interface", "name": "em2", "use_dhcp": true, - "defroute": no + "defroute": no, + "ethtool_opts": "speed 1000 duplex full" } ] } diff --git a/etc/os-net-config/samples/interface.yaml b/etc/os-net-config/samples/interface.yaml index 4f76e07..725091b 100644 --- a/etc/os-net-config/samples/interface.yaml +++ b/etc/os-net-config/samples/interface.yaml @@ -11,8 +11,13 @@ network_config: ip_netmask: 0.0.0.0/0 next_hop: 192.0.2.254 default: true + - + ip_netmask: 10.1.2.0/24 + next_hop: 192.0.2.5 + route_options: "metric 10" - type: interface name: em2 use_dhcp: true - defroute: no
\ No newline at end of file + defroute: no + ethtool_opts: "speed 1000 duplex full" diff --git a/etc/os-net-config/samples/nfvswitch.json b/etc/os-net-config/samples/nfvswitch.json index 2d8af8a..b081de9 100644 --- a/etc/os-net-config/samples/nfvswitch.json +++ b/etc/os-net-config/samples/nfvswitch.json @@ -2,7 +2,7 @@ "network_config": [ { "type": "nfvswitch_bridge", - "cpus": "2,3,4,5", + "options": "-c 2,3,4,5", "members": [ { "type": "interface", diff --git a/etc/os-net-config/samples/nfvswitch.yaml b/etc/os-net-config/samples/nfvswitch.yaml index 5af3f70..d7571ae 100644 --- a/etc/os-net-config/samples/nfvswitch.yaml +++ b/etc/os-net-config/samples/nfvswitch.yaml @@ -1,7 +1,7 @@ network_config: - type: nfvswitch_bridge - cpus: "2,3,4,5" + options: "-c 2,3,4,5" members: - type: interface @@ -22,4 +22,4 @@ network_config: vlan_id: 202 addresses: - - ip_netmask: 172.16.1.6/24
\ No newline at end of file + ip_netmask: 172.16.1.6/24 diff --git a/os_net_config/impl_eni.py b/os_net_config/impl_eni.py index d15872e..5b91270 100644 --- a/os_net_config/impl_eni.py +++ b/os_net_config/impl_eni.py @@ -181,14 +181,17 @@ class ENINetConfig(os_net_config.NetConfig): logger.info('adding custom route for interface: %s' % interface_name) data = "" for route in routes: + options = "" + if route.route_options: + options = " %s" % (route.route_options) if route.default and not route.ip_netmask: rt = netaddr.IPNetwork("0.0.0.0/0") else: rt = netaddr.IPNetwork(route.ip_netmask) - data += "up route add -net %s netmask %s gw %s\n" % ( - str(rt.ip), str(rt.netmask), route.next_hop) - data += "down route del -net %s netmask %s gw %s\n" % ( - str(rt.ip), str(rt.netmask), route.next_hop) + data += "up route add -net %s netmask %s gw %s%s\n" % ( + str(rt.ip), str(rt.netmask), route.next_hop, options) + data += "down route del -net %s netmask %s gw %s%s\n" % ( + str(rt.ip), str(rt.netmask), route.next_hop, options) self.routes[interface_name] = data logger.debug('route data: %s' % self.routes[interface_name]) diff --git a/os_net_config/impl_ifcfg.py b/os_net_config/impl_ifcfg.py index 79e3e42..e8dbd46 100644 --- a/os_net_config/impl_ifcfg.py +++ b/os_net_config/impl_ifcfg.py @@ -63,7 +63,7 @@ class IfcfgNetConfig(os_net_config.NetConfig): self.interface_data = {} self.ivsinterface_data = {} self.nfvswitch_intiface_data = {} - self.nfvswitch_cpus = None + self.nfvswitch_options = None self.vlan_data = {} self.route_data = {} self.route6_data = {} @@ -113,6 +113,8 @@ class IfcfgNetConfig(os_net_config.NetConfig): data += "TYPE=NFVSWITCHIntPort\n" elif isinstance(base_opt, objects.IbInterface): data += "TYPE=Infiniband\n" + if base_opt.ethtool_opts: + data += "ETHTOOL_OPTS=\"%s\"\n" % base_opt.ethtool_opts elif re.match('\w+\.\d+$', base_opt.name): data += "VLAN=yes\n" if base_opt.linux_bond_name: @@ -262,6 +264,9 @@ class IfcfgNetConfig(os_net_config.NetConfig): data += "BOOTPROTO=dhcp\n" elif not base_opt.addresses: data += "BOOTPROTO=none\n" + if isinstance(base_opt, objects.Interface): + if base_opt.ethtool_opts: + data += "ETHTOOL_OPTS=\"%s\"\n" % base_opt.ethtool_opts if base_opt.mtu: data += "MTU=%i\n" % base_opt.mtu @@ -313,24 +318,29 @@ class IfcfgNetConfig(os_net_config.NetConfig): data6 = "" first_line6 = "" for route in routes: + options = "" + if route.route_options: + options = " %s" % (route.route_options) if ":" not in route.next_hop: # Route is an IPv4 route if route.default: - first_line = "default via %s dev %s\n" % (route.next_hop, - interface_name) + first_line = "default via %s dev %s%s\n" % ( + route.next_hop, interface_name, + options) else: - data += "%s via %s dev %s\n" % (route.ip_netmask, - route.next_hop, - interface_name) + data += "%s via %s dev %s%s\n" % ( + route.ip_netmask, route.next_hop, + interface_name, options) else: # Route is an IPv6 route if route.default: - first_line6 = "default via %s dev %s\n" % (route.next_hop, - interface_name) + first_line6 = "default via %s dev %s%s\n" % ( + route.next_hop, interface_name, + options) else: - data6 += "%s via %s dev %s\n" % (route.ip_netmask, - route.next_hop, - interface_name) + data6 += "%s via %s dev %s%s\n" % ( + route.ip_netmask, route.next_hop, + interface_name, options) self.route_data[interface_name] = first_line + data self.route6_data[interface_name] = first_line6 + data6 logger.debug('route data: %s' % self.route_data[interface_name]) @@ -446,7 +456,7 @@ class IfcfgNetConfig(os_net_config.NetConfig): is running, the nfvswitch virtual switch will be available. :param bridge: The NfvswitchBridge object to add. """ - self.nfvswitch_cpus = bridge.cpus + self.nfvswitch_options = bridge.options def add_bond(self, bond): """Add an OvsBond object to the net config object. @@ -583,9 +593,9 @@ class IfcfgNetConfig(os_net_config.NetConfig): nfvswitch_internal_ifaces): """Generate configuration content for nfvswitch.""" - cpu_str = "" - if self.nfvswitch_cpus: - cpu_str = " -c " + self.nfvswitch_cpus + options_str = "" + if self.nfvswitch_options: + options_str = self.nfvswitch_options ifaces = [] for iface in nfvswitch_ifaces: @@ -599,7 +609,7 @@ class IfcfgNetConfig(os_net_config.NetConfig): ifaces.append(iface) internal_str = ''.join(ifaces) - data = ("SETUP_ARGS=\"%s%s%s\"" % (cpu_str, iface_str, internal_str)) + data = "SETUP_ARGS=\"%s%s%s\"" % (options_str, iface_str, internal_str) return data def apply(self, cleanup=False, activate=True): diff --git a/os_net_config/objects.py b/os_net_config/objects.py index 8fab1ab..8d7ee4c 100644 --- a/os_net_config/objects.py +++ b/os_net_config/objects.py @@ -134,17 +134,20 @@ def _mapped_nics(nic_mapping=None): class Route(object): """Base class for network routes.""" - def __init__(self, next_hop, ip_netmask="", default=False): + def __init__(self, next_hop, ip_netmask="", default=False, + route_options=""): self.next_hop = next_hop self.ip_netmask = ip_netmask self.default = default + self.route_options = route_options @staticmethod def from_json(json): next_hop = _get_required_field(json, 'next_hop', 'Route') ip_netmask = json.get('ip_netmask', "") + route_options = json.get('route_options', "") default = strutils.bool_from_string(str(json.get('default', False))) - return Route(next_hop, ip_netmask, default) + return Route(next_hop, ip_netmask, default, route_options) class Address(object): @@ -276,7 +279,7 @@ class Interface(_BaseOpts): 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): + dns_servers=None, ethtool_opts=None): addresses = addresses or [] routes = routes or [] dns_servers = dns_servers or [] @@ -284,12 +287,14 @@ class Interface(_BaseOpts): routes, mtu, primary, nic_mapping, persist_mapping, defroute, dhclient_args, dns_servers) + self.ethtool_opts = ethtool_opts @staticmethod def from_json(json): name = _get_required_field(json, 'name', 'Interface') opts = _BaseOpts.base_opts_from_json(json) - return Interface(name, *opts) + ethtool_opts = json.get('ethtool_opts', None) + return Interface(name, *opts, ethtool_opts=ethtool_opts) class Vlan(_BaseOpts): @@ -628,7 +633,7 @@ class NfvswitchBridge(_BaseOpts): def __init__(self, name='nfvswitch', use_dhcp=False, use_dhcpv6=False, addresses=None, routes=None, mtu=1500, members=None, nic_mapping=None, persist_mapping=False, defroute=True, - dhclient_args=None, dns_servers=None, cpus=""): + dhclient_args=None, dns_servers=None, options=""): addresses = addresses or [] routes = routes or [] members = members or [] @@ -638,7 +643,7 @@ class NfvswitchBridge(_BaseOpts): nic_mapping, persist_mapping, defroute, dhclient_args, dns_servers) - self.cpus = cpus + self.options = options self.members = members for member in self.members: if isinstance(member, OvsBond) or isinstance(member, LinuxBond): @@ -667,16 +672,9 @@ class NfvswitchBridge(_BaseOpts): msg = 'Members must be a list.' raise InvalidConfigException(msg) - cpus = '' - cpus_json = json.get('cpus') - if cpus_json: - if isinstance(cpus_json, basestring): - cpus = cpus_json - else: - msg = '"cpus" must be a string of numbers separated by commas.' - raise InvalidConfigException(msg) - else: - msg = 'Config "cpus" is mandatory.' + options = json.get('options') + if not options: + msg = 'Config "options" is mandatory.' raise InvalidConfigException(msg) return NfvswitchBridge(name, use_dhcp=use_dhcp, use_dhcpv6=use_dhcpv6, @@ -684,7 +682,7 @@ class NfvswitchBridge(_BaseOpts): members=members, nic_mapping=nic_mapping, persist_mapping=persist_mapping, defroute=defroute, dhclient_args=dhclient_args, - dns_servers=dns_servers, cpus=cpus) + dns_servers=dns_servers, options=options) class LinuxTeam(_BaseOpts): @@ -935,7 +933,7 @@ class IbInterface(_BaseOpts): 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): + dns_servers=None, ethtool_opts=None): addresses = addresses or [] routes = routes or [] dns_servers = dns_servers or [] @@ -943,12 +941,14 @@ class IbInterface(_BaseOpts): addresses, routes, mtu, primary, nic_mapping, persist_mapping, defroute, dhclient_args, dns_servers) + self.ethtool_opts = ethtool_opts @staticmethod def from_json(json): name = _get_required_field(json, 'name', 'IbInterface') + ethtool_opts = json.get('ethtool_opts', None) opts = _BaseOpts.base_opts_from_json(json) - return IbInterface(name, *opts) + return IbInterface(name, *opts, ethtool_opts=ethtool_opts) class OvsDpdkPort(_BaseOpts): diff --git a/os_net_config/tests/test_cli.py b/os_net_config/tests/test_cli.py index c5c825f..0626205 100644 --- a/os_net_config/tests/test_cli.py +++ b/os_net_config/tests/test_cli.py @@ -66,6 +66,24 @@ class TestCli(base.TestCase): self.assertIn(dev, stdout_yaml) self.assertEqual(stdout_yaml, stdout_json) + def test_ivs_noop_output(self): + ivs_yaml = os.path.join(SAMPLE_BASE, 'ivs.yaml') + ivs_json = os.path.join(SAMPLE_BASE, 'ivs.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=nic2', + 'DEVICE=nic3', + 'DEVICE=api201', + 'DEVICE=storage202', + 'DEVICETYPE=ivs'] + for dev in sanity_devices: + self.assertIn(dev, stdout_yaml) + self.assertEqual(stdout_yaml, stdout_json) + def test_bridge_noop_output(self): bridge_yaml = os.path.join(SAMPLE_BASE, 'bridge_dhcp.yaml') bridge_json = os.path.join(SAMPLE_BASE, 'bridge_dhcp.json') diff --git a/os_net_config/tests/test_impl_eni.py b/os_net_config/tests/test_impl_eni.py index 7f909ff..5d3bb8c 100644 --- a/os_net_config/tests/test_impl_eni.py +++ b/os_net_config/tests/test_impl_eni.py @@ -90,6 +90,8 @@ iface vlan5 inet manual _RTS = """up route add -net 172.19.0.0 netmask 255.255.255.0 gw 192.168.1.1 down route del -net 172.19.0.0 netmask 255.255.255.0 gw 192.168.1.1 +up route add -net 172.20.0.0 netmask 255.255.255.0 gw 192.168.1.5 metric 100 +down route del -net 172.20.0.0 netmask 255.255.255.0 gw 192.168.1.5 metric 100 """ @@ -169,8 +171,10 @@ class TestENINetConfig(base.TestCase): def test_network_with_routes(self): route1 = objects.Route('192.168.1.1', '172.19.0.0/24') + route2 = objects.Route('192.168.1.5', '172.20.0.0/24', + route_options="metric 100") v4_addr = objects.Address('192.168.1.2/24') - interface = self._default_interface([v4_addr], [route1]) + interface = self._default_interface([v4_addr], [route1, route2]) self.provider.add_interface(interface) self.assertEqual(_V4_IFACE_STATIC_IP, self.get_interface_config()) self.assertEqual(_RTS, self.get_route_config()) @@ -261,10 +265,12 @@ class TestENINetConfigApply(base.TestCase): super(TestENINetConfigApply, self).tearDown() def test_network_apply(self): - route = objects.Route('192.168.1.1', '172.19.0.0/24') + route1 = objects.Route('192.168.1.1', '172.19.0.0/24') + route2 = objects.Route('192.168.1.5', '172.20.0.0/24', + route_options="metric 100") v4_addr = objects.Address('192.168.1.2/24') interface = objects.Interface('eth0', addresses=[v4_addr], - routes=[route]) + routes=[route1, route2]) self.provider.add_interface(interface) self.provider.apply() @@ -273,10 +279,12 @@ class TestENINetConfigApply(base.TestCase): self.assertIn('eth0', self.ifup_interface_names) def test_apply_noactivate(self): - route = objects.Route('192.168.1.1', '172.19.0.0/24') + route1 = objects.Route('192.168.1.1', '172.19.0.0/24') + route2 = objects.Route('192.168.1.5', '172.20.0.0/24', + route_options="metric 100") v4_addr = objects.Address('192.168.1.2/24') interface = objects.Interface('eth0', addresses=[v4_addr], - routes=[route]) + routes=[route1, route2]) self.provider.add_interface(interface) self.provider.apply(activate=False) diff --git a/os_net_config/tests/test_impl_ifcfg.py b/os_net_config/tests/test_impl_ifcfg.py index c3acbc8..2e79aa1 100644 --- a/os_net_config/tests/test_impl_ifcfg.py +++ b/os_net_config/tests/test_impl_ifcfg.py @@ -115,12 +115,14 @@ _OVS_BRIDGE_IFCFG = _BASE_IFCFG + "DEVICETYPE=ovs\n" _LINUX_BRIDGE_IFCFG = _BASE_IFCFG + "BRIDGE=br-ctlplane\nBOOTPROTO=none\n" -_ROUTES = """default via 192.168.1.1 dev em1 +_ROUTES = """default via 192.168.1.1 dev em1 metric 10 172.19.0.0/24 via 192.168.1.1 dev em1 +172.20.0.0/24 via 192.168.1.5 dev em1 metric 100 """ _ROUTES_V6 = """default via 2001:db8::1 dev em1 2001:db8:dead:beef:cafe::/56 via fd00:fd00:2000::1 dev em1 +2001:db8:dead:beff::/64 via fd00:fd00:2000::1 dev em1 metric 100 """ @@ -318,7 +320,7 @@ IPADDR=172.16.2.7 NETMASK=255.255.255.0 """ -_NFVSWITCH_CONFIG = ('SETUP_ARGS=\" -c 2,3,4,5 -u em1 -m storage5\"') +_NFVSWITCH_CONFIG = ('SETUP_ARGS=\"-c 2,3,4,5 -u em1 -m storage5\"') _OVS_IFCFG_PATCH_PORT = """# This file is autogenerated by os-net-config DEVICE=br-pub-patch @@ -454,25 +456,35 @@ class TestIfcfgNetConfig(base.TestCase): self.assertEqual(_V6_IFCFG_MULTIPLE, self.get_interface_config()) def test_network_with_routes(self): - route1 = objects.Route('192.168.1.1', default=True) + route1 = objects.Route('192.168.1.1', default=True, + route_options="metric 10") route2 = objects.Route('192.168.1.1', '172.19.0.0/24') + route3 = objects.Route('192.168.1.5', '172.20.0.0/24', + route_options="metric 100") v4_addr = objects.Address('192.168.1.2/24') interface = objects.Interface('em1', addresses=[v4_addr], - routes=[route1, route2]) + routes=[route1, route2, route3]) self.provider.add_interface(interface) self.assertEqual(_V4_IFCFG, self.get_interface_config()) self.assertEqual(_ROUTES, self.get_route_config()) def test_network_with_ipv6_routes(self): - route1 = objects.Route('192.168.1.1', default=True) + route1 = objects.Route('192.168.1.1', default=True, + route_options="metric 10") route2 = objects.Route('192.168.1.1', '172.19.0.0/24') - route3 = objects.Route('2001:db8::1', default=True) - route4 = objects.Route('fd00:fd00:2000::1', + route3 = objects.Route('192.168.1.5', '172.20.0.0/24', + route_options="metric 100") + route4 = objects.Route('2001:db8::1', default=True) + route5 = objects.Route('fd00:fd00:2000::1', '2001:db8:dead:beef:cafe::/56') + route6 = objects.Route('fd00:fd00:2000::1', + '2001:db8:dead:beff::/64', + route_options="metric 100") v4_addr = objects.Address('192.168.1.2/24') v6_addr = objects.Address('2001:abc:a::/64') interface = objects.Interface('em1', addresses=[v4_addr, v6_addr], - routes=[route1, route2, route3, route4]) + routes=[route1, route2, route3, + route4, route5, route6]) self.provider.add_interface(interface) self.assertEqual(_V4_V6_IFCFG, self.get_interface_config()) self.assertEqual(_ROUTES_V6, self.get_route6_config()) @@ -590,7 +602,7 @@ class TestIfcfgNetConfig(base.TestCase): iface_name = nfvswitch_internal.name bridge = objects.NfvswitchBridge(members=[interface, nfvswitch_internal], - cpus="2,3,4,5") + options="-c 2,3,4,5") self.provider.add_interface(interface) self.provider.add_nfvswitch_internal(nfvswitch_internal) self.provider.add_nfvswitch_bridge(bridge) @@ -730,6 +742,21 @@ DHCLIENTARGS=--foobar """ self.assertEqual(em1_config, self.get_interface_config('em1')) + def test_interface_ethtool_opts(self): + interface1 = objects.Interface('em1', + ethtool_opts='speed 1000 duplex full') + self.provider.add_interface(interface1) + em1_config = """# This file is autogenerated by os-net-config +DEVICE=em1 +ONBOOT=yes +HOTPLUG=no +NM_CONTROLLED=no +PEERDNS=no +BOOTPROTO=none +ETHTOOL_OPTS=\"speed 1000 duplex full\" +""" + self.assertEqual(em1_config, self.get_interface_config('em1')) + def test_interface_single_dns_server(self): interface1 = objects.Interface('em1', dns_servers=['1.2.3.4']) self.provider.add_interface(interface1) @@ -884,11 +911,14 @@ class TestIfcfgNetConfigApply(base.TestCase): super(TestIfcfgNetConfigApply, self).tearDown() def test_network_apply(self): - route1 = objects.Route('192.168.1.1', default=True) + route1 = objects.Route('192.168.1.1', default=True, + route_options="metric 10") route2 = objects.Route('192.168.1.1', '172.19.0.0/24') + route3 = objects.Route('192.168.1.5', '172.20.0.0/24', + route_options="metric 100") v4_addr = objects.Address('192.168.1.2/24') interface = objects.Interface('em1', addresses=[v4_addr], - routes=[route1, route2]) + routes=[route1, route2, route3]) self.provider.add_interface(interface) self.provider.apply() diff --git a/os_net_config/tests/test_objects.py b/os_net_config/tests/test_objects.py index 2e5fbe4..3911ef3 100644 --- a/os_net_config/tests/test_objects.py +++ b/os_net_config/tests/test_objects.py @@ -25,26 +25,30 @@ from os_net_config import utils class TestRoute(base.TestCase): def test_from_json(self): - data = '{"next_hop": "172.19.0.1", "ip_netmask": "172.19.0.0/24"}' + data = '{"next_hop": "172.19.0.1", "ip_netmask": "172.19.0.0/24", ' \ + '"route_options": "metric 10"}' route = objects.Route.from_json(json.loads(data)) self.assertEqual("172.19.0.1", route.next_hop) self.assertEqual("172.19.0.0/24", route.ip_netmask) self.assertFalse(route.default) + self.assertEqual("metric 10", route.route_options) def test_from_json_default_route(self): data = '{"next_hop": "172.19.0.1", "ip_netmask": "172.19.0.0/24", ' \ - '"default": true}' + '"default": true, "route_options": "metric 10"}' route = objects.Route.from_json(json.loads(data)) self.assertEqual("172.19.0.1", route.next_hop) self.assertEqual("172.19.0.0/24", route.ip_netmask) self.assertTrue(route.default) + self.assertEqual("metric 10", route.route_options) data = '{"next_hop": "172.19.0.1", "ip_netmask": "172.19.0.0/24", ' \ - '"default": "true"}' + '"default": "true", "route_options": "metric 10"}' route = objects.Route.from_json(json.loads(data)) self.assertEqual("172.19.0.1", route.next_hop) self.assertEqual("172.19.0.0/24", route.ip_netmask) self.assertTrue(route.default) + self.assertEqual("metric 10", route.route_options) class TestAddress(base.TestCase): @@ -146,12 +150,14 @@ class TestInterface(base.TestCase): "name": "em1", "use_dhcp": false, "mtu": 1501, +"ethtool_opts": "speed 1000 duplex full", "addresses": [{ "ip_netmask": "192.0.2.1/24" }], "routes": [{ "next_hop": "192.0.2.1", - "ip_netmask": "192.0.2.1/24" + "ip_netmask": "192.0.2.1/24", + "route_options": "metric 10" }] } """ @@ -160,12 +166,14 @@ class TestInterface(base.TestCase): self.assertFalse(interface.use_dhcp) self.assertFalse(interface.use_dhcpv6) self.assertEqual(1501, interface.mtu) + self.assertEqual("speed 1000 duplex full", interface.ethtool_opts) address1 = interface.v4_addresses()[0] self.assertEqual("192.0.2.1", address1.ip) self.assertEqual("255.255.255.0", address1.netmask) route1 = interface.routes[0] self.assertEqual("192.0.2.1", route1.next_hop) self.assertEqual("192.0.2.1/24", route1.ip_netmask) + self.assertEqual("metric 10", route1.route_options) class TestVlan(base.TestCase): @@ -345,13 +353,13 @@ class TestLinuxBridge(base.TestCase): class TestIvsBridge(base.TestCase): - def test_interface_from_json(self): + def test_from_json(self): data = """{ "type": "ivs_bridge", -"members": [{ - "type": "interface", - "name": "nic2" -}] +"members": [ + {"type": "interface", "name": "nic2"}, + {"type": "interface", "name": "nic3"} + ] } """ bridge = objects.object_from_json(json.loads(data)) @@ -359,16 +367,20 @@ class TestIvsBridge(base.TestCase): interface1 = bridge.members[0] self.assertEqual("nic2", interface1.name) self.assertEqual(False, interface1.ovs_port) + interface2 = bridge.members[1] + self.assertEqual("nic3", interface2.name) + self.assertEqual(False, interface2.ovs_port) self.assertEqual("ivs", interface1.ivs_bridge_name) + +class TestIvsInterface(base.TestCase): + def test_ivs_interface_from_json(self): data = """{ "type": "ivs_bridge", -"members": [{ - "type": "ivs_interface", - "name": "storage", - "vlan_id": 202 -}] +"members": [ + {"type": "ivs_interface", "name": "storage", "vlan_id": 202} + ] } """ bridge = objects.object_from_json(json.loads(data)) @@ -403,27 +415,7 @@ class TestNfvswitchBridge(base.TestCase): def test_from_json(self): data = """{ "type": "nfvswitch_bridge", -"cpus": "2,3,4,5", -"members": [ - {"type": "interface", "name": "nic2"} - ] -} -""" - bridge = objects.object_from_json(json.loads(data)) - self.assertEqual("nfvswitch", bridge.name) - self.assertEqual("2,3,4,5", bridge.cpus) - interface1 = bridge.members[0] - self.assertEqual("nic2", interface1.name) - self.assertEqual(False, interface1.ovs_port) - self.assertEqual("nfvswitch", interface1.nfvswitch_bridge_name) - - -class TestNfvswitchInterface(base.TestCase): - - def test_interface_from_json(self): - data = """{ -"type": "nfvswitch_bridge", -"cpus": "2,3,4,5", +"options": "-c 2,3,4,5", "members": [ {"type": "interface","name": "nic1"}, {"type": "interface","name": "nic2"} @@ -432,18 +424,22 @@ class TestNfvswitchInterface(base.TestCase): """ bridge = objects.object_from_json(json.loads(data)) self.assertEqual("nfvswitch", bridge.name) - self.assertEqual("2,3,4,5", bridge.cpus) + self.assertEqual("-c 2,3,4,5", bridge.options) interface1 = bridge.members[0] self.assertEqual("nic1", interface1.name) + self.assertEqual(False, interface1.ovs_port) interface2 = bridge.members[1] self.assertEqual("nic2", interface2.name) self.assertEqual(False, interface2.ovs_port) self.assertEqual("nfvswitch", interface1.nfvswitch_bridge_name) + +class TestNfvswitchInterface(base.TestCase): + def test_nfvswitch_internal_from_json(self): data = """{ "type": "nfvswitch_bridge", -"cpus": "2,3,4,5", +"options": "-c 2,3,4,5", "members": [ {"type": "nfvswitch_internal", "name": "storage", "vlan_id": 202}, {"type": "nfvswitch_internal", "name": "api", "vlan_id": 201} @@ -452,7 +448,7 @@ class TestNfvswitchInterface(base.TestCase): """ bridge = objects.object_from_json(json.loads(data)) self.assertEqual("nfvswitch", bridge.name) - self.assertEqual("2,3,4,5", bridge.cpus) + self.assertEqual("-c 2,3,4,5", bridge.options) interface1 = bridge.members[0] self.assertEqual("storage202", interface1.name) interface2 = bridge.members[1] @@ -463,7 +459,7 @@ class TestNfvswitchInterface(base.TestCase): def test_bond_interface_from_json(self): data = """{ "type": "nfvswitch_bridge", -"cpus": "2,3,4,5", +"options": "-c 2,3,4,5", "members": [{ "type": "linux_bond", "name": "bond1", "members": [{"type": "interface", "name": "nic2"}, @@ -739,7 +735,8 @@ class TestIbInterface(base.TestCase): }], "routes": [{ "next_hop": "192.0.2.1", - "ip_netmask": "192.0.2.1/24" + "ip_netmask": "192.0.2.1/24", + "route_options": "metric 10" }] } """ @@ -754,6 +751,7 @@ class TestIbInterface(base.TestCase): route1 = ib_interface.routes[0] self.assertEqual("192.0.2.1", route1.next_hop) self.assertEqual("192.0.2.1/24", route1.ip_netmask) + self.assertEqual("metric 10", route1.route_options) class TestNicMapping(base.TestCase): diff --git a/os_net_config/tests/test_utils.py b/os_net_config/tests/test_utils.py index b6531a6..b766384 100644 --- a/os_net_config/tests/test_utils.py +++ b/os_net_config/tests/test_utils.py @@ -14,16 +14,43 @@ # License for the specific language governing permissions and limitations # under the License. +import os import os.path +import random import shutil import tempfile +import yaml from os_net_config.tests import base from os_net_config import utils +from oslo_concurrency import processutils + +_PCI_OUTPUT = '''driver: e1000e +version: 3.2.6-k +firmware-version: 0.13-3 +expansion-rom-version: +bus-info: 0000:00:19.0 +supports-statistics: yes +supports-test: yes +supports-eeprom-access: yes +supports-register-dump: yes +supports-priv-flags: no +''' + class TestUtils(base.TestCase): + def setUp(self): + super(TestUtils, self).setUp() + rand = str(int(random.random() * 100000)) + utils._DPDK_MAPPING_FILE = '/tmp/dpdk_mapping_' + rand + '.yaml' + + def tearDown(self): + super(TestUtils, self).tearDown() + if os.path.isfile(utils._DPDK_MAPPING_FILE): + os.remove(utils._DPDK_MAPPING_FILE) + def test_ordered_active_nics(self): tmpdir = tempfile.mkdtemp() @@ -49,3 +76,144 @@ class TestUtils(base.TestCase): self.assertEqual('z1', nics[7]) shutil.rmtree(tmpdir) + + def test_get_pci_address_success(self): + def test_execute(name, dummy1, dummy2=None, dummy3=None): + if 'ethtool' in name: + out = _PCI_OUTPUT + return out, None + self.stubs.Set(processutils, 'execute', test_execute) + pci = utils._get_pci_address('nic2', False) + self.assertEqual('0000:00:19.0', pci) + + def test_get_pci_address_exception(self): + def test_execute(name, dummy1, dummy2=None, dummy3=None): + if 'ethtool' in name: + raise processutils.ProcessExecutionError + self.stubs.Set(processutils, 'execute', test_execute) + pci = utils._get_pci_address('nic2', False) + self.assertEqual(None, pci) + + def test_get_pci_address_error(self): + def test_execute(name, dummy1, dummy2=None, dummy3=None): + if 'ethtool' in name: + return None, 'Error' + self.stubs.Set(processutils, 'execute', test_execute) + pci = utils._get_pci_address('nic2', False) + self.assertEqual(None, pci) + + def test_bind_dpdk_interfaces(self): + def test_execute(name, dummy1, dummy2=None, dummy3=None): + if 'ethtool' in name: + out = _PCI_OUTPUT + return out, None + if 'driverctl' in name: + return None, None + + def test_get_dpdk_mac_address(name): + return '01:02:03:04:05:06' + self.stubs.Set(processutils, 'execute', test_execute) + self.stubs.Set(utils, '_get_dpdk_mac_address', + test_get_dpdk_mac_address) + + utils.bind_dpdk_interfaces('nic2', 'vfio-pci', False) + + def test_bind_dpdk_interfaces_fail(self): + def test_execute(name, dummy1, dummy2=None, dummy3=None): + if 'ethtool' in name: + out = _PCI_OUTPUT + return out, None + if 'driverctl' in name: + return None, 'Error' + + def test_get_dpdk_mac_address(name): + return '01:02:03:04:05:06' + self.stubs.Set(processutils, 'execute', test_execute) + self.stubs.Set(utils, '_get_dpdk_mac_address', + test_get_dpdk_mac_address) + + self.assertRaises(utils.OvsDpdkBindException, + utils.bind_dpdk_interfaces, 'eth1', 'vfio-pci', + False) + + def test__update_dpdk_map_new(self): + utils._update_dpdk_map('eth1', '0000:03:00.0', '01:02:03:04:05:06', + 'vfio-pci') + contents = utils.get_file_data(utils._DPDK_MAPPING_FILE) + + dpdk_map = yaml.load(contents) if contents else [] + self.assertEqual(1, len(dpdk_map)) + dpdk_test = [{'name': 'eth1', 'pci_address': '0000:03:00.0', + 'mac_address': '01:02:03:04:05:06', + 'driver': 'vfio-pci'}] + self.assertListEqual(dpdk_test, dpdk_map) + + def test_update_dpdk_map_exist(self): + dpdk_test = [{'name': 'eth1', 'pci_address': '0000:03:00.0', + 'mac_address': '01:02:03:04:05:06', + 'driver': 'vfio-pci'}] + utils.write_yaml_config(utils._DPDK_MAPPING_FILE, dpdk_test) + + utils._update_dpdk_map('eth1', '0000:03:00.0', '01:02:03:04:05:06', + 'vfio-pci') + contents = utils.get_file_data(utils._DPDK_MAPPING_FILE) + + dpdk_map = yaml.load(contents) if contents else [] + self.assertEqual(1, len(dpdk_map)) + self.assertListEqual(dpdk_test, dpdk_map) + + def test_update_dpdk_map_value_change(self): + dpdk_test = [{'name': 'eth1', 'pci_address': '0000:03:00.0', + 'driver': 'vfio-pci'}] + utils.write_yaml_config(utils._DPDK_MAPPING_FILE, dpdk_test) + + dpdk_test = [{'name': 'eth1', 'pci_address': '0000:03:00.0', + 'mac_address': '01:02:03:04:05:06', + 'driver': 'vfio-pci'}] + utils._update_dpdk_map('eth1', '0000:03:00.0', '01:02:03:04:05:06', + 'vfio-pci') + try: + contents = utils.get_file_data(utils._DPDK_MAPPING_FILE) + except IOError: + pass + + dpdk_map = yaml.load(contents) if contents else [] + self.assertEqual(1, len(dpdk_map)) + self.assertListEqual(dpdk_test, dpdk_map) + + def test_ordered_active_nics_with_dpdk_mapping(self): + + tmpdir = tempfile.mkdtemp() + self.stubs.Set(utils, '_SYS_CLASS_NET', tmpdir) + + def test_is_active_nic(interface_name): + return True + self.stubs.Set(utils, '_is_active_nic', test_is_active_nic) + + for nic in ['a1', 'em1', 'em2', 'eth2', 'z1', + 'enp8s0', 'enp10s0', 'enp1s0f0']: + with open(os.path.join(tmpdir, nic), 'w') as f: + f.write(nic) + + utils._update_dpdk_map('eth1', '0000:03:00.0', '01:02:03:04:05:06', + 'vfio-pci') + utils._update_dpdk_map('p3p1', '0000:04:00.0', '01:02:03:04:05:07', + 'igb_uio') + + nics = utils.ordered_active_nics() + + self.assertEqual('em1', nics[0]) + self.assertEqual('em2', nics[1]) + self.assertEqual('eth1', nics[2]) # DPDK bound nic + self.assertEqual('eth2', nics[3]) + self.assertEqual('a1', nics[4]) + self.assertEqual('enp1s0f0', nics[5]) + self.assertEqual('enp8s0', nics[6]) + self.assertEqual('enp10s0', nics[7]) + self.assertEqual('p3p1', nics[8]) # DPDK bound nic + self.assertEqual('z1', nics[9]) + + shutil.rmtree(tmpdir) + + def test_interface_mac_raises(self): + self.assertRaises(IOError, utils.interface_mac, 'ens20f2p3') diff --git a/os_net_config/utils.py b/os_net_config/utils.py index 7b85d91..af359d5 100644 --- a/os_net_config/utils.py +++ b/os_net_config/utils.py @@ -18,12 +18,22 @@ import glob import logging import os import re +import yaml from oslo_concurrency import processutils logger = logging.getLogger(__name__) _SYS_CLASS_NET = '/sys/class/net' +# File to contain the DPDK mapped nics, as nic name will not be available after +# binding driver, which is required for correct nic numbering. +# Format of the file (list mapped nic's details): +# - +# name: eth1 +# pci_address: 0000:02:00.0 +# mac_address: 01:02:03:04:05:06 +# driver: vfio-pci +_DPDK_MAPPING_FILE = '/var/lib/os-net-config/dpdk_mapping.yaml' class OvsDpdkBindException(ValueError): @@ -35,6 +45,18 @@ def write_config(filename, data): f.write(str(data)) +def write_yaml_config(filepath, data): + ensure_directory_presence(filepath) + with open(filepath, 'w') as f: + yaml.dump(data, f, default_flow_style=False) + + +def ensure_directory_presence(filepath): + dir_path = os.path.dirname(filepath) + if not os.path.exists(dir_path): + os.makedirs(dir_path) + + def get_file_data(filename): if not os.path.exists(filename): return '' @@ -59,6 +81,12 @@ def interface_mac(name): with open('/sys/class/net/%s/address' % name, 'r') as f: return f.read().rstrip() except IOError: + # If the interface is bound to a DPDK driver, get the mac address from + # the dpdk mapping file as /sys files will be removed after binding. + dpdk_mac_address = _get_dpdk_mac_address(name) + if dpdk_mac_address: + return dpdk_mac_address + logger.error("Unable to read mac address: %s" % name) raise @@ -93,6 +121,12 @@ def _natural_sort_key(s): for text in re.split(nsre, s)] +def _is_embedded_nic(nic): + if nic.startswith('em') or nic.startswith('eth') or nic.startswith('eno'): + return True + return False + + def ordered_active_nics(): embedded_nics = [] nics = [] @@ -100,8 +134,7 @@ def ordered_active_nics(): for name in glob.iglob(_SYS_CLASS_NET + '/*'): nic = name[(len(_SYS_CLASS_NET) + 1):] if _is_active_nic(nic): - if nic.startswith('em') or nic.startswith('eth') or \ - nic.startswith('eno'): + if _is_embedded_nic(nic): logger.debug("%s is an embedded active nic" % nic) embedded_nics.append(nic) else: @@ -109,6 +142,24 @@ def ordered_active_nics(): nics.append(nic) else: logger.debug("%s is not an active nic" % nic) + + # Adding nics which are bound to DPDK as it will not be found in '/sys' + # after it is bound to DPDK driver. + contents = get_file_data(_DPDK_MAPPING_FILE) + if contents: + dpdk_map = yaml.load(contents) + for item in dpdk_map: + nic = item['name'] + if _is_embedded_nic(nic): + logger.debug("%s is an embedded DPDK bound nic" % nic) + embedded_nics.append(nic) + else: + logger.debug("%s is an DPDK bound nic" % nic) + nics.append(nic) + else: + logger.debug("No DPDK mapping available in path (%s)" % + _DPDK_MAPPING_FILE) + # NOTE: we could just natural sort all active devices, # but this ensures em, eno, and eth are ordered first # (more backwards compatible) @@ -127,20 +178,31 @@ def diff(filename, data): def bind_dpdk_interfaces(ifname, driver, noop): - pci_addres = _get_pci_address(ifname, noop) + pci_address = _get_pci_address(ifname, noop) if not noop: - if pci_addres: + if pci_address: # 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: + if 'vfio-pci' in driver: + try: + processutils.execute('modprobe', 'vfio-pci') + except processutils.ProcessExecutionError: + msg = "Failed to modprobe vfio-pci module" + raise OvsDpdkBindException(msg) + + mac_address = interface_mac(ifname) + try: + out, err = processutils.execute('driverctl', 'set-override', + pci_address, driver) + if err: + msg = "Failed to bind dpdk interface err - %s" % err + raise OvsDpdkBindException(msg) + else: + _update_dpdk_map(ifname, pci_address, mac_address, driver) + + except processutils.ProcessExecutionError: 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' % @@ -150,13 +212,51 @@ def bind_dpdk_interfaces(ifname, driver, noop): 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] + try: + 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] + except processutils.ProcessExecutionError: + # 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 generation. + return + else: logger.info('Fetch the PCI address of the interface %s using ' 'ethtool' % ifname) + + +# Once the interface is bound to a DPDK driver, all the references to the +# interface including '/sys' and '/proc', will be removed. And there is no +# way to identify the nic name after it is bound. So, the DPDK bound nic info +# is stored persistently in a file and is used to for nic numbering on +# subsequent runs of os-net-config. +def _update_dpdk_map(ifname, pci_address, mac_address, driver): + contents = get_file_data(_DPDK_MAPPING_FILE) + dpdk_map = yaml.load(contents) if contents else [] + for item in dpdk_map: + if item['pci_address'] == pci_address: + item['name'] = ifname + item['mac_address'] = mac_address + item['driver'] = driver + break + else: + new_item = {} + new_item['pci_address'] = pci_address + new_item['name'] = ifname + new_item['mac_address'] = mac_address + new_item['driver'] = driver + dpdk_map.append(new_item) + + write_yaml_config(_DPDK_MAPPING_FILE, dpdk_map) + + +def _get_dpdk_mac_address(name): + contents = get_file_data(_DPDK_MAPPING_FILE) + dpdk_map = yaml.load(contents) if contents else [] + for item in dpdk_map: + if item['name'] == name: + return item['mac_address'] |