Improved DHCP client messaging

pull/8/head
Jack O'Sullivan 5 years ago
parent 260d78f374
commit d9772c3ef7

@ -1,7 +1,9 @@
FROM python:3-alpine
COPY requirements.txt /opt/
RUN pip install -r /opt/requirements.txt
RUN apk --no-cache add gcc musl-dev && \
pip install -r /opt/requirements.txt && \
apk --no-cache del gcc musl-dev
RUN mkdir -p /opt/plugin /run/docker/plugins /var/run/docker/netns
COPY net-dhcp/ /opt/plugin/net_dhcp

@ -222,10 +222,11 @@ def join():
ipv6 = ipv6_enabled(req['NetworkID'])
for route in bridge.routes:
if route['type'] != rtypes['RTN_UNICAST'] or (route['family'] == socket.AF_INET6 and not ipv6):
if route['type'] != rtypes['RTN_UNICAST'] or \
(route['family'] == socket.AF_INET6 and not ipv6):
continue
if route['dst'] == '' or route['dst'] == '/0':
if route['dst'] in ('', '/0'):
if route['family'] == socket.AF_INET and 'Gateway' not in res:
logger.info('Adding IPv4 gateway %s', route['gateway'])
res['Gateway'] = route['gateway']
@ -253,15 +254,38 @@ class ContainerDHCPManager:
def __init__(self, network, endpoint):
self.network = network
self.endpoint = endpoint
self.ipv6 = ipv6_enabled(network)
self._thread = threading.Thread(target=self.run)
self._thread.start()
def _on_event(self, dhcp, event_type, _args):
if event_type != udhcpc.EventType.RENEW:
return
for route in dhcp.iface.routes:
if route['type'] != rtypes['RTN_UNICAST'] or \
(route['family'] == socket.AF_INET6 and not self.ipv6) or \
route['dst'] not in ('', '/0'):
continue
# Needed because Route.remove() doesn't like a blank destination
logger.info('Removing default route via %s', route['gateway'])
route['dst'] = '::' if route['family'] == socket.AF_INET6 else '0.0.0.0'
(route
.remove()
.commit())
logger.info('Adding default route via %s', dhcp.gateway)
(dhcp.iface.routes.add({'gateway': dhcp.gateway})
.commit())
def run(self):
iface = endpoint_container_iface(self.network, self.endpoint)
self.dhcp = udhcpc.DHCPClient(iface)
logger.info('Starting DHCP client on %s in container namespace %s', iface['ifname'], \
self.dhcp.netns)
def stop(self):
logger.info('Shutting down DHCP client on %s in container namespace %s', \
self.dhcp.iface['ifname'], self.dhcp.netns)

@ -2,15 +2,15 @@ from enum import Enum
import ipaddress
import os
from os import path
import fcntl
import time
from select import select
import threading
import subprocess
import logging
from eventfd import EventFD
import posix_ipc
from pyroute2.netns.process.proxy import NSPopen
EVENT_PREFIX = '__event'
HANDLER_SCRIPT = path.join(path.dirname(__file__), 'udhcpc_handler.py')
AWAIT_INTERVAL = 0.1
@ -26,13 +26,7 @@ class DHCPClientError(Exception):
pass
def _nspopen_wrapper(netns):
def _wrapper(*args, **kwargs):
# We have to set O_NONBLOCK on stdout since NSPopen uses a global lock
# on the object (e.g. deadlock if we try to readline() and terminate())
proc = NSPopen(netns, *args, **kwargs)
proc.stdout.fcntl(fcntl.F_SETFL, os.O_NONBLOCK)
return proc
return _wrapper
return lambda *args, **kwargs: NSPopen(netns, *args, **kwargs)
class DHCPClient:
def __init__(self, iface, once=False, event_listener=None):
self.iface = iface
@ -49,14 +43,17 @@ class DHCPClient:
Popen = _nspopen_wrapper(self.netns) if self.netns else subprocess.Popen
cmdline = ['/sbin/udhcpc', '-s', HANDLER_SCRIPT, '-i', iface['ifname'], '-f']
cmdline.append('-q' if once else '-R')
self.proc = Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding='utf-8')
self._event_queue = posix_ipc.MessageQueue(f'/udhcpc_{iface["address"].replace(":", "_")}', \
flags=os.O_CREAT | os.O_EXCL)
self.proc = Popen(cmdline, env={'EVENT_QUEUE': self._event_queue.name})
self._has_lease = threading.Event()
self.ip = None
self.gateway = None
self.domain = None
self._running = True
self._shutdown_event = EventFD()
self._event_thread = threading.Thread(target=self._read_events)
self._event_thread.start()
@ -73,21 +70,13 @@ class DHCPClient:
self.domain = None
def _read_events(self):
while self._running:
line = self.proc.stdout.readline().strip()
if not line:
# stdout will be O_NONBLOCK if udhcpc is in a netns
# We can't use select() since the file descriptor is from
# the NSPopen proxy
if self.netns and self._running:
time.sleep(0.1)
continue
while True:
r, _w, _e = select([self._shutdown_event, self._event_queue.mqd], [], [])
if self._shutdown_event in r:
break
if not line.startswith(EVENT_PREFIX):
logger.debug('[udhcpc#%d] %s', self.proc.pid, line)
continue
args = line.split(' ')[1:]
msg, _priority = self._event_queue.receive()
args = msg.decode('utf-8').split(' ')
try:
event_type = EventType(args[0])
except ValueError:
@ -114,7 +103,10 @@ class DHCPClient:
raise DHCPClientError(f'udhcpc exited with non-zero exit code {self.proc.returncode}')
if self.netns:
self.proc.release()
self._running = False
self._shutdown_event.set()
self._event_thread.join()
self._event_queue.close()
self._event_queue.unlink()
return self.ip

@ -2,7 +2,7 @@
import sys
from os import environ as env
EVENT_PREFIX = '__event'
import posix_ipc
if __name__ != '__main__':
print('You shouldn\'t be importing this script!')
@ -10,9 +10,15 @@ if __name__ != '__main__':
event_type = sys.argv[1]
if event_type in ('bound', 'renew'):
print(f'{EVENT_PREFIX} {event_type} {env["ip"]}/{env["mask"]} {env["router"]} {env["domain"]}')
event = f'{event_type} {env["ip"]}/{env["mask"]} {env["router"]} {env["domain"]}'
elif event_type in ('deconfig', 'leasefail', 'nak'):
print(f'{EVENT_PREFIX} {event_type}')
event = event_type
else:
print(f'unknown udhcpc event "{event_type}"')
sys.exit(1)
#with open(env['EVENT_FILE'], 'a') as event_file:
# event_file.write(event + '\n')
queue = posix_ipc.MessageQueue(env['EVENT_QUEUE'])
queue.send(event)
queue.close()

@ -2,3 +2,5 @@ flask==1.1.1
gunicorn==19.9.0
pyroute2==0.5.6
docker==4.0.2
eventfd==0.2
posix_ipc==1.0.4

Loading…
Cancel
Save