mirror of
https://github.com/devplayer0/docker-net-dhcp
synced 2024-11-10 13:10:29 +00:00
Initial DHCP functionality
This commit is contained in:
parent
f7d9086ec2
commit
05aec4b0f8
@ -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
|
||||||
|
|
||||||
|
3
Makefile
3
Makefile
@ -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}"
|
||||||
|
@ -19,6 +19,14 @@
|
|||||||
"options": [
|
"options": [
|
||||||
"bind"
|
"bind"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "/var/run/docker/netns",
|
||||||
|
"destination": "/var/run/docker/netns",
|
||||||
|
"type": "bind",
|
||||||
|
"options": [
|
||||||
|
"bind"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"linux": {
|
"linux": {
|
||||||
|
@ -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
73
net-dhcp/udhcpc.py
Normal 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
22
net-dhcp/udhcpc_handler.py
Executable 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)
|
Loading…
Reference in New Issue
Block a user