aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--etc/os-net-config/samples/interface.json8
-rw-r--r--etc/os-net-config/samples/interface.yaml7
-rw-r--r--etc/os-net-config/samples/nfvswitch.json2
-rw-r--r--etc/os-net-config/samples/nfvswitch.yaml4
-rw-r--r--os_net_config/impl_eni.py11
-rw-r--r--os_net_config/impl_ifcfg.py42
-rw-r--r--os_net_config/objects.py38
-rw-r--r--os_net_config/tests/test_cli.py18
-rw-r--r--os_net_config/tests/test_impl_eni.py18
-rw-r--r--os_net_config/tests/test_impl_ifcfg.py52
-rw-r--r--os_net_config/tests/test_objects.py78
-rw-r--r--os_net_config/tests/test_utils.py168
-rw-r--r--os_net_config/utils.py136
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']