Initial DHCP functionality

This commit is contained in:
Jack O'Sullivan 2019-08-25 12:48:03 +01:00
parent f7d9086ec2
commit 05aec4b0f8
6 changed files with 135 additions and 19 deletions

View File

@ -3,7 +3,7 @@ FROM python:3-alpine
COPY requirements.txt /opt/ COPY requirements.txt /opt/
RUN pip install -r /opt/requirements.txt RUN pip install -r /opt/requirements.txt
RUN mkdir -p /opt/plugin /run/docker/plugins RUN mkdir -p /opt/plugin /run/docker/plugins /var/run/docker/netns
COPY net-dhcp/ /opt/plugin/net_dhcp COPY net-dhcp/ /opt/plugin/net_dhcp
COPY plugin.sh /opt/plugin/launch.sh COPY plugin.sh /opt/plugin/launch.sh

View File

@ -28,7 +28,8 @@ create:
debug: debug:
@docker run --rm -ti --cap-add CAP_SYS_ADMIN --network host --volume /run/docker/plugins:/run/docker/plugins \ @docker run --rm -ti --cap-add CAP_SYS_ADMIN --network host --volume /run/docker/plugins:/run/docker/plugins \
--volume /run/docker.sock:/run/docker.sock ${PLUGIN_NAME}:rootfs --volume /run/docker.sock:/run/docker.sock --volume /var/run/docker/netns:/var/run/docker/netns \
${PLUGIN_NAME}:rootfs
enable: enable:
@echo "### enable plugin ${PLUGIN_NAME}:${PLUGIN_TAG}" @echo "### enable plugin ${PLUGIN_NAME}:${PLUGIN_TAG}"

View File

@ -19,6 +19,14 @@
"options": [ "options": [
"bind" "bind"
] ]
},
{
"source": "/var/run/docker/netns",
"destination": "/var/run/docker/netns",
"type": "bind",
"options": [
"bind"
]
} }
], ],
"linux": { "linux": {

View File

@ -9,7 +9,7 @@ from pyroute2.netlink.rtnl import rtypes
import docker import docker
from flask import request, jsonify from flask import request, jsonify
from . import NetDhcpError, app from . import NetDhcpError, udhcpc, app
OPTS_KEY = 'com.docker.network.generic' OPTS_KEY = 'com.docker.network.generic'
OPT_PREFIX = 'devplayer0.net-dhcp' OPT_PREFIX = 'devplayer0.net-dhcp'
@ -45,6 +45,8 @@ def get_bridges():
def net_bridge(n): def net_bridge(n):
return ndb.interfaces[client.networks.get(n).attrs['Options'][OPT_BRIDGE]] return ndb.interfaces[client.networks.get(n).attrs['Options'][OPT_BRIDGE]]
def ipv6_enabled(n):
return client.networks.get(n).attrs['EnableIPv6']
@app.route('/NetworkDriver.GetCapabilities', methods=['POST']) @app.route('/NetworkDriver.GetCapabilities', methods=['POST'])
def net_get_capabilities(): def net_get_capabilities():
@ -88,6 +90,10 @@ def create_endpoint():
if_container = (ndb.interfaces[if_container] if_container = (ndb.interfaces[if_container]
.set('state', 'up') .set('state', 'up')
.commit()) .commit())
(bridge
.add_port(if_host)
.commit())
res_iface = { res_iface = {
'MacAddress': '', 'MacAddress': '',
'Address': '', 'Address': '',
@ -107,31 +113,36 @@ def create_endpoint():
for bridge_addr in bridge_addrs: for bridge_addr in bridge_addrs:
if addr.ip == bridge_addr.ip: if addr.ip == bridge_addr.ip:
raise NetDhcpError(400, f'Address {addr} is already in use on bridge {bridge["ifname"]}') raise NetDhcpError(400, f'Address {addr} is already in use on bridge {bridge["ifname"]}')
logger.info('Adding address %s to %s', addr, if_container['ifname'])
elif type_ == 'v4': elif type_ == 'v4':
raise NetDhcpError(400, f'DHCP{type_} is currently unsupported') dhcp = udhcpc.DHCPClient(if_container['ifname'], once=True)
try_addr('v4') dhcp.finish()
try_addr('v6') addr = dhcp.ip
res_iface['Address'] = str(addr)
else:
raise NetDhcpError(400, f'DHCPv6 is currently unsupported')
logger.info('Adding address %s to %s', addr, if_container['ifname'])
(bridge try_addr('v4')
.add_port(if_host) if ipv6_enabled(req['NetworkID']):
.commit()) try_addr('v6')
res = jsonify({ res = jsonify({
'Interface': res_iface 'Interface': res_iface
}) })
except NetDhcpError as e:
(if_host
.remove()
.commit())
logger.error(e)
res = jsonify({'Err': str(e)}), e.status
except Exception as e: except Exception as e:
logger.exception(e)
(bridge
.del_port(if_host)
.commit())
(if_host (if_host
.remove() .remove()
.commit()) .commit())
res = jsonify({'Err': str(e)}), 500
if isinstance(e, NetDhcpError):
res = jsonify({'Err': str(e)}), e.status
else:
res = jsonify({'Err': str(e)}), 500
finally: finally:
return res return res
@ -180,8 +191,9 @@ def join():
}, },
'StaticRoutes': [] 'StaticRoutes': []
} }
ipv6 = ipv6_enabled(req['NetworkID'])
for route in bridge.routes: for route in bridge.routes:
if route['type'] != rtypes['RTN_UNICAST']: if route['type'] != rtypes['RTN_UNICAST'] or (route['family'] == socket.AF_INET6 and not ipv6):
continue continue
if route['dst'] == '' or route['dst'] == '/0': if route['dst'] == '' or route['dst'] == '/0':

