docker-net-dhcp/net-dhcp/udhcpc.py

147 lines
5.1 KiB
Python
Raw Normal View History

2019-08-25 11:48:03 +00:00
from enum import Enum
import ipaddress
2019-08-26 08:30:38 +00:00
import json
import struct
import binascii
2019-08-25 16:49:07 +00:00
import os
2019-08-25 11:48:03 +00:00
from os import path
2019-08-25 23:02:28 +00:00
from select import select
2019-08-25 11:48:03 +00:00
import threading
import subprocess
import logging
2019-08-25 23:02:28 +00:00
from eventfd import EventFD
import posix_ipc
2019-08-25 11:48:03 +00:00
from pyroute2.netns.process.proxy import NSPopen
HANDLER_SCRIPT = path.join(path.dirname(__file__), 'udhcpc_handler.py')
2019-08-25 12:11:35 +00:00
AWAIT_INTERVAL = 0.1
2019-08-26 19:55:10 +00:00
VENDOR_ID = 'docker'
2019-08-25 11:48:03 +00:00
class EventType(Enum):
BOUND = 'bound'
RENEW = 'renew'
2019-08-25 17:12:19 +00:00
DECONFIG = 'deconfig'
LEASEFAIL = 'leasefail'
2019-08-25 11:48:03 +00:00
logger = logging.getLogger('gunicorn.error')
class DHCPClientError(Exception):
pass
def _nspopen_wrapper(netns):
2019-08-25 23:02:28 +00:00
return lambda *args, **kwargs: NSPopen(netns, *args, **kwargs)
2019-08-25 11:48:03 +00:00
class DHCPClient:
def __init__(self, iface, v6=False, once=False, hostname=None, event_listener=None):
2019-08-25 11:48:03 +00:00
self.iface = iface
2019-08-26 12:58:56 +00:00
self.v6 = v6
2019-08-25 11:48:03 +00:00
self.once = once
2019-08-25 16:49:07 +00:00
self.event_listeners = [DHCPClient._attr_listener]
if event_listener:
self.event_listeners.append(event_listener)
2019-08-25 11:48:03 +00:00
2019-08-25 16:49:07 +00:00
self.netns = None
if iface['target'] and iface['target'] != 'localhost':
self.netns = iface['target']
logger.debug('udhcpc using netns %s', self.netns)
Popen = _nspopen_wrapper(self.netns) if self.netns else subprocess.Popen
2019-08-26 12:58:56 +00:00
bin_path = '/usr/bin/udhcpc6' if v6 else '/sbin/udhcpc'
cmdline = [bin_path, '-s', HANDLER_SCRIPT, '-i', iface['ifname'], '-f']
2019-08-25 11:48:03 +00:00
cmdline.append('-q' if once else '-R')
if hostname:
cmdline.append('-x')
if v6:
# TODO: We encode the fqdn for DHCPv6 because udhcpc6 seems to be broken
# flags: S bit set (see RFC4704)
enc_hostname = hostname.encode('utf-8')
enc_hostname = struct.pack('BB', 0b0001, len(enc_hostname)) + enc_hostname
enc_hostname = binascii.hexlify(enc_hostname).decode('ascii')
hostname_opt = f'0x27:{enc_hostname}'
else:
hostname_opt = f'hostname:{hostname}'
cmdline.append(hostname_opt)
2019-08-26 19:55:10 +00:00
if not v6:
cmdline += ['-V', VENDOR_ID]
2019-08-25 23:02:28 +00:00
2019-08-26 12:58:56 +00:00
self._suffix = '6' if v6 else ''
self._event_queue = posix_ipc.MessageQueue(f'/udhcpc{self._suffix}_{iface["address"].replace(":", "_")}', \
2019-08-25 23:02:28 +00:00
flags=os.O_CREAT | os.O_EXCL)
self.proc = Popen(cmdline, env={'EVENT_QUEUE': self._event_queue.name})
if hostname:
logger.debug('[udhcpc%s#%d] using hostname "%s"', self._suffix, self.proc.pid, hostname)
2019-08-25 11:48:03 +00:00
2019-08-25 17:12:19 +00:00
self._has_lease = threading.Event()
2019-08-25 11:48:03 +00:00
self.ip = None
self.gateway = None
self.domain = None
2019-08-25 16:49:07 +00:00
2019-08-25 23:02:28 +00:00
self._shutdown_event = EventFD()
2019-08-25 11:48:03 +00:00
self._event_thread = threading.Thread(target=self._read_events)
self._event_thread.start()
2019-08-26 08:30:38 +00:00
def _attr_listener(self, event_type, event):
2019-08-25 17:12:19 +00:00
if event_type in (EventType.BOUND, EventType.RENEW):
2019-08-26 08:30:38 +00:00
self.ip = ipaddress.ip_interface(event['ip'])
if 'gateway' in event:
self.gateway = ipaddress.ip_address(event['gateway'])
else:
self.gateway = None
self.domain = event.get('domain')
2019-08-25 17:12:19 +00:00
self._has_lease.set()
elif event_type == EventType.DECONFIG:
self._has_lease.clear()
self.ip = None
self.gateway = None
self.domain = None
2019-08-25 16:49:07 +00:00
2019-08-25 11:48:03 +00:00
def _read_events(self):
2019-08-25 23:02:28 +00:00
while True:
r, _w, _e = select([self._shutdown_event, self._event_queue.mqd], [], [])
if self._shutdown_event in r:
break
2019-08-25 16:49:07 +00:00
2019-08-25 23:02:28 +00:00
msg, _priority = self._event_queue.receive()
2019-08-26 08:30:38 +00:00
event = json.loads(msg.decode('utf-8'))
2019-08-25 11:48:03 +00:00
try:
2019-08-26 08:30:38 +00:00
event['type'] = EventType(event['type'])
2019-08-25 11:48:03 +00:00
except ValueError:
2019-08-26 12:58:56 +00:00
logger.warning('udhcpc%s#%d unknown event "%s"', self._suffix, self.proc.pid, event)
2019-08-25 11:48:03 +00:00
continue
2019-08-26 12:58:56 +00:00
logger.debug('[udhcp%s#%d event] %s', self._suffix, self.proc.pid, event)
2019-08-25 16:49:07 +00:00
for listener in self.event_listeners:
2019-08-26 08:30:38 +00:00
try:
listener(self, event['type'], event)
except Exception as ex:
logger.exception(ex)
2019-08-25 11:48:03 +00:00
2019-08-26 08:30:38 +00:00
def await_ip(self, timeout=10):
2019-08-25 17:12:19 +00:00
if not self._has_lease.wait(timeout=timeout):
2019-08-26 12:58:56 +00:00
raise DHCPClientError(f'Timed out waiting for lease from udhcpc{self._suffix}')
2019-08-25 17:12:19 +00:00
2019-08-25 12:11:35 +00:00
return self.ip
def finish(self, timeout=5):
2019-08-26 08:30:38 +00:00
if self._shutdown_event.is_set():
return
if self.proc.returncode is not None and (not self.once or self.proc.returncode != 0):
2019-08-26 12:58:56 +00:00
raise DHCPClientError(f'udhcpc{self._suffix} exited early with code {self.proc.returncode}')
2019-08-25 16:49:07 +00:00
if self.once:
self.await_ip()
else:
2019-08-25 11:48:03 +00:00
self.proc.terminate()
2019-08-25 16:49:07 +00:00
2019-08-25 12:11:35 +00:00
if self.proc.wait(timeout=timeout) != 0:
2019-08-26 12:58:56 +00:00
raise DHCPClientError(f'udhcpc{self._suffix} exited with non-zero exit code {self.proc.returncode}')
2019-08-25 16:49:07 +00:00
if self.netns:
self.proc.release()
2019-08-25 23:02:28 +00:00
self._shutdown_event.set()
2019-08-25 11:48:03 +00:00
self._event_thread.join()
2019-08-25 23:02:28 +00:00
self._event_queue.close()
self._event_queue.unlink()
2019-08-25 11:48:03 +00:00
2019-08-25 16:49:07 +00:00
return self.ip