Skip to content

Commit

Permalink
Merge pull request #10 from ypconfig/fix-routes
Browse files Browse the repository at this point in the history
Fix routes
  • Loading branch information
job committed Jun 7, 2018
2 parents e95e3d3 + 8593116 commit 1f0ae62
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 7 deletions.
15 changes: 14 additions & 1 deletion docs/configuration.md
@@ -1,4 +1,4 @@
ypconfig reads a yaml-file (defaults to /etc/ypconfig/interfaces.yml) and tries to configure the system accordingly. The configfile has the following syntax:
ypconfig reads a yaml-file (defaults to /etc/ypconfig/ypconfig.yml) and tries to configure the system accordingly. The configfile has the following syntax:

An array of interfaces, where each interface has the following options:
- description:
Expand Down Expand Up @@ -39,7 +39,20 @@ An array of interfaces, where each interface has the following options:
- lacp_rate:
Only valid if mode is ```802.3ad```. Can be ```slow``` or ```fast```, defaults to ```slow```.

ROUTES
======
There is a special 'interface' called ```routes```. If the configfile does not have a ```routes```, routes remain ***untouched***. If you do want ypconfig to handle routes, here's how:

routes:
default:
- 192.168.1.1
- fd00::192:168:1:1
172.16.0.0/24:
- 192.168.2.1
fd08::172:16:0:0/64
- fd00::192:168:2:1

Although the routes use an array for nexthop, only one nexthop is currently supported.

EXAMPLE 1
=========
Expand Down
41 changes: 36 additions & 5 deletions ypconfig/config.py
Expand Up @@ -45,8 +45,8 @@ def ipv4(teststring):
else:
raise ValueError("This is not a valid IPv4 address: %s" % (teststring))

def ipv6(teststring):
if teststring.startswith('fe80:'):
def ipv6(teststring, allowlinklocal=False):
if not allowlinklocal and teststring.startswith('fe80:'):
raise ValueError("Do not configure link-local addresses")

try:
Expand All @@ -63,6 +63,12 @@ def IP(ip):
else:
return ipv4(ip)

def SingleIP(ip, allowlinklocal=False):
if ':' in ip:
return ipv6('%s/128' % (ip), allowlinklocal)
else:
return ipv4('%s/32' % (ip))

def Adminstate(state):
if type(int()) == type(state):
state = list(['DOWN', 'UP'])[state]
Expand Down Expand Up @@ -230,16 +236,41 @@ def Interface(iface, iname):
ret['lacp_rate'] = 'slow'
return ret

def Route(gateways, destination):
# Test the destination, which should be a network or 'default'

if destination != 'default':
try:
IP(destination)
except ValueError as e:
raise e

# Now, test all gateways
for gw in gateways:
try:
SingleIP(gw, allowlinklocal=True)
except ValueError as e:
raise e

return gateways

# First, check if any weird values occur
olddoc = deepcopy(document)
for iface in olddoc.keys():
if not document[iface]:
raise ValueError("Empty interface configuration for %s" % (iface))
document[iface] = Interface(document[iface], iface)
if iface == 'routes':
for route in document[iface]:
document[iface][route] = Route(document[iface][route], route)
else:
if not document[iface]:
raise ValueError("Empty interface configuration for %s" % (iface))
document[iface] = Interface(document[iface], iface)

# Then, check if interfaces used in bonds are actually unconfigured
olddoc = deepcopy(document)
for iface in olddoc.keys():
if iface == 'routes':
continue

if document[iface]['type'] == 'bond':
for s in document[iface]['slaves']:
try:
Expand Down
134 changes: 133 additions & 1 deletion ypconfig/netlink.py
Expand Up @@ -2,11 +2,32 @@

from pyroute2 import IPRoute
from pyroute2 import IPDB
from socket import AF_INET, AF_INET6


def GetNow():
ip = IPRoute()
ret = {}

for route in ip.get_routes() + ip.get_routes(family=AF_INET6):
if route.get_attr('RTA_GATEWAY'):
try:
ret['routes']
except KeyError:
ret['routes'] = dict()

if not route.get_attr('RTA_DST'):
dst = 'default'
else:
dst = '/'.join([str(route.get_attr('RTA_DST')), str(route['dst_len'])])

try:
ret['routes'][dst]
except KeyError:
ret['routes'][dst] = list()

ret['routes'][dst].append(route.get_attr('RTA_GATEWAY'))

for iface in ip.get_links():
iname = iface.get_attr('IFLA_IFNAME')
try:
Expand Down Expand Up @@ -82,10 +103,36 @@ def GetNow():

def Commit(cur, new):
global ip

skiproutes = False
changed = False

try:
curroutes = cur['routes']
crouteset = set(cur['routes'].keys())
except KeyError:
crouteset = set()

try:
newroutes = new['routes']
nrouteset = set(new['routes'].keys())
except KeyError:
skiproutes = True
print("No routes configured, skipping routeconfiguration")

try:
del(new['routes'])
except:
pass

try:
del(cur['routes'])
except:
pass

curif = set(cur.keys())
newif = set(new.keys())

changed = False
with IPDB(mode='implicit') as ip:
curif = set(cur.keys())
newif = set(new.keys())
Expand Down Expand Up @@ -178,11 +225,96 @@ def Commit(cur, new):
pass
except Exception as e:
raise e

if not skiproutes:
try:
# These routes should be deleted
for route in crouteset.difference(nrouteset):
changed = True
DelRoute(route)

# These routes should be created
for route in nrouteset.difference(crouteset):
changed = True
AddRoute(route, newroutes[route])

# These routes should be checked
for route in nrouteset.intersection(crouteset):
if curroutes[route] != newroutes[route]:
changed = True
ChangeRoute(route, newroutes[route])

except Exception as e:
raise e

ip.commit()
ip.release()

return changed

def AddRoute(route, gws):
print("Adding route for %s/%s" % (route, gws))
global ip
for d in gws:
ip.routes.add(dst=route, gateway=d)
ip.commit()

def DelRoute(route):
print("Removing route for %s" % (route))
global ip
ip.routes.remove(route)

def DefaultRoute(gws):
global ip
v6new = [ d for d in gws if ':' in d ]
v4new = [ d for d in gws if '.' in d ]
v6cur = list()
v4cur = list()
for r in ip.routes:
if r['dst_len'] != 0:
continue
if r['family'] == 10:
v6cur.append(r['gateway'])
elif r['family'] == 2:
v4cur.append(r['gateway'])

if len(v6new) > len(v6cur):
AddRoute('default', v6new)

if len(v4new) > len(v4cur):
AddRoute('default', v4new)

for r in ip.routes:
if r['dst_len'] != 0:
continue
if r['family'] == 10:
if len(v6new) == 0:
r.remove()
elif len(v6new) == len(v6cur):
if v6new[0] == v6cur[0]:
continue
r.remove()
ip.commit()
AddRoute('default', v6new)
elif r['family'] == 2:
if len(v4new) == 0:
r.remove()
elif len(v4new) == len(v4cur):
if v4new[0] == v4cur[0]:
continue
r.remove()
ip.commit()
AddRoute('default', v4new)

def ChangeRoute(route, gws):
print("Changing route for %s" % (route))
global ip
if route == 'default':
return DefaultRoute(gws)

for d in gws:
ip.routes[route].gateway = d

def Delif(iface):
print("Removing interface %s" % (iface))
global ip
Expand Down

0 comments on commit 1f0ae62

Please sign in to comment.