aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDan Sneddon <dsneddon@redhat.com>2016-07-08 13:46:57 -0700
committerDan Sneddon <dsneddon@redhat.com>2016-07-25 21:33:39 +0000
commit311df0c3828fe7571079fad9763bca9e2414b51a (patch)
tree89495aec84485c89a29411515f6685c95e45b760
parentf17add2e8e3c40f79cf211b0cb82a359104ad675 (diff)
Add adapter teaming support using teamd for ifcfg-systems
This change adds support for Linux adapter teams using teamd to manage the bonds instead of the kernel bonding module. Adapter teams using teamd can act like bonds, but also support additional features and possibly more robust fault tolerance. This implementation is fairly straightforward, in order to maintain backward compatibility with templates made for Linux bonds. The only difference in the syntax between the two is type: team instead of type: linux_bond, and the bonding_options format is different. The configuration files for teams should contain the team options as a JSON string. The options that can be used are documented in the teamd.conf(5) man page. If an interface is marked as primary, the priority will be changed from default 0 to 100, making this interface the preferred one. In addition, the MAC address of the Team and all member interfaces will be set to that of the primary interface. At this time, there is no way to set the priority of link members individually, only the interface marked primary will have a non-default priority. This change has been tested on bare metal and worked for a team with two bonded interfaces using LACP. The team was part of an OVS bridge, and there was a VLAN interface on the team. Everything worked as expected. Unit tests are included and passing. Change-Id: If1d516ce8f9ada76375c3a52c5557d3f7348981a Implements: blueprint os-net-config-teaming
-rw-r--r--etc/os-net-config/samples/team.json13
-rw-r--r--etc/os-net-config/samples/team.yaml18
-rw-r--r--os_net_config/__init__.py11
-rw-r--r--os_net_config/impl_ifcfg.py61
-rw-r--r--os_net_config/objects.py59
-rw-r--r--os_net_config/tests/test_impl_ifcfg.py40
-rw-r--r--os_net_config/tests/test_objects.py22
7 files changed, 223 insertions, 1 deletions
diff --git a/etc/os-net-config/samples/team.json b/etc/os-net-config/samples/team.json
new file mode 100644
index 0000000..4524b3c
--- /dev/null
+++ b/etc/os-net-config/samples/team.json
@@ -0,0 +1,13 @@
+{ "network_config": [
+ {
+ "type": "team",
+ "name": "team1",
+ "use_dhcp": true,
+ "bonding_options": "{\"runner\": {\"name\": \"activebackup\"}}",
+ "members": [
+ { "type": "interface", "name": "em1", "primary": true },
+ { "type": "interface", "name": "em2" }
+ ]
+ }
+ ]
+}
diff --git a/etc/os-net-config/samples/team.yaml b/etc/os-net-config/samples/team.yaml
new file mode 100644
index 0000000..3770de0
--- /dev/null
+++ b/etc/os-net-config/samples/team.yaml
@@ -0,0 +1,18 @@
+# Config for bonding with teamd. Bonding options are provided as a JSON
+# string. The following runners are available in teamd: broadcast,
+# roundrobin, activebackup, loadbalance, and lacp.
+# Please see the teamd.conf(5) man page for more information.
+network_config:
+ -
+ type: team
+ name: team1
+ use_dhcp: true
+ bonding_options: '{"runner": {"name": "activebackup"}}'
+ members:
+ -
+ type: interface
+ name: em1
+ primary: true
+ -
+ type: interface
+ name: em2
diff --git a/os_net_config/__init__.py b/os_net_config/__init__.py
index fc1a38a..18c96a2 100644
--- a/os_net_config/__init__.py
+++ b/os_net_config/__init__.py
@@ -71,6 +71,10 @@ class NetConfig(object):
self.add_linux_bond(obj)
for member in obj.members:
self.add_object(member)
+ elif isinstance(obj, objects.LinuxTeam):
+ self.add_linux_team(obj)
+ for member in obj.members:
+ self.add_object(member)
elif isinstance(obj, objects.OvsTunnel):
self.add_ovs_tunnel(obj)
elif isinstance(obj, objects.OvsPatchPort):
@@ -127,6 +131,13 @@ class NetConfig(object):
"""
raise NotImplemented("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.")
+
def add_ovs_tunnel(self, tunnel):
"""Add a OvsTunnel object to the net config object.
diff --git a/os_net_config/impl_ifcfg.py b/os_net_config/impl_ifcfg.py
index 22aef22..49ad317 100644
--- a/os_net_config/impl_ifcfg.py
+++ b/os_net_config/impl_ifcfg.py
@@ -65,6 +65,7 @@ class IfcfgNetConfig(os_net_config.NetConfig):
self.linuxbridge_data = {}
self.linuxbond_data = {}
self.ib_interface_data = {}
+ self.linuxteam_data = {}
self.member_names = {}
self.renamed_interfaces = {}
self.bond_primary_ifaces = {}
@@ -109,11 +110,16 @@ class IfcfgNetConfig(os_net_config.NetConfig):
if base_opt.linux_bond_name:
data += "MASTER=%s\n" % base_opt.linux_bond_name
data += "SLAVE=yes\n"
+ if base_opt.linux_team_name:
+ data += "TEAM_MASTER=%s\n" % base_opt.linux_team_name
+ if base_opt.primary:
+ data += "TEAM_PORT_CONFIG='{\"prio\": 100}'\n"
if base_opt.ivs_bridge_name:
data += "DEVICETYPE=ivs\n"
data += "IVS_BRIDGE=%s\n" % base_opt.ivs_bridge_name
if base_opt.ovs_port:
- data += "DEVICETYPE=ovs\n"
+ if not isinstance(base_opt, objects.LinuxTeam):
+ data += "DEVICETYPE=ovs\n"
if base_opt.bridge_name:
if isinstance(base_opt, objects.Vlan):
data += "TYPE=OVSIntPort\n"
@@ -180,6 +186,19 @@ class IfcfgNetConfig(os_net_config.NetConfig):
self.member_names[base_opt.name] = members
if base_opt.bonding_options:
data += "BONDING_OPTS=\"%s\"\n" % base_opt.bonding_options
+ elif isinstance(base_opt, objects.LinuxTeam):
+ if base_opt.primary_interface_name:
+ primary_name = base_opt.primary_interface_name
+ primary_mac = utils.interface_mac(primary_name)
+ data += "MACADDR=\"%s\"\n" % primary_mac
+ if base_opt.use_dhcp:
+ data += "BOOTPROTO=dhcp\n"
+ if base_opt.members:
+ members = [member.name for member in base_opt.members]
+ self.member_names[base_opt.name] = members
+ data += "DEVICETYPE=Team\n"
+ if base_opt.bonding_options:
+ data += "TEAM_CONFIG='%s'\n" % base_opt.bonding_options
elif isinstance(base_opt, objects.OvsTunnel):
ovs_extra.extend(base_opt.ovs_extra)
data += "DEVICETYPE=ovs\n"
@@ -374,6 +393,18 @@ class IfcfgNetConfig(os_net_config.NetConfig):
if bond.routes:
self._add_routes(bond.name, bond.routes)
+ def add_linux_team(self, team):
+ """Add a LinuxTeam object to the net config object.
+
+ :param team: The LinuxTeam object to add.
+ """
+ logger.info('adding linux team: %s' % team.name)
+ data = self._add_common(team)
+ logger.debug('team data: %s' % data)
+ self.linuxteam_data[team.name] = data
+ if team.routes:
+ self._add_routes(team.name, team.routes)
+
def add_ovs_tunnel(self, tunnel):
"""Add a OvsTunnel object to the net config object.
@@ -451,6 +482,7 @@ class IfcfgNetConfig(os_net_config.NetConfig):
restart_vlans = []
restart_bridges = []
restart_linux_bonds = []
+ restart_linux_teams = []
update_files = {}
all_file_names = []
ivs_uplinks = [] # ivs physical uplinks
@@ -561,6 +593,27 @@ 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():
+ 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)
+ team_route_path = self.root_dir + route_config_path(team_name)
+ team_route6_path = self.root_dir + route6_config_path(team_name)
+ all_file_names.append(team_path)
+ all_file_names.append(team_route_path)
+ all_file_names.append(team_route6_path)
+ if (utils.diff(team_path, team_data) or
+ utils.diff(team_route_path, route_data) or
+ utils.diff(team_route6_path, route6_data)):
+ restart_linux_teams.append(team_name)
+ restart_interfaces.extend(self.child_members(team_name))
+ update_files[team_path] = team_data
+ update_files[team_route_path] = route_data
+ update_files[team_route6_path] = route6_data
+ else:
+ logger.info('No changes required for linux team: %s' %
+ team_name)
+
for bond_name, bond_data in self.linuxbond_data.iteritems():
route_data = self.route_data.get(bond_name, '')
route6_data = self.route6_data.get(bond_name, '')
@@ -628,6 +681,9 @@ class IfcfgNetConfig(os_net_config.NetConfig):
for linux_bond in restart_linux_bonds:
self.ifdown(linux_bond)
+ for linux_team in restart_linux_teams:
+ self.ifdown(linux_team)
+
for bridge in restart_bridges:
self.ifdown(bridge, iftype='bridge')
@@ -646,6 +702,9 @@ class IfcfgNetConfig(os_net_config.NetConfig):
for linux_bond in restart_linux_bonds:
self.ifup(linux_bond)
+ for linux_team in restart_linux_teams:
+ self.ifup(linux_team)
+
for bridge in restart_bridges:
self.ifup(bridge, iftype='bridge')
diff --git a/os_net_config/objects.py b/os_net_config/objects.py
index 289a0da..5137263 100644
--- a/os_net_config/objects.py
+++ b/os_net_config/objects.py
@@ -42,6 +42,8 @@ def object_from_json(json):
return OvsBond.from_json(json)
elif obj_type == "linux_bond":
return LinuxBond.from_json(json)
+ elif obj_type == "team":
+ return LinuxTeam.from_json(json)
elif obj_type == "linux_bridge":
return LinuxBridge.from_json(json)
elif obj_type == "ivs_bridge":
@@ -180,6 +182,7 @@ class _BaseOpts(object):
self.linux_bridge_name = None # internal
self.ivs_bridge_name = None # internal
self.linux_bond_name = None # internal
+ self.linux_team_name = None # internal
self.ovs_port = False # internal
self.primary_interface_name = None # internal
@@ -507,6 +510,62 @@ class IvsBridge(_BaseOpts):
dns_servers=dns_servers)
+class LinuxTeam(_BaseOpts):
+ """Base class for Linux bonds using teamd."""
+
+ def __init__(self, name, use_dhcp=False, use_dhcpv6=False, addresses=None,
+ routes=None, mtu=None, primary=False, members=None,
+ bonding_options=None, nic_mapping=None, persist_mapping=False,
+ defroute=True, dhclient_args=None, dns_servers=None):
+ addresses = addresses or []
+ routes = routes or []
+ members = members or []
+ dns_servers = dns_servers or []
+ super(LinuxTeam, self).__init__(name, use_dhcp, use_dhcpv6, addresses,
+ routes, mtu, primary, nic_mapping,
+ persist_mapping, defroute,
+ dhclient_args, dns_servers)
+ self.members = members
+ self.bonding_options = bonding_options
+ for member in self.members:
+ member.linux_team_name = name
+ if member.primary:
+ if self.primary_interface_name:
+ msg = 'Only one primary interface allowed per team.'
+ raise InvalidConfigException(msg)
+ if member.primary_interface_name:
+ self.primary_interface_name = member.primary_interface_name
+ else:
+ self.primary_interface_name = member.name
+
+ @staticmethod
+ def from_json(json):
+ name = _get_required_field(json, 'name', 'LinuxTeam')
+ (use_dhcp, use_dhcpv6, addresses, routes, mtu, nic_mapping,
+ persist_mapping, defroute, dhclient_args,
+ dns_servers) = _BaseOpts.base_opts_from_json(
+ json, include_primary=False)
+ bonding_options = json.get('bonding_options')
+ members = []
+
+ # members
+ members_json = json.get('members')
+ if members_json:
+ if isinstance(members_json, list):
+ for member in members_json:
+ members.append(object_from_json(member))
+ else:
+ msg = 'Members must be a list.'
+ raise InvalidConfigException(msg)
+
+ return LinuxTeam(name, use_dhcp=use_dhcp, use_dhcpv6=use_dhcpv6,
+ addresses=addresses, routes=routes, mtu=mtu,
+ members=members, bonding_options=bonding_options,
+ nic_mapping=nic_mapping,
+ persist_mapping=persist_mapping, defroute=defroute,
+ dhclient_args=dhclient_args, dns_servers=dns_servers)
+
+
class LinuxBond(_BaseOpts):
"""Base class for Linux bonds."""
diff --git a/os_net_config/tests/test_impl_ifcfg.py b/os_net_config/tests/test_impl_ifcfg.py
index f30eb89..b8a8402 100644
--- a/os_net_config/tests/test_impl_ifcfg.py
+++ b/os_net_config/tests/test_impl_ifcfg.py
@@ -244,11 +244,24 @@ BOOTPROTO=dhcp
"""
+_LINUX_TEAM_DHCP = """# This file is autogenerated by os-net-config
+DEVICE=team0
+ONBOOT=yes
+HOTPLUG=no
+NM_CONTROLLED=no
+BOOTPROTO=dhcp
+DEVICETYPE=Team
+"""
+
+
_LINUX_BOND_INTERFACE = _BASE_IFCFG + """MASTER=bond0
SLAVE=yes
BOOTPROTO=none
"""
+_LINUX_TEAM_INTERFACE = _BASE_IFCFG + """TEAM_MASTER=team0
+BOOTPROTO=none
+"""
_IVS_UPLINK = """# This file is autogenerated by os-net-config
DEVICE=em1
@@ -291,6 +304,17 @@ OVS_BRIDGE=br-ex
OVS_PATCH_PEER=br-ex-patch
"""
+_LINUX_TEAM_PRIMARY_IFACE = """# This file is autogenerated by os-net-config
+DEVICE=em1
+ONBOOT=yes
+HOTPLUG=no
+NM_CONTROLLED=no
+PEERDNS=no
+TEAM_MASTER=team1
+TEAM_PORT_CONFIG='{"prio": 100}'
+BOOTPROTO=none
+"""
+
class TestIfcfgNetConfig(base.TestCase):
@@ -311,6 +335,9 @@ class TestIfcfgNetConfig(base.TestCase):
def get_linux_bond_config(self, name='bond0'):
return self.provider.linuxbond_data[name]
+ def get_linux_team_config(self, name='team0'):
+ return self.provider.linuxteam_data[name]
+
def get_route_config(self, name='em1'):
return self.provider.route_data.get(name, '')
@@ -604,6 +631,19 @@ BOOTPROTO=none
self.assertEqual(_LINUX_BOND_INTERFACE,
self.get_interface_config('em1'))
+ def test_linux_team(self):
+ interface1 = objects.Interface('em1')
+ interface2 = objects.Interface('em2')
+ team = objects.LinuxTeam('team0', use_dhcp=True,
+ members=[interface1, interface2])
+ self.provider.add_linux_team(team)
+ self.provider.add_interface(interface1)
+ self.provider.add_interface(interface2)
+ self.assertEqual(_LINUX_TEAM_DHCP,
+ self.get_linux_team_config('team0'))
+ self.assertEqual(_LINUX_TEAM_INTERFACE,
+ self.get_interface_config('em1'))
+
def test_interface_defroute(self):
interface1 = objects.Interface('em1')
interface2 = objects.Interface('em2', defroute=False)
diff --git a/os_net_config/tests/test_objects.py b/os_net_config/tests/test_objects.py
index 51119ec..17b6927 100644
--- a/os_net_config/tests/test_objects.py
+++ b/os_net_config/tests/test_objects.py
@@ -456,6 +456,28 @@ class TestBond(base.TestCase):
self.assertEqual("em2", interface2.name)
+class TestLinuxTeam(base.TestCase):
+
+ def test_from_json_dhcp(self):
+ data = """{
+"type": "team",
+"name": "team1",
+"use_dhcp": true,
+"members": [
+ { "type": "interface", "name": "em1", "primary": true },
+ { "type": "interface", "name": "em2" }
+]
+}
+"""
+ team = objects.object_from_json(json.loads(data))
+ self.assertEqual("team1", team.name)
+ self.assertTrue(team.use_dhcp)
+ interface1 = team.members[0]
+ self.assertEqual("em1", interface1.name)
+ interface2 = team.members[1]
+ self.assertEqual("em2", interface2.name)
+
+
class TestLinuxBond(base.TestCase):
def test_from_json_dhcp(self):