diff options
-rw-r--r-- | README.rst | 9 | ||||
-rw-r--r-- | etc/os-net-config/samples/bridge_fail_mode.json | 15 | ||||
-rw-r--r-- | etc/os-net-config/samples/bridge_fail_mode.yaml | 10 | ||||
-rw-r--r-- | etc/os-net-config/samples/bridge_ovs_extra.json | 18 | ||||
-rw-r--r-- | etc/os-net-config/samples/bridge_ovs_extra.yaml | 12 | ||||
-rw-r--r-- | etc/os-net-config/samples/ib_interface.json | 2 | ||||
-rw-r--r-- | etc/os-net-config/samples/ib_interface.yaml | 2 | ||||
-rw-r--r-- | etc/os-net-config/samples/interface.json | 14 | ||||
-rw-r--r-- | etc/os-net-config/samples/interface.yaml | 12 | ||||
-rw-r--r-- | etc/os-net-config/samples/ovs_patch_port.yaml | 2 | ||||
-rw-r--r-- | os_net_config/__init__.py | 32 | ||||
-rw-r--r-- | os_net_config/cli.py | 2 | ||||
-rw-r--r-- | os_net_config/impl_eni.py | 22 | ||||
-rw-r--r-- | os_net_config/impl_ifcfg.py | 112 | ||||
-rw-r--r-- | os_net_config/objects.py | 81 | ||||
-rw-r--r-- | os_net_config/tests/test_impl_eni.py | 85 | ||||
-rw-r--r-- | os_net_config/tests/test_impl_ifcfg.py | 128 | ||||
-rw-r--r-- | os_net_config/tests/test_objects.py | 129 | ||||
-rw-r--r-- | os_net_config/tests/test_utils.py | 24 | ||||
-rw-r--r-- | os_net_config/utils.py | 11 |
20 files changed, 618 insertions, 104 deletions
@@ -1,3 +1,12 @@ +======================== +Team and repository tags +======================== + +.. image:: http://governance.openstack.org/badges/os-net-config.svg + :target: http://governance.openstack.org/reference/tags/index.html + +.. Change things from this point on + =============================== os-net-config =============================== diff --git a/etc/os-net-config/samples/bridge_fail_mode.json b/etc/os-net-config/samples/bridge_fail_mode.json new file mode 100644 index 0000000..6492da6 --- /dev/null +++ b/etc/os-net-config/samples/bridge_fail_mode.json @@ -0,0 +1,15 @@ +{ "network_config": [ + { + "type": "ovs_bridge", + "name": "br-ctlplane", + "ovs_fail_mode": "secure", + "use_dhcp": "true", + "members": [ + { + "type": "interface", + "name": "em1" + } + ] + } + ] +} diff --git a/etc/os-net-config/samples/bridge_fail_mode.yaml b/etc/os-net-config/samples/bridge_fail_mode.yaml new file mode 100644 index 0000000..7589c25 --- /dev/null +++ b/etc/os-net-config/samples/bridge_fail_mode.yaml @@ -0,0 +1,10 @@ +network_config: + - + type: ovs_bridge + name: br-ctlplane + use_dhcp: true + ovs_fail_mode: secure + members: + - + type: interface + name: em1 diff --git a/etc/os-net-config/samples/bridge_ovs_extra.json b/etc/os-net-config/samples/bridge_ovs_extra.json new file mode 100644 index 0000000..d71ead5 --- /dev/null +++ b/etc/os-net-config/samples/bridge_ovs_extra.json @@ -0,0 +1,18 @@ +{ "network_config": [ + { + "type": "ovs_bridge", + "name": "br-ctlplane", + "ovs_extra": [ + "br-set-external-id br-ctlplane bridge-id br-ctlplane", + "set bridge {name} stp_enable=true" + ], + "use_dhcp": "true", + "members": [ + { + "type": "interface", + "name": "em1" + } + ] + } + ] +} diff --git a/etc/os-net-config/samples/bridge_ovs_extra.yaml b/etc/os-net-config/samples/bridge_ovs_extra.yaml new file mode 100644 index 0000000..e369d62 --- /dev/null +++ b/etc/os-net-config/samples/bridge_ovs_extra.yaml @@ -0,0 +1,12 @@ +network_config: + - + type: ovs_bridge + name: br-ctlplane + use_dhcp: true + ovs_extra: + - br-set-external-id br-ctlplane bridge-id br-ctlplane + - set bridge {name} stp_enable=true + members: + - + type: interface + name: em1 diff --git a/etc/os-net-config/samples/ib_interface.json b/etc/os-net-config/samples/ib_interface.json index 4e42867..f552af3 100644 --- a/etc/os-net-config/samples/ib_interface.json +++ b/etc/os-net-config/samples/ib_interface.json @@ -20,7 +20,7 @@ "type": "ib_interface", "name": "ib1", "use_dhcp": true, - "defroute": no + "defroute": false } ] } diff --git a/etc/os-net-config/samples/ib_interface.yaml b/etc/os-net-config/samples/ib_interface.yaml index f930471..6210f0a 100644 --- a/etc/os-net-config/samples/ib_interface.yaml +++ b/etc/os-net-config/samples/ib_interface.yaml @@ -15,4 +15,4 @@ network_config: type: interface name: ib1 use_dhcp: true - defroute: no
\ No newline at end of file + defroute: false diff --git a/etc/os-net-config/samples/interface.json b/etc/os-net-config/samples/interface.json index 8a942b5..7b70e05 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,14 @@ "type": "interface", "name": "em2", "use_dhcp": true, - "defroute": no + "defroute": false, + "ethtool_opts": "speed 1000 duplex full" + }, + { + "type": "interface", + "name": "em3", + "use_dhcp": true, + "hotplug": true } ] } diff --git a/etc/os-net-config/samples/interface.yaml b/etc/os-net-config/samples/interface.yaml index 4f76e07..4c4269e 100644 --- a/etc/os-net-config/samples/interface.yaml +++ b/etc/os-net-config/samples/interface.yaml @@ -11,8 +11,18 @@ 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: false + ethtool_opts: "speed 1000 duplex full" + - + type: interface + name: em3 + use_dhcp: true + hotplug: true diff --git a/etc/os-net-config/samples/ovs_patch_port.yaml b/etc/os-net-config/samples/ovs_patch_port.yaml index 91858be..bae8880 100644 --- a/etc/os-net-config/samples/ovs_patch_port.yaml +++ b/etc/os-net-config/samples/ovs_patch_port.yaml @@ -10,7 +10,7 @@ network_config: # force the MAC address of the bridge to this interface primary: true mtu: 1500 - ovs_extra: "br-set-external-id br-ctlplane bridge-id br-ctlplane" + ovs_extra: ["br-set-external-id br-ctlplane bridge-id br-ctlplane"] - type: ovs_patch_port name: br_pub-patch diff --git a/os_net_config/__init__.py b/os_net_config/__init__.py index 19d4a1e..faa5e92 100644 --- a/os_net_config/__init__.py +++ b/os_net_config/__init__.py @@ -101,105 +101,105 @@ class NetConfig(object): :param interface: The Interface object to add. """ - raise NotImplemented("add_interface is not implemented.") + raise NotImplementedError("add_interface is not implemented.") def add_vlan(self, vlan): """Add a Vlan object to the net config object. :param vlan: The vlan object to add. """ - raise NotImplemented("add_vlan is not implemented.") + raise NotImplementedError("add_vlan is not implemented.") def add_bridge(self, bridge): """Add an OvsBridge object to the net config object. :param bridge: The OvsBridge object to add. """ - raise NotImplemented("add_bridge is not implemented.") + raise NotImplementedError("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.") + raise NotImplementedError("add_ovs_user_bridge is not implemented.") def add_linux_bridge(self, bridge): """Add a LinuxBridge object to the net config object. :param bridge: The LinuxBridge object to add. """ - raise NotImplemented("add_linux_bridge is not implemented.") + raise NotImplementedError("add_linux_bridge is not implemented.") def add_ivs_bridge(self, bridge): """Add a IvsBridge object to the net config object. :param bridge: The IvsBridge object to add. """ - raise NotImplemented("add_ivs_bridge is not implemented.") + raise NotImplementedError("add_ivs_bridge is not implemented.") def add_nfvswitch_bridge(self, bridge): """Add a NfvswitchBridge object to the net config object. :param bridge: The NfvswitchBridge object to add. """ - raise NotImplemented("add_nfvswitch_bridge is not implemented.") + raise NotImplementedError("add_nfvswitch_bridge is not implemented.") def add_bond(self, bond): """Add an OvsBond object to the net config object. :param bond: The OvsBond object to add. """ - raise NotImplemented("add_bond is not implemented.") + raise NotImplementedError("add_bond is not implemented.") def add_linux_bond(self, bond): """Add a LinuxBond object to the net config object. :param bond: The LinuxBond object to add. """ - raise NotImplemented("add_linux_bond is not implemented.") + raise NotImplementedError("add_linux_bond is not implemented.") def add_linux_team(self, team): """Add a LinuxTeam object to the net config object. :param team: The LinuxTeam object to add. """ - raise NotImplemented("add_linux_team is not implemented.") + raise NotImplementedError("add_linux_team is not implemented.") def add_ovs_tunnel(self, tunnel): """Add a OvsTunnel object to the net config object. :param tunnel: The OvsTunnel object to add. """ - raise NotImplemented("add_ovs_tunnel is not implemented.") + raise NotImplementedError("add_ovs_tunnel is not implemented.") def add_ovs_patch_port(self, ovs_patch_port): """Add a OvsPatchPort object to the net config object. :param ovs_patch_port: The OvsPatchPort object to add. """ - raise NotImplemented("add_ovs_patch_port is not implemented.") + raise NotImplementedError("add_ovs_patch_port is not implemented.") def add_ib_interface(self, ib_interface): """Add an InfiniBand Interface object to the net config object. :param interface: The InfiniBand Interface object to add. """ - raise NotImplemented("add_ib_interface is not implemented.") + raise NotImplementedError("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.") + raise NotImplementedError("add_ovs_dpdk_port is not implemented.") def add_ovs_dpdk_bond(self, ovs_dpdk_bond): """Add a OvsDpdkBond object to the net config object. :param ovs_dpdk_bond: The OvsDpdkBond object to add. """ - raise NotImplemented("add_ovs_dpdk_bond is not implemented.") + raise NotImplementedError("add_ovs_dpdk_bond is not implemented.") def apply(self, cleanup=False): """Apply the network configuration. @@ -211,7 +211,7 @@ class NetConfig(object): for each file that was changed (or would be changed if in --noop mode). """ - raise NotImplemented("apply is not implemented.") + raise NotImplementedError("apply is not implemented.") def execute(self, msg, cmd, *args, **kwargs): """Print a message and run a command. diff --git a/os_net_config/cli.py b/os_net_config/cli.py index c0ac5c4..479b3a3 100644 --- a/os_net_config/cli.py +++ b/os_net_config/cli.py @@ -186,7 +186,7 @@ def main(argv=sys.argv): files_changed = provider.apply(cleanup=opts.cleanup, activate=not opts.no_activate) if opts.noop: - for location, data in files_changed.iteritems(): + for location, data in files_changed.items(): print("File: %s\n" % location) print(data) print("----") diff --git a/os_net_config/impl_eni.py b/os_net_config/impl_eni.py index ae60099..360d8c8 100644 --- a/os_net_config/impl_eni.py +++ b/os_net_config/impl_eni.py @@ -127,14 +127,17 @@ class ENINetConfig(os_net_config.NetConfig): data += address_data data += " vlan-raw-device %s\n" % interface.device else: - data += "auto %s\n" % interface.name + if isinstance(interface, objects.Interface) and interface.hotplug: + data += "allow-hotplug %s\n" % interface.name + else: + data += "auto %s\n" % interface.name data += _iface data += address_data if interface.mtu: data += " mtu %i\n" % interface.mtu if interface.hwaddr: - raise NotImplemented("hwaddr is not implemented.") + raise NotImplementedError("hwaddr is not implemented.") if ovs_extra: data += " ovs_extra %s\n" % " -- ".join(ovs_extra) @@ -181,14 +184,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]) @@ -209,12 +215,12 @@ class ENINetConfig(os_net_config.NetConfig): # write out bridges first. This ensures that an ifup -a # on reboot brings them up first - for bridge_name, bridge_data in self.bridges.iteritems(): + for bridge_name, bridge_data in self.bridges.items(): route_data = self.routes.get(bridge_name) bridge_data += (route_data or '') new_config += bridge_data - for interface_name, iface_data in self.interfaces.iteritems(): + for interface_name, iface_data in self.interfaces.items(): route_data = self.routes.get(interface_name) iface_data += (route_data or '') new_config += iface_data diff --git a/os_net_config/impl_ifcfg.py b/os_net_config/impl_ifcfg.py index 56f2b33..6d3c681 100644 --- a/os_net_config/impl_ifcfg.py +++ b/os_net_config/impl_ifcfg.py @@ -16,6 +16,7 @@ import glob import logging +import os import re import os_net_config @@ -25,6 +26,9 @@ from os_net_config import utils logger = logging.getLogger(__name__) +# Import the raw NetConfig object so we can call its methods +netconfig = os_net_config.NetConfig() + def ifcfg_config_path(name): return "/etc/sysconfig/network-scripts/ifcfg-%s" % name @@ -55,6 +59,39 @@ def cleanup_pattern(): return "/etc/sysconfig/network-scripts/ifcfg-*" +def dhclient_path(): + if os.path.exists("/usr/sbin/dhclient"): + return "/usr/sbin/dhclient" + elif os.path.exists("/sbin/dhclient"): + return "/sbin/dhclient" + else: + raise RuntimeError("Could not find dhclient") + + +def stop_dhclient_process(interface): + """Stop a DHCP process when no longer needed. + + This method exists so that it may be stubbed out for unit tests. + :param interface: The interface on which to stop dhclient. + """ + pid_file = '/var/run/dhclient-%s.pid' % (interface) + try: + dhclient = dhclient_path() + except RuntimeError as err: + logger.info('Exception when stopping dhclient: %s' % err) + return + + if os.path.exists(pid_file): + msg = 'Stopping %s on interface %s' % (dhclient, interface) + netconfig.execute(msg, dhclient, '-r', '-pf', + pid_file, interface) + try: + os.unlink(pid_file) + except OSError as err: + logger.error('Could not remove dhclient pid file \'%s\': %s' % + (pid_file, err)) + + class IfcfgNetConfig(os_net_config.NetConfig): """Configure network interfaces using the ifcfg format.""" @@ -94,7 +131,10 @@ class IfcfgNetConfig(os_net_config.NetConfig): data = "# This file is autogenerated by os-net-config\n" data += "DEVICE=%s\n" % base_opt.name data += "ONBOOT=yes\n" - data += "HOTPLUG=no\n" + if isinstance(base_opt, objects.Interface) and base_opt.hotplug: + data += "HOTPLUG=yes\n" + else: + data += "HOTPLUG=no\n" data += "NM_CONTROLLED=no\n" if not base_opt.dns_servers and not base_opt.use_dhcp: data += "PEERDNS=no\n" @@ -113,6 +153,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: @@ -244,15 +286,18 @@ class IfcfgNetConfig(os_net_config.NetConfig): data += "OVS_BRIDGE=%s\n" % base_opt.bridge_name elif isinstance(base_opt, objects.OvsDpdkBond): ovs_extra.extend(base_opt.ovs_extra) - if base_opt.primary_interface_name: - primary_name = base_opt.primary_interface_name - self.bond_primary_ifaces[base_opt.name] = primary_name + # Referring to bug:1643026, the below commenting of the interfaces, + # is to workaround the error, but is not the long term solution. + # The long term solution is to run DPDK options before + # os-net-config, which is being tracked at BUG:1654975 + # if base_opt.primary_interface_name: + # primary_name = base_opt.primary_interface_name + # self.bond_primary_ifaces[base_opt.name] = primary_name data += "DEVICETYPE=ovs\n" data += "TYPE=OVSDPDKBond\n" data += "OVS_BRIDGE=%s\n" % base_opt.bridge_name if base_opt.members: members = [member.name for member in base_opt.members] - self.member_names[base_opt.name] = members data += ("BOND_IFACES=\"%s\"\n" % " ".join(members)) if base_opt.ovs_options: data += "OVS_OPTIONS=\"%s\"\n" % base_opt.ovs_options @@ -262,6 +307,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 +361,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]) @@ -629,8 +682,9 @@ class IfcfgNetConfig(os_net_config.NetConfig): ivs_interfaces = [] # ivs internal ports nfvswitch_interfaces = [] # nfvswitch physical interfaces nfvswitch_internal_ifaces = [] # nfvswitch internal/management ports + stop_dhclient_interfaces = [] - for interface_name, iface_data in self.interface_data.iteritems(): + for interface_name, iface_data in self.interface_data.items(): route_data = self.route_data.get(interface_name, '') route6_data = self.route6_data.get(interface_name, '') interface_path = self.root_dir + ifcfg_config_path(interface_name) @@ -652,11 +706,13 @@ class IfcfgNetConfig(os_net_config.NetConfig): update_files[interface_path] = iface_data update_files[route_path] = route_data update_files[route6_path] = route6_data + if "BOOTPROTO=dhcp" not in iface_data: + stop_dhclient_interfaces.append(interface_name) else: logger.info('No changes required for interface: %s' % interface_name) - for interface_name, iface_data in self.ivsinterface_data.iteritems(): + for interface_name, iface_data in self.ivsinterface_data.items(): route_data = self.route_data.get(interface_name, '') route6_data = self.route6_data.get(interface_name, '') interface_path = self.root_dir + ifcfg_config_path(interface_name) @@ -677,7 +733,7 @@ class IfcfgNetConfig(os_net_config.NetConfig): logger.info('No changes required for ivs interface: %s' % interface_name) - for iface_name, iface_data in self.nfvswitch_intiface_data.iteritems(): + for iface_name, iface_data in self.nfvswitch_intiface_data.items(): route_data = self.route_data.get(iface_name, '') route6_data = self.route6_data.get(iface_name, '') iface_path = self.root_dir + ifcfg_config_path(iface_name) @@ -698,7 +754,7 @@ class IfcfgNetConfig(os_net_config.NetConfig): logger.info('No changes required for nfvswitch interface: %s' % iface_name) - for vlan_name, vlan_data in self.vlan_data.iteritems(): + for vlan_name, vlan_data in self.vlan_data.items(): route_data = self.route_data.get(vlan_name, '') route6_data = self.route6_data.get(vlan_name, '') vlan_path = self.root_dir + ifcfg_config_path(vlan_name) @@ -718,7 +774,7 @@ class IfcfgNetConfig(os_net_config.NetConfig): logger.info('No changes required for vlan interface: %s' % vlan_name) - for bridge_name, bridge_data in self.bridge_data.iteritems(): + for bridge_name, bridge_data in self.bridge_data.items(): route_data = self.route_data.get(bridge_name, '') route6_data = self.route6_data.get(bridge_name, '') bridge_path = self.root_dir + bridge_config_path(bridge_name) @@ -742,7 +798,7 @@ class IfcfgNetConfig(os_net_config.NetConfig): else: logger.info('No changes required for bridge: %s' % bridge_name) - for bridge_name, bridge_data in self.linuxbridge_data.iteritems(): + for bridge_name, bridge_data in self.linuxbridge_data.items(): route_data = self.route_data.get(bridge_name, '') route6_data = self.route6_data.get(bridge_name, '') bridge_path = self.root_dir + bridge_config_path(bridge_name) @@ -762,7 +818,7 @@ class IfcfgNetConfig(os_net_config.NetConfig): else: logger.info('No changes required for bridge: %s' % bridge_name) - for team_name, team_data in self.linuxteam_data.iteritems(): + for team_name, team_data in self.linuxteam_data.items(): route_data = self.route_data.get(team_name, '') route6_data = self.route6_data.get(team_name, '') team_path = self.root_dir + bridge_config_path(team_name) @@ -783,7 +839,7 @@ class IfcfgNetConfig(os_net_config.NetConfig): logger.info('No changes required for linux team: %s' % team_name) - for bond_name, bond_data in self.linuxbond_data.iteritems(): + for bond_name, bond_data in self.linuxbond_data.items(): route_data = self.route_data.get(bond_name, '') route6_data = self.route6_data.get(bond_name, '') bond_path = self.root_dir + bridge_config_path(bond_name) @@ -805,7 +861,7 @@ class IfcfgNetConfig(os_net_config.NetConfig): bond_name) # Infiniband interfaces are handled similarly to Ethernet interfaces - for interface_name, iface_data in self.ib_interface_data.iteritems(): + for interface_name, iface_data in self.ib_interface_data.items(): route_data = self.route_data.get(interface_name, '') route6_data = self.route6_data.get(interface_name, '') interface_path = self.root_dir + ifcfg_config_path(interface_name) @@ -856,10 +912,10 @@ class IfcfgNetConfig(os_net_config.NetConfig): for bridge in restart_bridges: self.ifdown(bridge, iftype='bridge') - for oldname, newname in self.renamed_interfaces.iteritems(): + for oldname, newname in self.renamed_interfaces.items(): self.ifrename(oldname, newname) - for location, data in update_files.iteritems(): + for location, data in update_files.items(): self.write_config(location, data) if ivs_uplinks or ivs_interfaces: @@ -880,6 +936,10 @@ class IfcfgNetConfig(os_net_config.NetConfig): for bridge in restart_bridges: self.ifup(bridge, iftype='bridge') + # If dhclient is running and dhcp not set, stop dhclient + for interface in stop_dhclient_interfaces: + stop_dhclient_process(interface) + for interface in restart_interfaces: self.ifup(interface) diff --git a/os_net_config/objects.py b/os_net_config/objects.py index 3c67ada..3136be0 100644 --- a/os_net_config/objects.py +++ b/os_net_config/objects.py @@ -25,6 +25,8 @@ logger = logging.getLogger(__name__) _MAPPED_NICS = None +DEFAULT_OVS_BRIDGE_FAIL_MODE = 'standalone' + class InvalidConfigException(ValueError): pass @@ -131,20 +133,29 @@ def _mapped_nics(nic_mapping=None): return _MAPPED_NICS +def format_ovs_extra(obj, templates): + """Map OVS object properties into a string to be used for ovs_extra.""" + + return [t.format(name=obj.name) for t in templates or []] + + 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 +287,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, hotplug=False): addresses = addresses or [] routes = routes or [] dns_servers = dns_servers or [] @@ -284,12 +295,17 @@ class Interface(_BaseOpts): routes, mtu, primary, nic_mapping, persist_mapping, defroute, dhclient_args, dns_servers) + self.ethtool_opts = ethtool_opts + self.hotplug = hotplug @staticmethod def from_json(json): name = _get_required_field(json, 'name', 'Interface') + hotplug = strutils.bool_from_string(str(json.get('hotplug', False))) 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, + hotplug=hotplug) class Vlan(_BaseOpts): @@ -386,11 +402,11 @@ class OvsBridge(_BaseOpts): 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): + defroute=True, dhclient_args=None, dns_servers=None, + fail_mode=None): addresses = addresses or [] routes = routes or [] members = members or [] - ovs_extra = ovs_extra or [] dns_servers = dns_servers or [] super(OvsBridge, self).__init__(name, use_dhcp, use_dhcpv6, addresses, routes, mtu, False, nic_mapping, @@ -398,7 +414,10 @@ class OvsBridge(_BaseOpts): dhclient_args, dns_servers) self.members = members self.ovs_options = ovs_options - self.ovs_extra = ovs_extra + ovs_extra = ovs_extra or [] + if fail_mode: + ovs_extra.append('set bridge {name} fail_mode=%s' % fail_mode) + self.ovs_extra = format_ovs_extra(self, ovs_extra) for member in self.members: member.bridge_name = name if not isinstance(member, OvsTunnel): @@ -421,6 +440,9 @@ class OvsBridge(_BaseOpts): json, include_primary=False) ovs_options = json.get('ovs_options') ovs_extra = json.get('ovs_extra', []) + if not isinstance(ovs_extra, list): + ovs_extra = [ovs_extra] + fail_mode = json.get('ovs_fail_mode', DEFAULT_OVS_BRIDGE_FAIL_MODE) members = [] # members @@ -438,7 +460,8 @@ class OvsBridge(_BaseOpts): 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) + dhclient_args=dhclient_args, dns_servers=dns_servers, + fail_mode=fail_mode) class OvsUserBridge(_BaseOpts): @@ -447,7 +470,8 @@ class OvsUserBridge(_BaseOpts): 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): + defroute=True, dhclient_args=None, dns_servers=None, + fail_mode=None): super(OvsUserBridge, self).__init__(name, use_dhcp, use_dhcpv6, addresses, routes, mtu, False, nic_mapping, persist_mapping, @@ -455,7 +479,10 @@ class OvsUserBridge(_BaseOpts): dns_servers) self.members = members or [] self.ovs_options = ovs_options - self.ovs_extra = ovs_extra or [] + ovs_extra = ovs_extra or [] + if fail_mode: + ovs_extra.append('set bridge {name} fail_mode=%s' % fail_mode) + self.ovs_extra = format_ovs_extra(self, ovs_extra) for member in self.members: member.bridge_name = name if not isinstance(member, OvsTunnel) and \ @@ -480,6 +507,9 @@ class OvsUserBridge(_BaseOpts): json, include_primary=False) ovs_options = json.get('ovs_options') ovs_extra = json.get('ovs_extra', []) + if not isinstance(ovs_extra, list): + ovs_extra = [ovs_extra] + fail_mode = json.get('ovs_fail_mode', DEFAULT_OVS_BRIDGE_FAIL_MODE) members = [] # members @@ -498,7 +528,7 @@ class OvsUserBridge(_BaseOpts): ovs_extra=ovs_extra, nic_mapping=nic_mapping, persist_mapping=persist_mapping, defroute=defroute, dhclient_args=dhclient_args, - dns_servers=dns_servers) + dns_servers=dns_servers, fail_mode=fail_mode) class LinuxBridge(_BaseOpts): @@ -803,7 +833,6 @@ class OvsBond(_BaseOpts): addresses = addresses or [] routes = routes or [] members = members or [] - ovs_extra = ovs_extra or [] dns_servers = dns_servers or [] super(OvsBond, self).__init__(name, use_dhcp, use_dhcpv6, addresses, routes, mtu, primary, nic_mapping, @@ -811,7 +840,7 @@ class OvsBond(_BaseOpts): dns_servers) self.members = members self.ovs_options = ovs_options - self.ovs_extra = ovs_extra + self.ovs_extra = format_ovs_extra(self, ovs_extra) for member in self.members: if member.primary: if self.primary_interface_name: @@ -835,6 +864,8 @@ class OvsBond(_BaseOpts): json, include_primary=False) ovs_options = json.get('ovs_options') ovs_extra = json.get('ovs_extra', []) + if not isinstance(ovs_extra, list): + ovs_extra = [ovs_extra] members = [] # members @@ -865,7 +896,6 @@ class OvsTunnel(_BaseOpts): ovs_extra=None): addresses = addresses or [] routes = routes or [] - ovs_extra = ovs_extra or [] dns_servers = dns_servers or [] super(OvsTunnel, self).__init__(name, use_dhcp, use_dhcpv6, addresses, routes, mtu, primary, nic_mapping, @@ -873,7 +903,7 @@ class OvsTunnel(_BaseOpts): dhclient_args, dns_servers) self.tunnel_type = tunnel_type self.ovs_options = ovs_options or [] - self.ovs_extra = ovs_extra or [] + self.ovs_extra = format_ovs_extra(self, ovs_extra) @staticmethod def from_json(json): @@ -882,6 +912,8 @@ class OvsTunnel(_BaseOpts): ovs_options = json.get('ovs_options', []) ovs_options = ['options:%s' % opt for opt in ovs_options] ovs_extra = json.get('ovs_extra', []) + if not isinstance(ovs_extra, list): + ovs_extra = [ovs_extra] opts = _BaseOpts.base_opts_from_json(json) return OvsTunnel(name, *opts, tunnel_type=tunnel_type, ovs_options=ovs_options, ovs_extra=ovs_extra) @@ -897,7 +929,6 @@ class OvsPatchPort(_BaseOpts): ovs_options=None, ovs_extra=None): addresses = addresses or [] routes = routes or [] - ovs_extra = ovs_extra or [] dns_servers = dns_servers or [] super(OvsPatchPort, self).__init__(name, use_dhcp, use_dhcpv6, addresses, routes, mtu, primary, @@ -907,7 +938,7 @@ class OvsPatchPort(_BaseOpts): self.bridge_name = bridge_name self.peer = peer self.ovs_options = ovs_options or [] - self.ovs_extra = ovs_extra or [] + self.ovs_extra = format_ovs_extra(self, ovs_extra) @staticmethod def from_json(json): @@ -917,6 +948,8 @@ class OvsPatchPort(_BaseOpts): ovs_options = json.get('ovs_options', []) ovs_options = ['options:%s' % opt for opt in ovs_options] ovs_extra = json.get('ovs_extra', []) + if not isinstance(ovs_extra, list): + ovs_extra = [ovs_extra] opts = _BaseOpts.base_opts_from_json(json) return OvsPatchPort(name, *opts, bridge_name=bridge_name, peer=peer, ovs_options=ovs_options, ovs_extra=ovs_extra) @@ -928,7 +961,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 [] @@ -936,12 +969,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): @@ -960,7 +995,7 @@ class OvsDpdkPort(_BaseOpts): dns_servers) self.members = members or [] self.ovs_options = ovs_options or [] - self.ovs_extra = ovs_extra or [] + self.ovs_extra = format_ovs_extra(self, ovs_extra) self.driver = driver @staticmethod @@ -998,6 +1033,8 @@ class OvsDpdkPort(_BaseOpts): ovs_options = json.get('ovs_options', []) ovs_options = ['options:%s' % opt for opt in ovs_options] ovs_extra = json.get('ovs_extra', []) + if not isinstance(ovs_extra, list): + ovs_extra = [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) @@ -1017,7 +1054,7 @@ class OvsDpdkBond(_BaseOpts): defroute, dhclient_args, dns_servers) self.members = members or [] self.ovs_options = ovs_options - self.ovs_extra = ovs_extra or [] + self.ovs_extra = format_ovs_extra(self, ovs_extra) for member in self.members: if member.primary: @@ -1042,6 +1079,8 @@ class OvsDpdkBond(_BaseOpts): json, include_primary=False) ovs_options = json.get('ovs_options') ovs_extra = json.get('ovs_extra', []) + if not isinstance(ovs_extra, list): + ovs_extra = [ovs_extra] members = [] # members diff --git a/os_net_config/tests/test_impl_eni.py b/os_net_config/tests/test_impl_eni.py index 7f909ff..4911cb9 100644 --- a/os_net_config/tests/test_impl_eni.py +++ b/os_net_config/tests/test_impl_eni.py @@ -32,6 +32,12 @@ _V4_IFACE_STATIC_IP = _AUTO + """iface eth0 inet static netmask 255.255.255.0 """ +_IFACE_HOTPLUG = """allow-hotplug eth0 +iface eth0 inet static + address 192.168.1.2 + netmask 255.255.255.0 +""" + _V4_IFACE_STATIC_IP_MULTIPLE = (_V4_IFACE_STATIC_IP + _AUTO + """iface eth0 inet static address 10.0.0.2 @@ -66,13 +72,18 @@ iface br0 inet dhcp pre-up ip addr flush dev eth0 """ +_OVS_BRIDGE_DHCP_STANDALONE = _OVS_BRIDGE_DHCP + \ + " ovs_extra set bridge br0 fail_mode=standalone\n" + +_OVS_BRIDGE_DHCP_SECURE = _OVS_BRIDGE_DHCP + \ + " ovs_extra set bridge br0 fail_mode=secure\n" + _OVS_BRIDGE_DHCP_PRIMARY_INTERFACE = _OVS_BRIDGE_DHCP + \ " ovs_extra set bridge br0 other-config:hwaddr=a1:b2:c3:d4:e5\n" - _OVS_BRIDGE_DHCP_OVS_EXTRA = _OVS_BRIDGE_DHCP + \ " ovs_extra set bridge br0 other-config:hwaddr=a1:b2:c3:d4:e5" + \ - " -- br-set-external-id br-ctlplane bridge-id br-ctlplane\n" + " -- br-set-external-id br0 bridge-id br0\n" _VLAN_NO_IP = """auto vlan5 @@ -90,6 +101,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 """ @@ -110,8 +123,9 @@ class TestENINetConfig(base.TestCase): def get_route_config(self): return self.provider.routes[self.if_name] - def _default_interface(self, addr=[], rts=[]): - return objects.Interface(self.if_name, addresses=addr, routes=rts) + def _default_interface(self, addr=[], rts=[], hotplug=False): + return objects.Interface(self.if_name, addresses=addr, routes=rts, + hotplug=hotplug) def test_interface_no_ip(self): interface = self._default_interface() @@ -124,6 +138,12 @@ class TestENINetConfig(base.TestCase): self.provider.add_interface(interface) self.assertEqual(_V4_IFACE_STATIC_IP, self.get_interface_config()) + def test_add_interface_with_hotplug(self): + v4_addr = objects.Address('192.168.1.2/24') + interface = self._default_interface(addr=[v4_addr], hotplug=True) + self.provider.add_interface(interface) + self.assertEqual(_IFACE_HOTPLUG, self.get_interface_config()) + def test_add_interface_with_v4_multiple(self): v4_addresses = [objects.Address('192.168.1.2/24'), objects.Address('10.0.0.2/8')] @@ -169,8 +189,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()) @@ -184,6 +206,28 @@ class TestENINetConfig(base.TestCase): self.assertEqual(_OVS_PORT_IFACE, self.get_interface_config()) self.assertEqual(_OVS_BRIDGE_DHCP, self.provider.bridges['br0']) + def test_network_ovs_bridge_with_standalone_fail_mode(self): + interface = self._default_interface() + bridge = objects.OvsBridge('br0', use_dhcp=True, + members=[interface], + fail_mode='standalone') + self.provider.add_bridge(bridge) + self.provider.add_interface(interface) + self.assertEqual(_OVS_PORT_IFACE, self.get_interface_config()) + self.assertEqual(_OVS_BRIDGE_DHCP_STANDALONE, + self.provider.bridges['br0']) + + def test_network_ovs_bridge_with_secure_fail_mode(self): + interface = self._default_interface() + bridge = objects.OvsBridge('br0', use_dhcp=True, + members=[interface], + fail_mode='secure') + self.provider.add_bridge(bridge) + self.provider.add_interface(interface) + self.assertEqual(_OVS_PORT_IFACE, self.get_interface_config()) + self.assertEqual(_OVS_BRIDGE_DHCP_SECURE, + self.provider.bridges['br0']) + def test_network_ovs_bridge_with_dhcp_and_primary_interface(self): def test_interface_mac(name): @@ -206,7 +250,24 @@ class TestENINetConfig(base.TestCase): self.stubs.Set(utils, 'interface_mac', test_interface_mac) interface = objects.Interface(self.if_name, primary=True) - ovs_extra = "br-set-external-id br-ctlplane bridge-id br-ctlplane" + ovs_extra = "br-set-external-id br0 bridge-id br0" + bridge = objects.OvsBridge('br0', use_dhcp=True, + members=[interface], + ovs_extra=[ovs_extra]) + self.provider.add_bridge(bridge) + self.provider.add_interface(interface) + self.assertEqual(_OVS_PORT_IFACE, self.get_interface_config()) + self.assertEqual(_OVS_BRIDGE_DHCP_OVS_EXTRA, + self.provider.bridges['br0']) + + def test_network_ovs_bridge_with_dhcp_and_primary_with_ovs_format(self): + + def test_interface_mac(name): + return "a1:b2:c3:d4:e5" + self.stubs.Set(utils, 'interface_mac', test_interface_mac) + + interface = objects.Interface(self.if_name, primary=True) + ovs_extra = "br-set-external-id {name} bridge-id {name}" bridge = objects.OvsBridge('br0', use_dhcp=True, members=[interface], ovs_extra=[ovs_extra]) @@ -261,10 +322,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 +336,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 8586daa..9621b8c 100644 --- a/os_net_config/tests/test_impl_ifcfg.py +++ b/os_net_config/tests/test_impl_ifcfg.py @@ -33,6 +33,15 @@ NM_CONTROLLED=no PEERDNS=no """ +_HOTPLUG = """# This file is autogenerated by os-net-config +DEVICE=em1 +ONBOOT=yes +HOTPLUG=yes +NM_CONTROLLED=no +PEERDNS=no +BOOTPROTO=none +""" + _NO_IP = _BASE_IFCFG + "BOOTPROTO=none\n" _V4_IFCFG = _BASE_IFCFG + """BOOTPROTO=static @@ -115,12 +124,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 """ @@ -141,6 +152,13 @@ OVSBOOTPROTO=dhcp OVSDHCPINTERFACES="em1" """ +_OVS_BRIDGE_DHCP_STANDALONE = _OVS_BRIDGE_DHCP + \ + "OVS_EXTRA=\"set bridge br-ctlplane fail_mode=standalone\"\n" + +_OVS_BRIDGE_DHCP_SECURE = _OVS_BRIDGE_DHCP + \ + "OVS_EXTRA=\"set bridge br-ctlplane fail_mode=secure\"\n" + + _LINUX_BRIDGE_DHCP = """# This file is autogenerated by os-net-config DEVICE=br-ctlplane ONBOOT=yes @@ -377,6 +395,11 @@ class TestIfcfgNetConfig(base.TestCase): self.provider.add_interface(interface) self.assertEqual(_NO_IP, self.get_interface_config()) + def test_add_interface_with_hotplug(self): + interface = objects.Interface('em1', hotplug=True) + self.provider.add_interface(interface) + self.assertEqual(_HOTPLUG, self.get_interface_config()) + def test_add_base_interface_vlan(self): interface = objects.Interface('em1.120') self.provider.add_interface(interface) @@ -454,25 +477,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()) @@ -487,6 +520,28 @@ class TestIfcfgNetConfig(base.TestCase): self.assertEqual(_OVS_BRIDGE_DHCP, self.provider.bridge_data['br-ctlplane']) + def test_network_ovs_bridge_with_standalone_fail_mode(self): + interface = objects.Interface('em1') + bridge = objects.OvsBridge('br-ctlplane', use_dhcp=True, + members=[interface], + fail_mode='standalone') + self.provider.add_interface(interface) + self.provider.add_bridge(bridge) + self.assertEqual(_OVS_INTERFACE, self.get_interface_config()) + self.assertEqual(_OVS_BRIDGE_DHCP_STANDALONE, + self.provider.bridge_data['br-ctlplane']) + + def test_network_ovs_bridge_with_secure_fail_mode(self): + interface = objects.Interface('em1') + bridge = objects.OvsBridge('br-ctlplane', use_dhcp=True, + members=[interface], + fail_mode='secure') + self.provider.add_interface(interface) + self.provider.add_bridge(bridge) + self.assertEqual(_OVS_INTERFACE, self.get_interface_config()) + self.assertEqual(_OVS_BRIDGE_DHCP_SECURE, + self.provider.bridge_data['br-ctlplane']) + def test_network_linux_bridge_with_dhcp(self): interface = objects.Interface('em1') bridge = objects.LinuxBridge('br-ctlplane', use_dhcp=True, @@ -565,6 +620,22 @@ class TestIfcfgNetConfig(base.TestCase): self.assertEqual(_OVS_BRIDGE_DHCP_OVS_EXTRA, self.provider.bridge_data['br-ctlplane']) + def test_network_ovs_bridge_with_dhcp_primary_interface_with_format(self): + def test_interface_mac(name): + return "a1:b2:c3:d4:e5" + self.stubs.Set(utils, 'interface_mac', test_interface_mac) + + interface = objects.Interface('em1', primary=True) + ovs_extra = "br-set-external-id {name} bridge-id {name}" + bridge = objects.OvsBridge('br-ctlplane', use_dhcp=True, + members=[interface], + ovs_extra=[ovs_extra]) + self.provider.add_interface(interface) + self.provider.add_bridge(bridge) + self.assertEqual(_OVS_INTERFACE, self.get_interface_config()) + self.assertEqual(_OVS_BRIDGE_DHCP_OVS_EXTRA, + self.provider.bridge_data['br-ctlplane']) + def test_network_ivs_with_uplink_and_interface(self): interface = objects.Interface('em1') v4_addr = objects.Address('172.16.2.7/24') @@ -730,6 +801,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) @@ -843,6 +929,7 @@ class TestIfcfgNetConfigApply(base.TestCase): self.temp_cleanup_file = tempfile.NamedTemporaryFile(delete=False) self.ifup_interface_names = [] self.ovs_appctl_cmds = [] + self.stop_dhclient_interfaces = [] def test_ifcfg_path(name): return self.temp_ifcfg_file.name @@ -864,6 +951,11 @@ class TestIfcfgNetConfigApply(base.TestCase): return self.temp_cleanup_file.name self.stubs.Set(impl_ifcfg, 'cleanup_pattern', test_cleanup_pattern) + def test_stop_dhclient_process(interface): + self.stop_dhclient_interfaces.append(interface) + self.stubs.Set(impl_ifcfg, 'stop_dhclient_process', + test_stop_dhclient_process) + def test_execute(*args, **kwargs): if args[0] == '/sbin/ifup': self.ifup_interface_names.append(args[1]) @@ -884,11 +976,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() @@ -913,6 +1008,21 @@ class TestIfcfgNetConfigApply(base.TestCase): route_data = utils.get_file_data(self.temp_route_file.name) self.assertEqual("", route_data) + def test_dhclient_stop_on_iface_activate(self): + self.stop_dhclient_interfaces = [] + v4_addr = objects.Address('192.168.1.2/24') + interface = objects.Interface('em1', addresses=[v4_addr]) + interface2 = objects.Interface('em2', use_dhcp=True) + interface3 = objects.Interface('em3', use_dhcp=False) + self.provider.add_interface(interface) + self.provider.add_interface(interface2) + self.provider.add_interface(interface3) + self.provider.apply() + # stop dhclient on em1 due to static IP and em3 due to no IP + self.assertIn('em1', self.stop_dhclient_interfaces) + self.assertIn('em3', self.stop_dhclient_interfaces) + self.assertNotIn('em2', self.stop_dhclient_interfaces) + def test_apply_noactivate(self): interface = objects.Interface('em1') bridge = objects.OvsBridge('br-ctlplane', use_dhcp=True, diff --git a/os_net_config/tests/test_objects.py b/os_net_config/tests/test_objects.py index 7500392..f5daf31 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): @@ -94,6 +98,27 @@ class TestInterface(base.TestCase): self.assertEqual("em1", interface.name) self.assertTrue(interface.use_dhcp) + def test_from_json_hotplug(self): + data = """{ +"type": "interface", +"name": "em1", +"hotplug": true +} +""" + interface = objects.object_from_json(json.loads(data)) + self.assertEqual("em1", interface.name) + self.assertTrue(interface.hotplug) + + def test_from_json_hotplug_off_by_default(self): + data = """{ +"type": "interface", +"name": "em1" +} +""" + interface = objects.object_from_json(json.loads(data)) + self.assertEqual("em1", interface.name) + self.assertFalse(interface.hotplug) + def test_from_json_defroute(self): data = '{"type": "interface", "name": "em1", "use_dhcp": true}' interface1 = objects.object_from_json(json.loads(data)) @@ -146,12 +171,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 +187,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): @@ -266,6 +295,34 @@ class TestBridge(base.TestCase): self.assertTrue(interface2.ovs_port) self.assertEqual("br-foo", interface2.bridge_name) + def test_from_json_ovs_extra(self): + data = """{ +"type": "ovs_bridge", +"name": "br-foo", +"ovs_extra": ["bar"], +"ovs_fail_mode": "standalone" +} +""" + bridge = objects.object_from_json(json.loads(data)) + self.assertTrue(2 == len(bridge.ovs_extra)) + self.assertEqual("bar", bridge.ovs_extra[0]) + self.assertEqual("set bridge br-foo fail_mode=standalone", + bridge.ovs_extra[1]) + + def test_from_json_ovs_extra_string(self): + data = """{ +"type": "ovs_bridge", +"name": "br-foo", +"ovs_extra": "bar", +"ovs_fail_mode": "standalone" +} +""" + bridge = objects.object_from_json(json.loads(data)) + self.assertTrue(2 == len(bridge.ovs_extra)) + self.assertEqual("bar", bridge.ovs_extra[0]) + self.assertEqual("set bridge br-foo fail_mode=standalone", + bridge.ovs_extra[1]) + class TestLinuxBridge(base.TestCase): @@ -399,7 +456,7 @@ class TestIvsInterface(base.TestCase): objects.IvsBridge.from_json, json.loads(data)) expected = 'IVS does not support bond interfaces.' - self.assertIn(expected, err) + self.assertIn(expected, six.text_type(err)) class TestNfvswitchBridge(base.TestCase): @@ -464,7 +521,7 @@ class TestNfvswitchInterface(base.TestCase): objects.NfvswitchBridge.from_json, json.loads(data)) expected = 'NFVSwitch does not support bond interfaces.' - self.assertIn(expected, err) + self.assertIn(expected, six.text_type(err)) class TestBond(base.TestCase): @@ -638,6 +695,44 @@ class TestOvsTunnel(base.TestCase): ["ovs extra"], tun0.ovs_extra) + def test_ovs_extra_formatting(self): + data = """{ +"type": "ovs_bridge", +"name": "br-foo", +"ovs_extra": [ + "set bridge {name} something" +], +"members": [{ + "type": "ovs_tunnel", + "name": "tun0", + "tunnel_type": "gre", + "ovs_options": [ + "remote_ip=192.168.1.1" + ], + "ovs_extra": [ + "ovs extra", + "ovs {name} extra" + ] +}] +} +""" + bridge = objects.object_from_json(json.loads(data)) + self.assertEqual("br-foo", bridge.name) + self.assertEqual(["set bridge br-foo something", + "set bridge br-foo fail_mode=standalone"], + bridge.ovs_extra) + tun0 = bridge.members[0] + self.assertEqual("tun0", tun0.name) + self.assertFalse(tun0.ovs_port) + self.assertEqual("br-foo", tun0.bridge_name) + self.assertEqual("gre", tun0.tunnel_type) + self.assertEqual( + ["options:remote_ip=192.168.1.1"], + tun0.ovs_options) + self.assertEqual( + ["ovs extra", "ovs tun0 extra"], + tun0.ovs_extra) + class TestOvsPatchPort(base.TestCase): @@ -654,6 +749,24 @@ class TestOvsPatchPort(base.TestCase): self.assertEqual("br-ex", patch_port.bridge_name) self.assertEqual("br-ex-patch", patch_port.peer) + def test_from_json_with_extra(self): + data = """{ +"type": "ovs_patch_port", +"name": "br-pub-patch", +"bridge_name": "br-ex", +"peer": "br-ex-patch", +"ovs_extra": [ + "ovs {name} extra" +] +} +""" + patch_port = objects.object_from_json(json.loads(data)) + self.assertEqual(["ovs br-pub-patch extra"], + patch_port.ovs_extra) + self.assertEqual("br-pub-patch", patch_port.name) + self.assertEqual("br-ex", patch_port.bridge_name) + self.assertEqual("br-ex-patch", patch_port.peer) + class TestIbInterface(base.TestCase): @@ -727,7 +840,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" }] } """ @@ -742,6 +856,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 b766384..1885cbb 100644 --- a/os_net_config/tests/test_utils.py +++ b/os_net_config/tests/test_utils.py @@ -217,3 +217,27 @@ class TestUtils(base.TestCase): def test_interface_mac_raises(self): self.assertRaises(IOError, utils.interface_mac, 'ens20f2p3') + + def test_is_active_nic_for_sriov_vf(self): + + tmpdir = tempfile.mkdtemp() + self.stubs.Set(utils, '_SYS_CLASS_NET', tmpdir) + + # SR-IOV PF = ens802f0 + # SR-IOV VF = enp129s2 + for nic in ['ens802f0', 'enp129s2']: + nic_path = os.path.join(tmpdir, nic) + os.makedirs(nic_path) + os.makedirs(os.path.join(nic_path, 'device')) + with open(os.path.join(nic_path, 'operstate'), 'w') as f: + f.write('up') + with open(os.path.join(nic_path, 'address'), 'w') as f: + f.write('1.2.3.4') + + nic_path = os.path.join(tmpdir, 'enp129s2', 'device', 'physfn') + os.makedirs(nic_path) + + self.assertEqual(utils._is_active_nic('ens802f0'), True) + self.assertEqual(utils._is_active_nic('enp129s2'), False) + + shutil.rmtree(tmpdir) diff --git a/os_net_config/utils.py b/os_net_config/utils.py index af359d5..98bfe99 100644 --- a/os_net_config/utils.py +++ b/os_net_config/utils.py @@ -107,7 +107,16 @@ def _is_active_nic(interface_name): with open(_SYS_CLASS_NET + '/%s/address' % interface_name, 'r') as f: address = f.read().rstrip() - if has_device_dir and operstate == 'up' and address: + # If SR-IOV Virtual Functions (VF) are enabled in an interface, there + # will be additional nics created for each VF. It has to be ignored in + # the nic numbering. All the VFs will have a reference to the PF with + # directory name as 'physfn', if this directory is present it should be + # ignored. + vf_path_check = _SYS_CLASS_NET + '/%s/device/physfn' % interface_name + is_sriov_vf = os.path.isdir(vf_path_check) + + if (has_device_dir and operstate == 'up' and address and + not is_sriov_vf): return True else: return False |