73
net-dhcp/udhcpc.py Normal file
View File

@ -0,0 +1,73 @@
from enum import Enum
import ipaddress
from os import path
import threading
import subprocess
import logging
from pyroute2.netns.process.proxy import NSPopen
INFO_PREFIX = '__info'
HANDLER_SCRIPT = path.join(path.dirname(__file__), 'udhcpc_handler.py')
class EventType(Enum):
BOUND = 'bound'
RENEW = 'renew'
logger = logging.getLogger('gunicorn.error')
class DHCPClientError(Exception):
pass
def _nspopen_wrapper(netns):
return lambda *args, **kwargs: NSPopen(netns, *args, **kwargs)
class DHCPClient:
def __init__(self, iface, netns=None, once=False, event_listener=lambda t, ip, gw, dom: None):
self.netns = netns
self.iface = iface
self.once = once
self.event_listener = event_listener
Popen = _nspopen_wrapper(netns) if netns else subprocess.Popen
cmdline = ['/sbin/udhcpc', '-s', HANDLER_SCRIPT, '-i', iface, '-f']
cmdline.append('-q' if once else '-R')
self.proc = Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding='utf-8')
self.ip = None
self.gateway = None
self.domain = None
self._event_thread = threading.Thread(target=self._read_events)
self._event_thread.start()
def _read_events(self):
while True:
line = self.proc.stdout.readline()
if not line:
break
if not line.startswith(INFO_PREFIX):
logger.debug('[udhcpc] %s', line)
continue
args = line.split(' ')[1:]
try:
event_type = EventType(args[0])
except ValueError:
logger.warning('udhcpc unknown event "%s"', ' '.join(args))
continue
self.ip = ipaddress.ip_interface(args[1])
self.gateway = ipaddress.ip_address(args[2])
self.domain = args[3]
logger.debug('[udhcp event] %s %s %s %s', event_type, self.ip, self.gateway, self.domain)
self.event_listener(event_type, self.ip, self.gateway, self.domain)
def finish(self):
if not self.once:
self.proc.terminate()
if self.proc.wait(timeout=10) != 0:
raise DHCPClientError(f'udhcpc exited with non-zero exit code {self.proc.returncode}')
self._event_thread.join()
if self.once and not self.ip:
raise DHCPClientError(f'failed to obtain lease')

22
net-dhcp/udhcpc_handler.py Executable file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
import sys
from os import environ as env
INFO_PREFIX = '__info'
if __name__ != '__main__':
print('You shouldn\'t be importing this script!')
sys.exit(1)
event_type = sys.argv[1]
if event_type == 'bound' or event_type == 'renew':
print(f'{INFO_PREFIX} {event_type} {env["ip"]}/{env["mask"]} {env["router"]} {env["domain"]}')
elif event_type == 'deconfig':
print('udhcpc startup / lost lease')
elif event_type == 'leasefail':
print('udhcpc failed to get a lease')
elif event_type == 'nak':
print('udhcpc received NAK')
else:
print(f'unknown udhcpc event "{event_type}"')
sys.exit(1)