From d1231d55c01e6440da6f0994b54ec9512ed67849 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Tue, 3 Sep 2019 18:40:34 +0200 Subject: [PATCH 01/17] Start porting to Py3 only and fix old syntax --- pybootd.py | 4 +- pybootd/__init__.py | 2 +- pybootd/daemons.py | 27 ++++++----- pybootd/etc/pybootd.ini | 1 - pybootd/pxed.py | 59 ++++++++++++------------ pybootd/tftpd.py | 39 ++++++++-------- pybootd/util.py | 100 +++++++++++++++++++++++++++------------- setup.py | 6 +-- 8 files changed, 137 insertions(+), 101 deletions(-) diff --git a/pybootd.py b/pybootd.py index 4e7679f..3b937dd 100755 --- a/pybootd.py +++ b/pybootd.py @@ -1,7 +1,7 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # -# Copyright (c) 2010-2016 Emmanuel Blot +# Copyright (c) 2010-2019 Emmanuel Blot # Copyright (c) 2010-2011 Neotion # # This library is free software; you can redistribute it and/or diff --git a/pybootd/__init__.py b/pybootd/__init__.py index 627c1ea..726491b 100644 --- a/pybootd/__init__.py +++ b/pybootd/__init__.py @@ -21,7 +21,7 @@ import os import sys -def _get_package_name(default='', version='1.5.0'): +def _get_package_name(default='', version='1.6.0'): try: from pkg_resources import WorkingSet except ImportError: diff --git a/pybootd/daemons.py b/pybootd/daemons.py index 9e97a7d..af0e60e 100755 --- a/pybootd/daemons.py +++ b/pybootd/daemons.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# Copyright (c) 2010-2016 Emmanuel Blot +# Copyright (c) 2010-2019 Emmanuel Blot # Copyright (c) 2010-2011 Neotion # # This library is free software; you can redistribute it and/or @@ -21,14 +21,13 @@ """Boot up server, a tiny BOOTP/DHCP/TFTP/PXE server""" -import os -import sys -from pxed import BootpServer -from pybootd import pybootd_path, PRODUCT_NAME, __version__ as VERSION -from six import print_ -from tftpd import TftpServer -from util import logger_factory, EasyConfigParser +from os.path import isfile from threading import Thread +from sys import exit as sysexit, modules, stderr +from . import pybootd_path, PRODUCT_NAME, __version__ as VERSION +from .pxed import BootpServer +from .tftpd import TftpServer +from .util import logger_factory, EasyConfigParser class BootpDaemon(Thread): @@ -65,7 +64,7 @@ def main(): debug = False try: from argparse import ArgumentParser - argparser = ArgumentParser(description=sys.modules[__name__].__doc__) + argparser = ArgumentParser(description=modules[__name__].__doc__) argparser.add_argument('-c', '--config', dest='config', default='pybootd/etc/pybootd.ini', help='configuration file') @@ -80,7 +79,7 @@ def main(): args = argparser.parse_args() debug = args.debug - if not os.path.isfile(args.config): + if not isfile(args.config): argparser.error('Invalid configuration file') if args.pxe and args.tftp: @@ -110,10 +109,10 @@ def main(): if not daemon.is_alive(): break except Exception as e: - print_('\nError: %s' % e, file=sys.stderr) + print('\nError: %s' % e, file=stderr) if debug: import traceback - print_(traceback.format_exc(), file=sys.stderr) - sys.exit(1) + print(traceback.format_exc(), file=stderr) + sysexit(1) except KeyboardInterrupt: - print_("Aborting...") + print("Aborting...") diff --git a/pybootd/etc/pybootd.ini b/pybootd/etc/pybootd.ini index 8a41793..af8331a 100644 --- a/pybootd/etc/pybootd.ini +++ b/pybootd/etc/pybootd.ini @@ -14,7 +14,6 @@ lease_time = 86400 access = mac allow_simple_dhcp = enable dns = 10.130.0.2 -boot_file = pxelinux.0 set_gateway = true ; use "nc -l -u 127.0.0.1 -p 12345" to debug ; notify = 192.168.26.201:12345;192.168.26.200:12345 diff --git a/pybootd/pxed.py b/pybootd/pxed.py index cc3ace3..877bc48 100644 --- a/pybootd/pxed.py +++ b/pybootd/pxed.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (c) 2010-2016 Emmanuel Blot +# Copyright (c) 2010-2019 Emmanuel Blot # Copyright (c) 2010-2011 Neotion # # This library is free software; you can redistribute it and/or @@ -25,7 +25,7 @@ import struct import sys import time from binascii import hexlify -from pybootd import PRODUCT_NAME +from . import PRODUCT_NAME from .util import hexline, to_bool, iptoint, inttoip, get_iface_config BOOTP_PORT_REQUEST = 67 @@ -34,10 +34,10 @@ BOOTP_PORT_REPLY = 68 BOOTREQUEST = 1 BOOTREPLY = 2 -BOOTPFormat = '!4bIHH4s4s4s4s16s64s128s64s' -BOOTPFormatSize = struct.calcsize(BOOTPFormat) -DHCPFormat = '!4bIHH4s4s4s4s16s64s128s4s' -DHCPFormatSize = struct.calcsize(DHCPFormat) +BOOTPFORMAT = '!4bIHH4s4s4s4s16s64s128s64s' +BOOTPFORMATSIZE = struct.calcsize(BOOTPFORMAT) +DHCPFORMAT = '!4bIHH4s4s4s4s16s64s128s4s' +DHCPFORMATSIZE = struct.calcsize(DHCPFORMAT) (BOOTP_OP, BOOTP_HTYPE, BOOTP_HLEN, BOOTP_HOPS, BOOTP_XID, BOOTP_SECS, BOOTP_FLAGS, BOOTP_CIADDR, BOOTP_YIADDR, BOOTP_SIADDR, BOOTP_GIADDR, @@ -46,7 +46,7 @@ DHCPFormatSize = struct.calcsize(DHCPFormat) BOOTP_FLAGS_NONE = 0 BOOTP_FLAGS_BROADCAST = 1<<15 -COOKIE='\0x63\0x82\0x53\0x63' +COOKIE = r'\0x63\0x82\0x53\0x63' DHCP_OPTIONS = {0: 'Byte padding', 1: 'Subnet mask', @@ -191,7 +191,8 @@ class BootpServer: host = self.config.get(self.bootp_section, 'address', '0.0.0.0') self.netconfig = get_iface_config(host) if not self.netconfig: - raise BootpError('Unable to detect network configuration') + # the available networks on the host may not match the config... + raise BootpError('Unable to detect a matching network config') keys = sorted(self.netconfig.keys()) self.log.info('Using %s' % ', '.join(map( @@ -204,8 +205,8 @@ class BootpServer: for n in nlist: n = n.strip().split(':') self.notify.append((n[0], int(n[1]))) - except Exception, e: - raise BootpError('Invalid notification URL: %s' % str(e)) + except Exception as exc: + raise BootpError('Invalid notification URL: %s' % exc) access = self.config.get(self.bootp_section, 'access') if not access: self.acl = None @@ -263,9 +264,9 @@ class BootpServer: for sock in r: data, addr = sock.recvfrom(556) self.handle(sock, addr, data) - except Exception, e: + except Exception as exc: import traceback - self.log.critical('%s\n%s' % (str(e), traceback.format_exc())) + self.log.critical('%s\n%s' % (exc, traceback.format_exc())) time.sleep(1) def parse_options(self, tail): @@ -315,8 +316,8 @@ class BootpServer: len(vendor), vendor) buf += struct.pack('!BBB', 255, 0, 0) return buf - except KeyError, e: - self.log.error('Missing options, cancelling: ' + str(e)) + except KeyError as exc: + self.log.error('Missing options, cancelling: %s' % exc) return None def build_dhcp_options(self, clientname): @@ -329,10 +330,10 @@ class BootpServer: def handle(self, sock, addr, data): self.log.info('Sender: %s on socket %s' % (addr, sock.getsockname())) - if len(data) < DHCPFormatSize: + if len(data) < DHCPFORMATSIZE: self.log.error('Cannot be a DHCP or BOOTP request - too small!') - tail = data[DHCPFormatSize:] - buf = list(struct.unpack(DHCPFormat, data[:DHCPFormatSize])) + tail = data[DHCPFORMATSIZE:] + buf = list(struct.unpack(DHCPFORMAT, data[:DHCPFORMATSIZE])) if buf[BOOTP_OP] != BOOTREQUEST: self.log.warn('Not a BOOTREQUEST') return @@ -360,9 +361,9 @@ class BootpServer: uuid = self.uuidpool.get(mac_addr, None) pxe = False self.log.info('PXE UUID not present in request') - uuid_str = uuid and ('%s-%s-%s-%s-%s' % tuple( - [hexlify(x) for x in uuid[0:4], uuid[4:6], uuid[6:8], - uuid[8:10], uuid[10:16]])).upper() + uuid_str = uuid and ('%s-%s-%s-%s-%s' % tuple([hexlify(x) + for x in (uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:16]) + ])).upper() if uuid_str: self.log.info('UUID is %s for MAC %s' % (uuid_str, mac_str)) @@ -441,16 +442,16 @@ class BootpServer: filename = v except ValueError: pass - except urllib2.HTTPError, e: - self.log.error('HTTP Error: %s' % str(e)) + except urllib2.HTTPError as exc: + self.log.error('HTTP Error: %s' % exc) self.states[mac_str] = self.ST_IDLE return - except urllib2.URLError, e: - self.log.critical('Internal error: %s' % str(e)) + except urllib2.URLError as exc: + self.log.critical('Internal error: %s' % exc) self.states[mac_str] = self.ST_IDLE return - except httplib.HTTPException, e: - self.log.error('Server error: %s' % type(e)) + except httplib.HTTPException as exc: + self.log.error('Server error: %s' % type(exc)) self.states[mac_str] = self.ST_IDLE return # local access is only validated if mac address is not yet known @@ -566,7 +567,7 @@ class BootpServer: else: self.log.debug('No filename defined for IP %s' % ip) - pkt = struct.pack(DHCPFormat, *buf) + pkt = struct.pack(DHCPFORMAT, *buf) pkt += struct.pack('!BBB', DHCP_MSG, 1, dhcp_reply) server = socket.inet_aton(server_addr) pkt += struct.pack('!BB4s', DHCP_SERVER, 4, server) @@ -622,7 +623,7 @@ class BootpServer: self.states[mac_str] = newstate def get_dns_servers(self): - nscre = re.compile('nameserver\s+(\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3})\s') + nscre = re.compile(r'nameserver\s+(\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3})\s') result = [] try: with open('/etc/resolv.conf', 'r') as resolv: @@ -632,7 +633,7 @@ class BootpServer: dns = mo.group(1) self.log.info('Found nameserver: %s' % dns) result.append(dns) - except Exception, e: + except Exception: pass if not result: self.log.info('No nameserver found') diff --git a/pybootd/tftpd.py b/pybootd/tftpd.py index 6318b3d..64d0d46 100644 --- a/pybootd/tftpd.py +++ b/pybootd/tftpd.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (c) 2010-2016 Emmanuel Blot +# Copyright (c) 2010-2019 Emmanuel Blot # Copyright (c) 2010-2011 Neotion # # This library is free software; you can redistribute it and/or @@ -25,13 +25,13 @@ import string import struct import sys import time -import thread -import urllib2 -import urlparse -from ConfigParser import NoSectionError -from cStringIO import StringIO -from pybootd import pybootd_path -from util import hexline +from configparser import NoSectionError +from io import StringIO +from threading import Thread +from urllib.parse import urlparse +from urllib.request import urlopen +from . import pybootd_path +from .util import hexline __all__ = ['TftpServer'] @@ -217,8 +217,8 @@ class TftpConnection(object): else: raise TftpError(5, 'Invalid opcode') self.log.debug('End of active: %s:%s' % addr) - except TftpError, detail: - self.send_error(detail[0], detail[1]) + except TftpError as exc: + self.send_error(exc[0], exc[1]) except: import traceback self.log.error(traceback.format_exc()) @@ -310,7 +310,7 @@ class TftpConnection(object): else: try: if self.is_url(resource): - rp = urllib2.urlopen(resource) + rp = urlopen(resource) meta = rp.info() filesize = int(meta.getheaders('Content-Length')[0]) else: @@ -333,7 +333,7 @@ class TftpConnection(object): try: if self.is_url(resource): self.log.info("Sending resource '%s'" % resource) - self.file = urllib2.urlopen(resource) + self.file = urlopen(resource) else: resource = os.path.realpath(resource) self.log.info("Sending file '%s'" % resource) @@ -420,8 +420,9 @@ class TftpServer: r, w, e = select.select(self.sock, [], self.sock) for sock in r: data, addr = sock.recvfrom(516) - t = TftpConnection(self) - thread.start_new_thread(t.connect, (addr, data)) + tc = TftpConnection(self) + thread = Thread(tc.connect, (addr, data)) + thread.start() def filter_file(self, connexion, mo): # extract the position of the matching pattern, then extract the @@ -442,12 +443,12 @@ class TftpServer: for pos, pattern in enumerate(self.config.options('filters'), 1): value = self.config.get('filters', pattern).strip() pattern = pattern.strip('\r\n \t') - pattern = pattern.replace('.', '\.') - pattern = pattern.replace('*', '.*').replace('?', '.') + pattern = pattern.replace(r'.', r'\.') + pattern = pattern.replace(r'*', r'.*').replace(r'?', r'.') pname = 'p%d' % pos replacements[pname] = value - patterns.append('(?P<%s>%s)' % (pname, pattern)) - xre = '^(?:\./)?(?:%s)$' % '|'.join(patterns) + patterns.append(r'(?P<%s>%s)' % (pname, pattern)) + xre = r'^(?:\./)?(?:%s)$' % r'|'.join(patterns) except NoSectionError: - xre = '^$' + xre = r'^$' return (re.compile(xre), replacements) diff --git a/pybootd/util.py b/pybootd/util.py index e1a1c58..70d6ad0 100644 --- a/pybootd/util.py +++ b/pybootd/util.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (c) 2010-2016 Emmanuel Blot +# Copyright (c) 2010-2019 Emmanuel Blot # Copyright (c) 2010-2011 Neotion # # This library is free software; you can redistribute it and/or @@ -18,14 +18,17 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from array import array -import commands -import logging -import re -import socket -import struct -import sys -from ConfigParser import SafeConfigParser -from six import PY3, integer_types, binary_type +from configparser import SafeConfigParser, InterpolationSyntaxError +from logging import (DEBUG, INFO, ERROR, CRITICAL, WARNING, + Formatter, FileHandler, StreamHandler, getLogger) +from logging.handlers import (BufferingHandler, NTEventLogHandler, + SysLogHandler) +from re import match +from socket import inet_aton, inet_ntoa +from subprocess import run +from struct import pack as spack, unpack as sunpack +from sys import stderr + try: import netifaces @@ -35,6 +38,7 @@ except ImportError: raise ImportError('netifaces package is not installed') netifaces = None + # String values evaluated as true boolean values TRUE_BOOLEANS = ['on', 'high', 'true', 'enable', 'enabled', 'yes', '1'] # String values evaluated as false boolean values @@ -56,9 +60,9 @@ def to_int(value): """ if not value: return 0 - if isinstance(value, integer_types): + if isinstance(value, int): return int(value) - mo = re.match('^\s*(\d+)\s*(?:([KMkm]i?)?B?)?\s*$', value) + mo = match(r'^\s*(\d+)\s*(?:([KMkm]i?)?B?)?\s*$', value) if mo: mult = {'K': (1000), 'KI': (1 << 10), @@ -108,7 +112,7 @@ def hexline(data, sep=' '): of the buffer data """ try: - if isinstance(data, (binary_type, array)): + if isinstance(data, (bytes, array)): src = bytearray(data) elif isinstance(data, bytearray): src = data @@ -128,20 +132,20 @@ def hexline(data, sep=' '): def logger_factory(logtype='syslog', logfile=None, level='WARNING', logid='PXEd', format=None): # this code has been copied from Trac (MIT modified license) - logger = logging.getLogger(logid) + logger = getLogger(logid) logtype = logtype.lower() if logtype == 'file': - hdlr = logging.FileHandler(logfile) + hdlr = FileHandler(logfile) elif logtype in ('winlog', 'eventlog', 'nteventlog'): # Requires win32 extensions - hdlr = logging.handlers.NTEventLogHandler(logid, + hdlr = NTEventLogHandler(logid, logtype='Application') elif logtype in ('syslog', 'unix'): - hdlr = logging.handlers.SysLogHandler('/dev/log') + hdlr = SysLogHandler('/dev/log') elif logtype in ('stderr'): - hdlr = logging.StreamHandler(sys.stderr) + hdlr = StreamHandler(stderr) else: - hdlr = logging.handlers.BufferingHandler(0) + hdlr = BufferingHandler(0) if not format: format = 'PXEd[%(module)s] %(levelname)s: %(message)s' @@ -152,23 +156,23 @@ def logger_factory(logtype='syslog', logfile=None, level='WARNING', datefmt = '%X' level = level.upper() if level in ('DEBUG', 'ALL'): - logger.setLevel(logging.DEBUG) + logger.setLevel(DEBUG) elif level == 'INFO': - logger.setLevel(logging.INFO) + logger.setLevel(INFO) elif level == 'ERROR': - logger.setLevel(logging.ERROR) + logger.setLevel(ERROR) elif level == 'CRITICAL': - logger.setLevel(logging.CRITICAL) + logger.setLevel(CRITICAL) else: - logger.setLevel(logging.WARNING) - formatter = logging.Formatter(format, datefmt) + logger.setLevel(WARNING) + formatter = Formatter(format, datefmt) hdlr.setFormatter(formatter) logger.addHandler(hdlr) def logerror(record): import traceback - print_(record.msg) - print_(record.args) + print(record.msg) + print(record.args) traceback.print_exc() # uncomment the following line to show logger formatting error #hdlr.handleError = logerror @@ -177,11 +181,11 @@ def logger_factory(logtype='syslog', logfile=None, level='WARNING', def iptoint(ipstr): - return struct.unpack('!I', socket.inet_aton(ipstr))[0] + return sunpack('!I', inet_aton(ipstr))[0] def inttoip(ipval): - return socket.inet_ntoa(struct.pack('!I', ipval)) + return inet_ntoa(spack('!I', ipval)) def _netifaces_get_iface_config(address): @@ -213,7 +217,7 @@ def _netifaces_get_iface_config(address): def _iproute_get_iface_config(address): pool = iptoint(address) iplines = (line.strip() - for line in commands.getoutput("ip address show").split('\n')) + for line in run("ip address show").stdout.split('\n')) iface = None for l in iplines: items = l.split() @@ -246,11 +250,43 @@ def get_iface_config(address): class EasyConfigParser(SafeConfigParser): - "ConfigParser extension to support default config values" + """ConfigParser extension to support default config values and do not + mess with multi-line option strings""" - def get(self, section, option, default=None): + INDENT_SIZE = 8 + + InterpolationSyntaxError = InterpolationSyntaxError + + def get(self, section, option, default=None, raw=True, vars=None, + fallback=None): + """Return the section:option value if it exists, or the default value + if either the section or the option is missing""" if not self.has_section(section): return default if not self.has_option(section, option): return default - return SafeConfigParser.get(self, section, option) + return SafeConfigParser.get(self, section, option, raw=raw, vars=vars, + fallback=fallback) + + def write(self, filep): + """Write an .ini-format representation of the configuration state, + with automatic line wrapping, using improved multi-line + representation. + """ + for section in self._sections: + filep.write("[%s]\n" % section) + for (key, value) in self._sections[section].items(): + if key != "__name__": + filep.write("%s = %s\n" % + (key, str(value).replace('\n', '\n' + + ' ' * self.INDENT_SIZE))) + filep.write("\n") + + def _interpolate(self, section, option, rawval, vars): + # special overloading of SafeConfigParser._interpolate: + # do not attempt to interpolate if the string is (double-)quoted + if is_quoted(rawval): + return rawval + # cannot use 'super' here as ConfigParser is outdated + return SafeConfigParser._interpolate(self, section, option, + rawval, vars) diff --git a/setup.py b/setup.py index 31cf3b9..8acab9b 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# Copyright (c) 2010-2016 Emmanuel Blot +# Copyright (c) 2010-2019 Emmanuel Blot # Copyright (c) 2010-2011 Neotion # # This library is free software; you can redistribute it and/or @@ -26,7 +26,7 @@ def _read(fname): return open(os.path.join(os.path.dirname(__file__), fname)).read() -requirements = ['six'] +requirements = [] if os.uname()[0].lower() == 'darwin': requirements.append('netifaces (>= 0.5)') @@ -52,7 +52,7 @@ setup( 'Lesser General Public License (LGPL)', 'Operating System :: MacOS :: MacOS X', 'Operating System :: POSIX', - 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.5', 'Topic :: Internet', 'Topic :: System :: Installation/Setup', 'Topic :: System :: Networking', From 02c32b412c69776cc5857f9cb1eba08fce1408c4 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Tue, 3 Sep 2019 19:12:16 +0200 Subject: [PATCH 02/17] More cleanup and removing old useless stuff --- pybootd/__init__.py | 31 ++----------- pybootd/daemons.py | 4 +- pybootd/etc/pybootd.ini | 3 +- pybootd/pxed.py | 100 +++++++++++++++++++--------------------- pybootd/tftpd.py | 70 +++++++++++++--------------- pybootd/util.py | 4 +- 6 files changed, 89 insertions(+), 123 deletions(-) diff --git a/pybootd/__init__.py b/pybootd/__init__.py index 726491b..1e81d7e 100644 --- a/pybootd/__init__.py +++ b/pybootd/__init__.py @@ -18,33 +18,8 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os -import sys - -def _get_package_name(default='', version='1.6.0'): - try: - from pkg_resources import WorkingSet - except ImportError: - ws = [] - else: - ws = WorkingSet() - _path, _ = os.path.split(os.path.dirname( - sys.modules['pybootd'].__file__)) - _path = os.path.normpath(_path) - if 'nt' not in os.name: - for dist in ws: - if os.path.samefile(os.path.normpath(dist.location), _path): - return dist.project_name, dist.version - else: # tweak for windows - _path = os.path.abspath(_path).lower() - for dist in ws: - if 'pybootd' in dist.location: - if _path == os.path.abspath(dist.location).lower(): - return dist.project_name, dist.version - return default, version - - -PRODUCT_NAME, __version__ = _get_package_name('pybootd') +__version__ = '1.7.0' def pybootd_path(path): @@ -61,9 +36,9 @@ def pybootd_path(path): except ImportError: raise IOError('pkg_resources module not available') try: - newpath = resource_filename(Requirement.parse(PRODUCT_NAME), path) + newpath = resource_filename(Requirement.parse('pybootd'), path) if not newpath: - localpath = get_distribution(PRODUCT_NAME).location + localpath = get_distribution('pybootd').location newpath = os.path.join(localpath, path) except DistributionNotFound: newpath = path diff --git a/pybootd/daemons.py b/pybootd/daemons.py index af0e60e..058c042 100755 --- a/pybootd/daemons.py +++ b/pybootd/daemons.py @@ -24,7 +24,7 @@ from os.path import isfile from threading import Thread from sys import exit as sysexit, modules, stderr -from . import pybootd_path, PRODUCT_NAME, __version__ as VERSION +from . import pybootd_path, __version__ from .pxed import BootpServer from .tftpd import TftpServer from .util import logger_factory, EasyConfigParser @@ -94,7 +94,7 @@ def main(): logfile=cfgparser.get('logger', 'file'), level=cfgparser.get('logger', 'level', 'info')) - logger.info('-'.join((PRODUCT_NAME, VERSION))) + logger.info('-'.join(('pybootd', __version__))) daemon = None if not args.tftp: diff --git a/pybootd/etc/pybootd.ini b/pybootd/etc/pybootd.ini index af8331a..912f1e5 100644 --- a/pybootd/etc/pybootd.ini +++ b/pybootd/etc/pybootd.ini @@ -5,7 +5,8 @@ level = info [bootp] address = 0.0.0.0 ; pool_start should be in a valid subnet -pool_start = 192.168.25.100 +; pool_start = 192.168.25.100 +pool_start = 10.113.116.245 pool_count = 5 domain = localdomain server_name = debug diff --git a/pybootd/pxed.py b/pybootd/pxed.py index 877bc48..76ed398 100644 --- a/pybootd/pxed.py +++ b/pybootd/pxed.py @@ -17,15 +17,15 @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -import re -import select -import socket -import string -import struct -import sys -import time from binascii import hexlify -from . import PRODUCT_NAME +from re import compile as recompile +from select import select +from socket import (inet_aton, inet_ntoa, socket, + AF_INET, SOCK_DGRAM, IPPROTO_UDP, SOL_SOCKET, + SO_BROADCAST, SO_REUSEADDR) +from struct import calcsize as scalc, pack as spack, unpack as sunpack +from time import sleep +from traceback import format_exc from .util import hexline, to_bool, iptoint, inttoip, get_iface_config BOOTP_PORT_REQUEST = 67 @@ -35,9 +35,9 @@ BOOTREQUEST = 1 BOOTREPLY = 2 BOOTPFORMAT = '!4bIHH4s4s4s4s16s64s128s64s' -BOOTPFORMATSIZE = struct.calcsize(BOOTPFORMAT) +BOOTPFORMATSIZE = scalc(BOOTPFORMAT) DHCPFORMAT = '!4bIHH4s4s4s4s16s64s128s4s' -DHCPFORMATSIZE = struct.calcsize(DHCPFORMAT) +DHCPFORMATSIZE = scalc(DHCPFORMAT) (BOOTP_OP, BOOTP_HTYPE, BOOTP_HLEN, BOOTP_HOPS, BOOTP_XID, BOOTP_SECS, BOOTP_FLAGS, BOOTP_CIADDR, BOOTP_YIADDR, BOOTP_SIADDR, BOOTP_GIADDR, @@ -177,9 +177,7 @@ class BootpServer: self.ippool = {} # key MAC address string, value assigned IP string self.filepool = {} # key IP string, value pathname self.states = {} # key MAC address string, value client state - name_ = PRODUCT_NAME.split('-') - name_[0] = 'bootp' - self.bootp_section = '_'.join(name_) + self.bootp_section = 'bootp' self.pool_start = self.config.get(self.bootp_section, 'pool_start') if not self.pool_start: raise BootpError('Missing pool_start definition') @@ -236,7 +234,7 @@ class BootpServer: msg = ','.join([notice, uuid_str, mac_str, ip]) else: msg = ','.join([notice, mac_str, ip]) - notify_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + notify_sock = socket(AF_INET, SOCK_DGRAM) for n in self.notify: self.log.info('Notifying %s with %s' % (n, msg)) notify_sock.sendto(msg, n) @@ -249,10 +247,9 @@ class BootpServer: host = self.config.get(self.bootp_section, 'address', '0.0.0.0') port = self.config.get(self.bootp_section, 'port', str(BOOTP_PORT_REQUEST)) - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, - socket.IPPROTO_UDP) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) + sock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1) + sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) self.sock.append(sock) self.log.info('Listening to %s:%s' % (host, port)) sock.bind((host, int(port))) @@ -260,14 +257,13 @@ class BootpServer: def forever(self): while True: try: - r, w, e = select.select(self.sock, [], self.sock) + r, w, e = select(self.sock, [], self.sock) for sock in r: data, addr = sock.recvfrom(556) self.handle(sock, addr, data) except Exception as exc: - import traceback - self.log.critical('%s\n%s' % (exc, traceback.format_exc())) - time.sleep(1) + self.log.critical('%s\n%s' % (exc, format_exc())) + sleep(1) def parse_options(self, tail): self.log.debug('Parsing DHCP options') @@ -280,7 +276,7 @@ class BootpServer: if tag == 0xff: return dhcp_tags length = ord(tail[1]) - (value, ) = struct.unpack('!%ss' % length, tail[2:2+length]) + (value, ) = sunpack('!%ss' % length, tail[2:2+length]) tail = tail[2+length:] try: option = DHCP_OPTIONS[tag] @@ -296,25 +292,25 @@ class BootpServer: try: buf = '' uuid = options[97] - buf += struct.pack('!BB%ds' % len(uuid), + buf += spack('!BB%ds' % len(uuid), 97, len(uuid), uuid) clientclass = options[60] clientclass = clientclass[:clientclass.find(':')] - buf += struct.pack('!BB%ds' % len(clientclass), + buf += spack('!BB%ds' % len(clientclass), 60, len(clientclass), clientclass) vendor = '' - vendor += struct.pack('!BBB', PXE_DISCOVERY_CONTROL, 1, 0x0A) - vendor += struct.pack('!BBHB4s', PXE_BOOT_SERVERS, 2+1+4, + vendor += spack('!BBB', PXE_DISCOVERY_CONTROL, 1, 0x0A) + vendor += spack('!BBHB4s', PXE_BOOT_SERVERS, 2+1+4, 0, 1, server) srvstr = 'Python' - vendor += struct.pack('!BBHB%ds' % len(srvstr), PXE_BOOT_MENU, + vendor += spack('!BBHB%ds' % len(srvstr), PXE_BOOT_MENU, 2+1+len(srvstr), 0, len(srvstr), srvstr) prompt = 'Stupid PXE' - vendor += struct.pack('!BBB%ds' % len(prompt), PXE_MENU_PROMPT, + vendor += spack('!BBB%ds' % len(prompt), PXE_MENU_PROMPT, 1+len(prompt), len(prompt), prompt) - buf += struct.pack('!BB%ds' % len(vendor), 43, + buf += spack('!BB%ds' % len(vendor), 43, len(vendor), vendor) - buf += struct.pack('!BBB', 255, 0, 0) + buf += spack('!BBB', 255, 0, 0) return buf except KeyError as exc: self.log.error('Missing options, cancelling: %s' % exc) @@ -324,7 +320,7 @@ class BootpServer: buf = '' if not clientname: return buf - buf += struct.pack('!BB%ds' % len(clientname), + buf += spack('!BB%ds' % len(clientname), 12, len(clientname), clientname) return buf @@ -333,7 +329,7 @@ class BootpServer: if len(data) < DHCPFORMATSIZE: self.log.error('Cannot be a DHCP or BOOTP request - too small!') tail = data[DHCPFORMATSIZE:] - buf = list(struct.unpack(DHCPFORMAT, data[:DHCPFORMATSIZE])) + buf = list(sunpack(DHCPFORMAT, data[:DHCPFORMATSIZE])) if buf[BOOTP_OP] != BOOTREQUEST: self.log.warn('Not a BOOTREQUEST') return @@ -477,7 +473,7 @@ class BootpServer: # construct reply buf[BOOTP_HOPS] = 0 buf[BOOTP_OP] = BOOTREPLY - self.log.info('Client IP: %s' % socket.inet_ntoa(buf[7])) + self.log.info('Client IP: %s' % inet_ntoa(buf[7])) if buf[BOOTP_CIADDR] == '\x00\x00\x00\x00': self.log.debug('Client needs its address') ipaddr = iptoint(self.pool_start) @@ -501,20 +497,20 @@ class BootpServer: self.bootp_section, 'netmask', self.netconfig['mask'])) reply_broadcast = iptoint(ip) & mask reply_broadcast |= (~mask) & ((1 << 32)-1) - buf[BOOTP_YIADDR] = socket.inet_aton(ip) + buf[BOOTP_YIADDR] = inet_aton(ip) buf[BOOTP_SECS] = 0 buf[BOOTP_FLAGS] = BOOTP_FLAGS_BROADCAST relay = buf[BOOTP_GIADDR] if relay != b'\x00\x00\x00\x00': - addr = (socket.inet_ntoa(relay), addr[1]) + addr = (inet_ntoa(relay), addr[1]) else: addr = (inttoip(reply_broadcast), addr[1]) self.log.info('Reply to: %s:%s' % addr) else: buf[BOOTP_YIADDR] = buf[BOOTP_CIADDR] - ip = socket.inet_ntoa(buf[BOOTP_YIADDR]) - buf[BOOTP_SIADDR] = socket.inet_aton(server_addr) + ip = inet_ntoa(buf[BOOTP_YIADDR]) + buf[BOOTP_SIADDR] = inet_aton(server_addr) # sname buf[BOOTP_SNAME] = \ '.'.join([self.config.get(self.bootp_section, @@ -567,38 +563,38 @@ class BootpServer: else: self.log.debug('No filename defined for IP %s' % ip) - pkt = struct.pack(DHCPFORMAT, *buf) - pkt += struct.pack('!BBB', DHCP_MSG, 1, dhcp_reply) - server = socket.inet_aton(server_addr) - pkt += struct.pack('!BB4s', DHCP_SERVER, 4, server) + pkt = spack(DHCPFORMAT, *buf) + pkt += spack('!BBB', DHCP_MSG, 1, dhcp_reply) + server = inet_aton(server_addr) + pkt += spack('!BB4s', DHCP_SERVER, 4, server) - mask = socket.inet_aton(self.config.get( + mask = inet_aton(self.config.get( self.bootp_section, 'netmask', self.netconfig['mask'])) - pkt += struct.pack('!BB4s', DHCP_IP_MASK, 4, mask) + pkt += spack('!BB4s', DHCP_IP_MASK, 4, mask) gateway_addr = self.config.get(self.bootp_section, 'gateway', '') if gateway_addr: - gateway = socket.inet_aton(gateway_addr) + gateway = inet_aton(gateway_addr) else: gateway = server - pkt += struct.pack('!BB4s', DHCP_IP_GATEWAY, 4, gateway) + pkt += spack('!BB4s', DHCP_IP_GATEWAY, 4, gateway) dns = self.config.get(self.bootp_section, 'dns', None) if dns: if dns.lower() == 'auto': - dns_list = self.get_dns_servers() or [socket.inet_ntoa(server)] + dns_list = self.get_dns_servers() or [inet_ntoa(server)] else: dns_list = dns.split(';') for dns_str in dns_list: - dns_ip = socket.inet_aton(dns_str) - pkt += struct.pack('!BB4s', DHCP_IP_DNS, 4, dns_ip) - pkt += struct.pack('!BBI', DHCP_LEASE_TIME, 4, + dns_ip = inet_aton(dns_str) + pkt += spack('!BB4s', DHCP_IP_DNS, 4, dns_ip) + pkt += spack('!BBI', DHCP_LEASE_TIME, 4, int(self.config.get(self.bootp_section, 'lease_time', str(24*3600)))) - pkt += struct.pack('!BB', DHCP_END, 0) + pkt += spack('!BB', DHCP_END, 0) # do not attempt to produce a PXE-augmented response for # regular DHCP requests @@ -623,7 +619,7 @@ class BootpServer: self.states[mac_str] = newstate def get_dns_servers(self): - nscre = re.compile(r'nameserver\s+(\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3})\s') + nscre = recompile(r'nameserver\s+(\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3})\s') result = [] try: with open('/etc/resolv.conf', 'r') as resolv: diff --git a/pybootd/tftpd.py b/pybootd/tftpd.py index 64d0d46..47ac0ac 100644 --- a/pybootd/tftpd.py +++ b/pybootd/tftpd.py @@ -18,22 +18,21 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os -import re -import select -import socket -import string -import struct -import sys -import time from configparser import NoSectionError from io import StringIO +from re import compile as recompile, sub as resub +from select import select +from socket import socket, AF_INET, SOCK_DGRAM +from struct import pack as spack, unpack as sunpack +from sys import argv, exc_info from threading import Thread +from time import time as now +from traceback import print_exc from urllib.parse import urlparse from urllib.request import urlopen from . import pybootd_path from .util import hexline -__all__ = ['TftpServer'] TFTP_PORT = 69 @@ -86,7 +85,7 @@ class TftpConnection(object): timeout = self.timeout retry = self.server.retry while retry: - r, w, e = select.select([fno], [], [fno], timeout) + r, w, e = select([fno], [], [fno], timeout) if not r: # We timed out -- retransmit retry = retry - 1 @@ -121,13 +120,13 @@ class TftpConnection(object): path = self.server.bootpd.get_filename(client_ip) return path - def parse(self, data, unpack=struct.unpack): + def parse(self, data, unpack=sunpack): self.log.debug('parse') buf = buffer(data) pkt = {} opcode = pkt['opcode'] = unpack('!h', buf[:2])[0] if (opcode == self.RRQ) or (opcode == self.WRQ): - resource, mode, options = string.split(data[2:], '\000', 2) + resource, mode, options = data[2:].split('\000', 2) resource = self.server.fcre.sub(self._filter_file, resource) if self.server.root and self.is_url(self.server.root): resource = '%s/%s' % (self.server.root, resource) @@ -138,7 +137,7 @@ class TftpConnection(object): if not self.server.genfilecre.match(resource): if resource.startswith('^%s' % os.sep): resource = os.path.join( - os.path.dirname(sys.argv[0]), + os.path.dirname(argv[0]), resource.lstrip('^%s' % os.sep)) elif self.server.root: if self.server.root.startswith(os.sep): @@ -147,7 +146,7 @@ class TftpConnection(object): resource) else: # Relative root directory, from the daemon path - daemonpath = os.path.dirname(sys.argv[0]) + daemonpath = os.path.dirname(argv[0]) if not daemonpath.startswith(os.sep): daemonpath = os.path.normpath( os.path.join(os.getcwd(), daemonpath)) @@ -220,8 +219,7 @@ class TftpConnection(object): except TftpError as exc: self.send_error(exc[0], exc[1]) except: - import traceback - self.log.error(traceback.format_exc()) + self.log.error(format_exc()) self.log.debug('Ending connection %s:%s' % addr) def recv_ack(self, pkt): @@ -245,19 +243,19 @@ class TftpConnection(object): self.handle_err(pkt) self.retransmit() - def send_data(self, data, pack=struct.pack): + def send_data(self, data, pack=spack): self.log.debug('send_data') if not self.time: - self.time = time.time() + self.time = now() blocksize = self.blocksize block = self.blockNumber = self.blockNumber + 1 lendata = len(data) - format = '!hh%ds' % lendata - pkt = pack(format, self.DATA, block, data) + fmt = '!hh%ds' % lendata + pkt = pack(fmt, self.DATA, block, data) self.send(pkt) self.active = (len(data) == blocksize) if not self.active and self.time: - total = time.time()-self.time + total = now()-self.time self.time = 0 try: name = self.file.name @@ -271,26 +269,24 @@ class TftpConnection(object): # StringIO does not have a 'name' attribute pass except Exception: - import traceback - traceback.print_exc() - pass + print_exc() - def send_ack(self, pack=struct.pack): + def send_ack(self, pack=spack): self.log.debug('send_ack') block = self.blockNumber self.blockNumber = self.blockNumber + 1 - format = '!hh' - pkt = pack(format, self.ACK, block) + fmt = '!hh' + pkt = pack(fmt, self.ACK, block) self.send(pkt) - def send_error(self, errnum, errtext, pack=struct.pack): + def send_error(self, errnum, errtext, pack=spack): self.log.debug('send_error') errtext = errtext + '\000' - format = '!hh%ds' % len(errtext) - outdata = pack(format, self.ERR, errnum, errtext) + fmt = '!hh%ds' % len(errtext) + outdata = pack(fmt, self.ERR, errnum, errtext) self.sock.sendto(outdata, self.client_addr) - def send_oack(self, options, pack=struct.pack): + def send_oack(self, options, pack=spack): self.log.debug('send_oack') pkt = pack('!h', self.OACK) for k, v in options: @@ -341,7 +337,7 @@ class TftpConnection(object): except Exception: self.send_error(1, 'Cannot open resource') self.log.warn('Cannot open file for reading %s: %s' % - sys.exc_info()[:2]) + exc_info()[:2]) return if 'tsize' not in pkt: self.send_data(self.file.read(self.blocksize)) @@ -359,7 +355,7 @@ class TftpConnection(object): except: self.send_error(1, 'Cannot open file') self.log.error('Cannot open file for writing %s: %s' % - sys.exc_info()[:2]) + exc_info()[:2]) return self.send_ack() @@ -398,7 +394,7 @@ class TftpServer: self.retry = int(self.config.get('tftp', 'blocksize', '5')) self.root = self.config.get('tftp', 'root', os.getcwd()) self.fcre, self.filepatterns = self.get_file_filters() - self.genfilecre = re.compile(r'\[(?P[\w\.\-]+)\]') + self.genfilecre = recompile(r'\[(?P[\w\.\-]+)\]') def bind(self): netconfig = self.bootpd and self.bootpd.get_netconfig() @@ -407,7 +403,7 @@ class TftpServer: if not host: raise TftpError('TFTP address no defined') port = int(self.config.get('tftp', 'port', str(TFTP_PORT))) - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock = socket(AF_INET, SOCK_DGRAM) self.sock.append(sock) sock.bind((host, port)) @@ -417,7 +413,7 @@ class TftpServer: if not self.bootpd.is_alive(): self.log.info('Bootp daemon is dead, exiting') break - r, w, e = select.select(self.sock, [], self.sock) + r, w, e = select(self.sock, [], self.sock) for sock in r: data, addr = sock.recvfrom(516) tc = TftpConnection(self) @@ -433,7 +429,7 @@ class TftpServer: if not filename: continue filepattern = self.filepatterns[group] - return re.sub(r'\{(\w+)\}', connexion._dynreplace, filepattern) + return resub(r'\{(\w+)\}', connexion._dynreplace, filepattern) raise TftpError('Internal error, file matching pattern issue') def get_file_filters(self): @@ -451,4 +447,4 @@ class TftpServer: xre = r'^(?:\./)?(?:%s)$' % r'|'.join(patterns) except NoSectionError: xre = r'^$' - return (re.compile(xre), replacements) + return (recompile(xre), replacements) diff --git a/pybootd/util.py b/pybootd/util.py index 70d6ad0..e7665cd 100644 --- a/pybootd/util.py +++ b/pybootd/util.py @@ -17,7 +17,6 @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -from array import array from configparser import SafeConfigParser, InterpolationSyntaxError from logging import (DEBUG, INFO, ERROR, CRITICAL, WARNING, Formatter, FileHandler, StreamHandler, getLogger) @@ -29,7 +28,6 @@ from subprocess import run from struct import pack as spack, unpack as sunpack from sys import stderr - try: import netifaces except ImportError: @@ -112,7 +110,7 @@ def hexline(data, sep=' '): of the buffer data """ try: - if isinstance(data, (bytes, array)): + if isinstance(data, bytes): src = bytearray(data) elif isinstance(data, bytearray): src = data From 23b8327ff1b830a6c5ace941bfe1ca035b273d3a Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Thu, 5 Sep 2019 11:06:14 +0200 Subject: [PATCH 03/17] Fix several type error issues * bytes vs. int * string vs. bytes --- pybootd/daemons.py | 2 ++ pybootd/pxed.py | 60 ++++++++++++++++++++++------------------------ pybootd/tftpd.py | 4 +++- 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/pybootd/daemons.py b/pybootd/daemons.py index 058c042..e3b8813 100755 --- a/pybootd/daemons.py +++ b/pybootd/daemons.py @@ -29,6 +29,8 @@ from .pxed import BootpServer from .tftpd import TftpServer from .util import logger_factory, EasyConfigParser +#pybootd: disable-msg=broad-except + class BootpDaemon(Thread): diff --git a/pybootd/pxed.py b/pybootd/pxed.py index 76ed398..2f5a36a 100644 --- a/pybootd/pxed.py +++ b/pybootd/pxed.py @@ -26,8 +26,14 @@ from socket import (inet_aton, inet_ntoa, socket, from struct import calcsize as scalc, pack as spack, unpack as sunpack from time import sleep from traceback import format_exc +from urllib.error import HTTPError, URLError +from urllib.parse import urlencode, urlunsplit +from urllib.request import urlopen from .util import hexline, to_bool, iptoint, inttoip, get_iface_config +#pybootd: disable-msg=broad-except + + BOOTP_PORT_REQUEST = 67 BOOTP_PORT_REPLY = 68 @@ -269,13 +275,13 @@ class BootpServer: self.log.debug('Parsing DHCP options') dhcp_tags = {} while tail: - tag = ord(tail[0]) + tag = tail[0] # padding if tag == 0: continue if tag == 0xff: return dhcp_tags - length = ord(tail[1]) + length = tail[1] (value, ) = sunpack('!%ss' % length, tail[2:2+length]) tail = tail[2+length:] try: @@ -289,40 +295,38 @@ class BootpServer: dhcp_tags[tag] = value def build_pxe_options(self, options, server): + buf = b'' try: - buf = '' uuid = options[97] buf += spack('!BB%ds' % len(uuid), - 97, len(uuid), uuid) + 97, len(uuid), uuid) clientclass = options[60] clientclass = clientclass[:clientclass.find(':')] buf += spack('!BB%ds' % len(clientclass), - 60, len(clientclass), clientclass) + 60, len(clientclass), clientclass) vendor = '' vendor += spack('!BBB', PXE_DISCOVERY_CONTROL, 1, 0x0A) vendor += spack('!BBHB4s', PXE_BOOT_SERVERS, 2+1+4, - 0, 1, server) + 0, 1, server) srvstr = 'Python' vendor += spack('!BBHB%ds' % len(srvstr), PXE_BOOT_MENU, - 2+1+len(srvstr), 0, len(srvstr), srvstr) + 2+1+len(srvstr), 0, len(srvstr), srvstr) prompt = 'Stupid PXE' vendor += spack('!BBB%ds' % len(prompt), PXE_MENU_PROMPT, - 1+len(prompt), len(prompt), prompt) + 1+len(prompt), len(prompt), prompt) buf += spack('!BB%ds' % len(vendor), 43, - len(vendor), vendor) + len(vendor), vendor) buf += spack('!BBB', 255, 0, 0) return buf except KeyError as exc: self.log.error('Missing options, cancelling: %s' % exc) - return None + return b'' def build_dhcp_options(self, clientname): - buf = '' if not clientname: - return buf - buf += spack('!BB%ds' % len(clientname), - 12, len(clientname), clientname) - return buf + return b'' + return spack('!BB%ds' % len(clientname), + 12, len(clientname), clientname) def handle(self, sock, addr, data): self.log.info('Sender: %s on socket %s' % (addr, sock.getsockname())) @@ -340,13 +344,13 @@ class BootpServer: # Extras (DHCP options) try: - dhcp_msg_type = ord(options[53][0]) + dhcp_msg_type = options[53][0] except KeyError: dhcp_msg_type = None server_addr = self.netconfig['server'] mac_addr = buf[BOOTP_CHADDR][:6] - mac_str = ':'.join(['%02X' % ord(x) for x in mac_addr]) + mac_str = ':'.join(['%02X' % x for x in mac_addr]) # is the UUID received (PXE mode) if 97 in options and len(options[97]) == 17: uuid = options[97][1:] @@ -401,8 +405,6 @@ class BootpServer: # remote access is always validated on each request if self.access in self.ACCESS_REMOTE: # need to query a host to grant or reject access - import urlparse - import urllib netloc = self.config.get(self.access, 'location') path = self.config.get(self.access, pxe and 'pxe' or 'dhcp') timeout = int(self.config.get(self.access, 'timeout', '5')) @@ -419,14 +421,12 @@ class BootpServer: if to_bool(always_check): checkhost = True if checkhost: - query = urllib.urlencode(parameters) + query = urlencode(parameters) urlparts = (self.access, netloc, path, query, '') - url = urlparse.urlunsplit(urlparts) + url = urlunsplit(urlparts) self.log.info('Requesting URL: %s' % url) - import urllib2 - import httplib try: - up = urllib2.urlopen(url, timeout=timeout) + up = urlopen(url, timeout=timeout) for l in up: try: # Look for extra definition within the reply @@ -438,18 +438,14 @@ class BootpServer: filename = v except ValueError: pass - except urllib2.HTTPError as exc: + except HTTPError as exc: self.log.error('HTTP Error: %s' % exc) self.states[mac_str] = self.ST_IDLE return - except urllib2.URLError as exc: + except URLError as exc: self.log.critical('Internal error: %s' % exc) self.states[mac_str] = self.ST_IDLE return - except httplib.HTTPException as exc: - self.log.error('Server error: %s' % type(exc)) - self.states[mac_str] = self.ST_IDLE - return # local access is only validated if mac address is not yet known elif mac_str not in self.ippool: item = locals()['%s_str' % self.access] @@ -516,10 +512,10 @@ class BootpServer: '.'.join([self.config.get(self.bootp_section, 'servername', 'unknown'), self.config.get(self.bootp_section, - 'domain', 'localdomain')]) + 'domain', 'localdomain')]).encode() # file buf[BOOTP_FILE] = self.config.get(self.bootp_section, - 'boot_file', '\x00') + 'boot_file', '\x00').encode() if not dhcp_msg_type: self.log.warn('No DHCP message type found, discarding request') diff --git a/pybootd/tftpd.py b/pybootd/tftpd.py index 47ac0ac..6c2598e 100644 --- a/pybootd/tftpd.py +++ b/pybootd/tftpd.py @@ -33,6 +33,8 @@ from urllib.request import urlopen from . import pybootd_path from .util import hexline +#pybootd: disable-msg=broad-except + TFTP_PORT = 69 @@ -122,7 +124,7 @@ class TftpConnection(object): def parse(self, data, unpack=sunpack): self.log.debug('parse') - buf = buffer(data) + buf = data pkt = {} opcode = pkt['opcode'] = unpack('!h', buf[:2])[0] if (opcode == self.RRQ) or (opcode == self.WRQ): From 841274ca524e629f3e2aef539e91e2e47382d9a3 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Thu, 5 Sep 2019 11:06:38 +0200 Subject: [PATCH 04/17] Do not use `uname()` --- pybootd/util.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/pybootd/util.py b/pybootd/util.py index e7665cd..5fac6e5 100644 --- a/pybootd/util.py +++ b/pybootd/util.py @@ -26,15 +26,14 @@ from re import match from socket import inet_aton, inet_ntoa from subprocess import run from struct import pack as spack, unpack as sunpack -from sys import stderr +from sys import platform, stderr try: - import netifaces + import netifaces as nif except ImportError: - import os - if os.uname()[0].lower() == 'darwin': + if platform == 'darwin': raise ImportError('netifaces package is not installed') - netifaces = None + nif = None # String values evaluated as true boolean values @@ -188,11 +187,11 @@ def inttoip(ipval): def _netifaces_get_iface_config(address): pool = iptoint(address) - for iface in netifaces.interfaces(): - ifinfo = netifaces.ifaddresses(iface) - if netifaces.AF_INET not in ifinfo: + for iface in nif.interfaces(): + ifinfo = nif.ifaddresses(iface) + if nif.AF_INET not in ifinfo: continue - for inetinfo in netifaces.ifaddresses(iface)[netifaces.AF_INET]: + for inetinfo in nif.ifaddresses(iface)[nif.AF_INET]: addr_s = inetinfo.get('addr') netmask_s = inetinfo.get('netmask') if addr_s is None or netmask_s is None: @@ -242,9 +241,8 @@ def _iproute_get_iface_config(address): def get_iface_config(address): if not address: return None - if not netifaces: - return _iproute_get_iface_config(address) - return _netifaces_get_iface_config(address) + nifcfg = _netifaces_get_iface_config if nif else _iproute_get_iface_config + return nifcfg(address) class EasyConfigParser(SafeConfigParser): From 38bd055b150f11b8c687e4f1eabd59ff1bfc3453 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Thu, 5 Sep 2019 11:52:52 +0200 Subject: [PATCH 05/17] More fixin'... --- pybootd/daemons.py | 12 ++++++----- pybootd/pxed.py | 28 +++++++++++++++--------- pybootd/tests/httpd.py | 2 +- pybootd/tftpd.py | 48 ++++++++++++++++++++++++------------------ pybootd/util.py | 12 +---------- 5 files changed, 54 insertions(+), 48 deletions(-) diff --git a/pybootd/daemons.py b/pybootd/daemons.py index e3b8813..a573610 100755 --- a/pybootd/daemons.py +++ b/pybootd/daemons.py @@ -24,6 +24,8 @@ from os.path import isfile from threading import Thread from sys import exit as sysexit, modules, stderr +from traceback import format_exc + from . import pybootd_path, __version__ from .pxed import BootpServer from .tftpd import TftpServer @@ -110,11 +112,11 @@ def main(): daemon.join(0.5) if not daemon.is_alive(): break - except Exception as e: - print('\nError: %s' % e, file=stderr) + except Exception as exc: + print('\nError: %s' % exc, file=stderr) if debug: - import traceback - print(traceback.format_exc(), file=stderr) + print(format_exc(chain=False), file=stderr) sysexit(1) except KeyboardInterrupt: - print("Aborting...") + print("\nAborting...") + sysexit(2) diff --git a/pybootd/pxed.py b/pybootd/pxed.py index 2f5a36a..a573e29 100644 --- a/pybootd/pxed.py +++ b/pybootd/pxed.py @@ -31,7 +31,14 @@ from urllib.parse import urlencode, urlunsplit from urllib.request import urlopen from .util import hexline, to_bool, iptoint, inttoip, get_iface_config -#pybootd: disable-msg=broad-except +#pylint: disable-msg=broad-except +#pylint: disable-msg=invalid-name +#pylint: disable-msg=missing-docstring +#pylint: disable-msg=too-many-return-statements +#pylint: disable-msg=too-many-branches +#pylint: disable-msg=too-many-locals +#pylint: disable-msg=too-many-statements +#pylint: disable-msg=too-many-nested-blocks BOOTP_PORT_REQUEST = 67 @@ -263,7 +270,7 @@ class BootpServer: def forever(self): while True: try: - r, w, e = select(self.sock, [], self.sock) + r = select(self.sock, [], self.sock)[0] for sock in r: data, addr = sock.recvfrom(556) self.handle(sock, addr, data) @@ -315,7 +322,7 @@ class BootpServer: vendor += spack('!BBB%ds' % len(prompt), PXE_MENU_PROMPT, 1+len(prompt), len(prompt), prompt) buf += spack('!BB%ds' % len(vendor), 43, - len(vendor), vendor) + len(vendor), vendor) buf += spack('!BBB', 255, 0, 0) return buf except KeyError as exc: @@ -389,12 +396,12 @@ class BootpServer: # if the state has not evolved from idle, there is nothing to do if newstate == self.ST_IDLE: - self.log.info('Request from %s ignored (idle state)' % mac_str) sdhcp = 'allow_simple_dhcp' simple_dhcp = \ self.config.has_option(self.bootp_section, sdhcp) and \ to_bool(self.config.get(self.bootp_section, sdhcp)) if not simple_dhcp: + self.log.info('Request from %s ignored (idle state)' % mac_str) return if not dhcp_msg_type: # Legacy DHCP: assuming discover by default @@ -469,9 +476,9 @@ class BootpServer: # construct reply buf[BOOTP_HOPS] = 0 buf[BOOTP_OP] = BOOTREPLY - self.log.info('Client IP: %s' % inet_ntoa(buf[7])) - if buf[BOOTP_CIADDR] == '\x00\x00\x00\x00': - self.log.debug('Client needs its address') + ciaddr = buf[BOOTP_CIADDR] + if not sunpack('!I', ciaddr)[0]: + self.log.info('Client needs its address') ipaddr = iptoint(self.pool_start) ip = None if mac_str in self.ippool: @@ -479,7 +486,7 @@ class BootpServer: self.log.info('Lease for MAC %s already defined as IP %s' % (mac_str, ip)) else: - for idx in xrange(self.pool_count): + for idx in range(self.pool_count): ipkey = inttoip(ipaddr+idx) self.log.debug('Check for IP %s' % ipkey) if ipkey not in self.ippool.values(): @@ -498,13 +505,14 @@ class BootpServer: buf[BOOTP_FLAGS] = BOOTP_FLAGS_BROADCAST relay = buf[BOOTP_GIADDR] - if relay != b'\x00\x00\x00\x00': + if sunpack('!I', relay)[0]: addr = (inet_ntoa(relay), addr[1]) else: addr = (inttoip(reply_broadcast), addr[1]) self.log.info('Reply to: %s:%s' % addr) else: - buf[BOOTP_YIADDR] = buf[BOOTP_CIADDR] + self.log.info('Client IP: %s' % inet_ntoa(ciaddr)) + buf[BOOTP_YIADDR] = ciaddr ip = inet_ntoa(buf[BOOTP_YIADDR]) buf[BOOTP_SIADDR] = inet_aton(server_addr) # sname diff --git a/pybootd/tests/httpd.py b/pybootd/tests/httpd.py index fcb67d2..e074c7f 100755 --- a/pybootd/tests/httpd.py +++ b/pybootd/tests/httpd.py @@ -88,7 +88,7 @@ if __name__ == "__main__": (options, args) = optparser.parse_args(sys.argv[1:]) if not options.config: - raise AssertionError('Missing configuration file') + raise RuntimeError('Missing configuration file') cfgparser = EasyConfigParser() with open(options.config, 'rt') as config: diff --git a/pybootd/tftpd.py b/pybootd/tftpd.py index 6c2598e..b52ec25 100644 --- a/pybootd/tftpd.py +++ b/pybootd/tftpd.py @@ -27,21 +27,25 @@ from struct import pack as spack, unpack as sunpack from sys import argv, exc_info from threading import Thread from time import time as now -from traceback import print_exc -from urllib.parse import urlparse +from traceback import format_exc +from urllib.parse import urlparse, urlsplit from urllib.request import urlopen from . import pybootd_path from .util import hexline -#pybootd: disable-msg=broad-except +#pylint: disable-msg=broad-except +#pylint: disable-msg=invalid-name +#pylint: disable-msg=missing-docstring TFTP_PORT = 69 -class TftpError(AssertionError): +class TftpError(RuntimeError): """Any TFTP error""" - pass + def __init__(self, code, msg): + super(TftpError, self).__init__(msg) + self.code = code class TftpConnection(object): @@ -71,7 +75,7 @@ class TftpConnection(object): def _bind(self, host='', port=TFTP_PORT): self.log.debug('bind %s:%d' % (host, port)) - self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.sock = socket(AF_INET, SOCK_DGRAM) if host or port: self.sock.bind((host, port)) @@ -87,7 +91,7 @@ class TftpConnection(object): timeout = self.timeout retry = self.server.retry while retry: - r, w, e = select([fno], [], [fno], timeout) + r = select([fno], [], [fno], timeout)[0] if not r: # We timed out -- retransmit retry = retry - 1 @@ -128,8 +132,9 @@ class TftpConnection(object): pkt = {} opcode = pkt['opcode'] = unpack('!h', buf[:2])[0] if (opcode == self.RRQ) or (opcode == self.WRQ): - resource, mode, options = data[2:].split('\000', 2) - resource = self.server.fcre.sub(self._filter_file, resource) + resource, mode, options = data[2:].split(b'\x00', 2) + resource = self.server.fcre.sub(self._filter_file, + resource.decode()) if self.server.root and self.is_url(self.server.root): resource = '%s/%s' % (self.server.root, resource) else: @@ -159,7 +164,7 @@ class TftpConnection(object): pkt['filename'] = resource pkt['mode'] = mode while options: - key, value, options = options.split('\000', 2) + key, value, options = options.split(b'\x00', 2) if key == 'blksize': self.blocksize = int(value) elif key == 'timeout': @@ -219,9 +224,9 @@ class TftpConnection(object): raise TftpError(5, 'Invalid opcode') self.log.debug('End of active: %s:%s' % addr) except TftpError as exc: - self.send_error(exc[0], exc[1]) - except: - self.log.error(format_exc()) + self.send_error(exc.code, str(exc)) + except Exception: + self.log.error(format_exc(chain=False)) self.log.debug('Ending connection %s:%s' % addr) def recv_ack(self, pkt): @@ -270,8 +275,9 @@ class TftpConnection(object): except AttributeError: # StringIO does not have a 'name' attribute pass - except Exception: - print_exc() + except Exception as exc: + self.log.error('Error: %s' % exc) + self.log.warn('%s', format_exc(chain=False)) def send_ack(self, pack=spack): self.log.debug('send_ack') @@ -283,7 +289,7 @@ class TftpConnection(object): def send_error(self, errnum, errtext, pack=spack): self.log.debug('send_error') - errtext = errtext + '\000' + errtext = errtext.encode() + b'\x00' fmt = '!hh%ds' % len(errtext) outdata = pack(fmt, self.ERR, errnum, errtext) self.sock.sendto(outdata, self.client_addr) @@ -292,7 +298,7 @@ class TftpConnection(object): self.log.debug('send_oack') pkt = pack('!h', self.OACK) for k, v in options: - pkt += k + '\x00' + v + '\x00' + pkt += k.encode() + b'\x00' + v.encode() + b'\x00' self.send(pkt) # clear out the last packet buffer to prevent from retransmitting it self.lastpkt = '' @@ -336,10 +342,10 @@ class TftpConnection(object): resource = os.path.realpath(resource) self.log.info("Sending file '%s'" % resource) self.file = open(resource, 'rb') - except Exception: + except Exception as exc: self.send_error(1, 'Cannot open resource') self.log.warn('Cannot open file for reading %s: %s' % - exc_info()[:2]) + (resource, exc)) return if 'tsize' not in pkt: self.send_data(self.file.read(self.blocksize)) @@ -377,7 +383,7 @@ class TftpConnection(object): @staticmethod def is_url(path): - return bool(urlparse.urlsplit(path).scheme) + return bool(urlsplit(path).scheme) class TftpServer: @@ -419,7 +425,7 @@ class TftpServer: for sock in r: data, addr = sock.recvfrom(516) tc = TftpConnection(self) - thread = Thread(tc.connect, (addr, data)) + thread = Thread(target=tc.connect, args=(addr, data)) thread.start() def filter_file(self, connexion, mo): diff --git a/pybootd/util.py b/pybootd/util.py index 5fac6e5..ff17864 100644 --- a/pybootd/util.py +++ b/pybootd/util.py @@ -135,8 +135,7 @@ def logger_factory(logtype='syslog', logfile=None, level='WARNING', hdlr = FileHandler(logfile) elif logtype in ('winlog', 'eventlog', 'nteventlog'): # Requires win32 extensions - hdlr = NTEventLogHandler(logid, - logtype='Application') + hdlr = NTEventLogHandler(logid, logtype='Application') elif logtype in ('syslog', 'unix'): hdlr = SysLogHandler('/dev/log') elif logtype in ('stderr'): @@ -165,15 +164,6 @@ def logger_factory(logtype='syslog', logfile=None, level='WARNING', formatter = Formatter(format, datefmt) hdlr.setFormatter(formatter) logger.addHandler(hdlr) - - def logerror(record): - import traceback - print(record.msg) - print(record.args) - traceback.print_exc() - # uncomment the following line to show logger formatting error - #hdlr.handleError = logerror - return logger From 973f615736eb4e8784e1073af5781f89a1b91708 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Thu, 5 Sep 2019 12:01:04 +0200 Subject: [PATCH 06/17] Rework TFTP error codes --- pybootd/tftpd.py | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/pybootd/tftpd.py b/pybootd/tftpd.py index b52ec25..0ec6b45 100644 --- a/pybootd/tftpd.py +++ b/pybootd/tftpd.py @@ -42,6 +42,15 @@ TFTP_PORT = 69 class TftpError(RuntimeError): + (NOT_DEF, + FILE_NOT_FOUND, + ACCESS_ERROR, + ALLOC_EXCEED, + ILLEGAL, + UNKNOWN_ID, + ALREADY_EXIST, + NO_SUCH_USER) = range(8) + """Any TFTP error""" def __init__(self, code, msg): super(TftpError, self).__init__(msg) @@ -55,6 +64,7 @@ class TftpConnection(object): ACK = 4 ERR = 5 OACK = 6 + HDRSIZE = 4 # number of bytes for OPCODE and BLOCK in header def __init__(self, server, port=0): @@ -103,7 +113,7 @@ class TftpConnection(object): if addr == client_addr: break else: - raise TftpError(4, 'Transfer timed out') + raise TftpError(TftpError.ALLOC_EXCEED, 'Transfer timed out') # end while return self.parse(data) @@ -179,7 +189,7 @@ class TftpConnection(object): errnum = pkt['errnum'] = unpack('!h', buf[2:4])[0] errtxt = pkt['errtxt'] = buf[4:-1] else: - raise TftpError(4, 'Unknown packet type') + raise TftpError(TftpError.ILLEGAL, 'Unknown packet type') return pkt def retransmit(self): @@ -200,7 +210,7 @@ class TftpConnection(object): pkt = self.parse(data) opcode = pkt['opcode'] if opcode not in (RRQ, WRQ): - raise TftpError(4, 'Bad request') + raise TftpError(TftpError.ILLEGAL, 'Bad request') # Start lock-step transfer self.active = 1 @@ -221,7 +231,7 @@ class TftpConnection(object): elif opcode == ERR: self.recv_err(pkt) else: - raise TftpError(5, 'Invalid opcode') + raise TftpError(TftpError.ILLEGAL, 'Invalid opcode') self.log.debug('End of active: %s:%s' % addr) except TftpError as exc: self.send_error(exc.code, str(exc)) @@ -321,7 +331,8 @@ class TftpConnection(object): filesize = os.stat(resource)[6] except Exception: self.active = False - self.send_error(1, 'Cannot access resource') + self.send_error(TftpError.FILE_NOT_FOUND, + 'Cannot access resource') self.log.warn('Cannot stat resource %s' % resource) return self.log.info('Send size request file %s size: %d' % @@ -343,7 +354,8 @@ class TftpConnection(object): self.log.info("Sending file '%s'" % resource) self.file = open(resource, 'rb') except Exception as exc: - self.send_error(1, 'Cannot open resource') + self.send_error(TftpError.FILE_NOT_FOUND, + 'Cannot open resource') self.log.warn('Cannot open file for reading %s: %s' % (resource, exc)) return @@ -361,7 +373,7 @@ class TftpConnection(object): self.log.info('Receiving file: %s' % resource) self.file = open(resource, 'wb') except: - self.send_error(1, 'Cannot open file') + self.send_error(TftpError.FILE_NOT_FOUND, 'Cannot open file') self.log.error('Cannot open file for writing %s: %s' % exc_info()[:2]) return @@ -409,7 +421,7 @@ class TftpServer: host = self.config.get('tftp', 'address', netconfig and netconfig['server']) if not host: - raise TftpError('TFTP address no defined') + raise TftpError(TftpError.NO_SUCH_USER, 'TFTP address no defined') port = int(self.config.get('tftp', 'port', str(TFTP_PORT))) sock = socket(AF_INET, SOCK_DGRAM) self.sock.append(sock) @@ -438,7 +450,8 @@ class TftpServer: continue filepattern = self.filepatterns[group] return resub(r'\{(\w+)\}', connexion._dynreplace, filepattern) - raise TftpError('Internal error, file matching pattern issue') + raise TftpError(TftpError.NOT_DEF, + 'Internal error, file matching pattern issue') def get_file_filters(self): patterns = [] From 6d8a7111cb9877efa01aaa6186fff12a03f2f62a Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Thu, 5 Sep 2019 17:08:57 +0200 Subject: [PATCH 07/17] Fix log messages --- pybootd/tftpd.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pybootd/tftpd.py b/pybootd/tftpd.py index 0ec6b45..c548526 100644 --- a/pybootd/tftpd.py +++ b/pybootd/tftpd.py @@ -347,12 +347,12 @@ class TftpConnection(object): else: try: if self.is_url(resource): - self.log.info("Sending resource '%s'" % resource) self.file = urlopen(resource) + self.log.info("Sending resource '%s'" % resource) else: resource = os.path.realpath(resource) - self.log.info("Sending file '%s'" % resource) self.file = open(resource, 'rb') + self.log.info("Sending file '%s'" % resource) except Exception as exc: self.send_error(TftpError.FILE_NOT_FOUND, 'Cannot open resource') From 3a2148537adeb73e96b066e4f9b3d246f989f39a Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Thu, 5 Sep 2019 17:44:14 +0200 Subject: [PATCH 08/17] Update doc, and use `d` suffix for local daemons/servers --- README.rst | 79 ++++++++++++++++++++++------------------- pybootd/daemons.py | 4 ++- pybootd/etc/pybootd.ini | 16 ++++----- pybootd/pxed.py | 37 +++++++++---------- pybootd/tftpd.py | 17 +++++---- 5 files changed, 82 insertions(+), 71 deletions(-) diff --git a/README.rst b/README.rst index 19d9ebf..9145f52 100644 --- a/README.rst +++ b/README.rst @@ -21,17 +21,15 @@ Requirements Python ------ -- Python_ 2.7 or above is required. Python_ 3.x is not yet supported. -- Six_ compatibility module +- Python_ 3.5+ or above is required. Python_ 2.x is not longer supported. - Netifaces_ Python module is required on OS X; on Linux only, iproute2_ can be used as an alternative -- Optional: python-pkg-resources_ Python module +- Optional: python_pkg_resources_ Python module .. _Python: http://python.org/ .. _Netifaces: http://alastairs-place.net/netifaces/ .. _iproute2: http://www.linuxfoundation.org/collaborate/workgroups/networking/iproute2 -.. _python-pkg-resources: http://pythonhosted.org/distribute/pkg_resources.html -.. _Six: http://pythonhosted.org/six +.. _python_pkg_resources: http://pythonhosted.org/distribute/pkg_resources.html Permissions ----------- @@ -81,7 +79,7 @@ Common errors ``pybootd.pxed.BootpError: Unable to detect network configuration`` This error is often triggered when the ``pool_start`` address is not part of a valid network. Double check the network configuration and - fix up the ``[bootp]`` section so that it matches the actual + fix up the ``[bootpd]`` section so that it matches the actual network. If you don't want to allocate addresses dynamically from the pool (with ``pool_count = 0``), you still need to specify ``pool_start`` to some address in the local network you want to @@ -95,8 +93,8 @@ Common errors Configuration ------------- -``pybootd`` has a few option switches. The server offers two services: bootp -(which supports Dhcp and PXE extensions) and tftp. It is possible to disable +``pybootd`` has a few option switches. The server offers two services: *bootpd* +(which supports DHCP and PXE extensions) and *tftpd*. It is possible to disable either services. Usage: pybootd.py [options] @@ -145,8 +143,8 @@ client requests at least an IP address twice: ``file`` The path to the output log file, if ``type`` is set to ``file``. -``[bootp]`` section -................... +``[bootpd]`` section +.................... ``access`` Type of access control list. If this option is not defined, all BOOTP @@ -228,36 +226,40 @@ client requests at least an IP address twice: ``servername`` Name of the BOOTP server. + ``[mac]`` section ................. - The ``[mac]`` section contains one entry for each MAC address to allow or - block. The value for each entry is a boolean, *i.e.*:: +The ``[mac]`` section contains one entry for each MAC address to allow or +block. The value for each entry is a boolean, *i.e.*:: - AA-BB-CC-DD-EE-FF = enable + ``AA-BB-CC-DD-EE-FF = enable`` + +Note that due to a limitation of the configuration parser, ':' byte separator +in MAC addresses is not allowed, please use '-' separator. - Note that due to a limitation of the configuration parser, ':' byte separator - in MAC addresses is not allowed, please use '-' separator. ``[static_dhcp]`` section ......................... - The ``[static_dhcp]`` section contains one entry for each MAC - address to associate with a specific IP address. The IP address can be - any IPv4 address in dotted notation, *i.e.*: +The ``[static_dhcp]`` section contains one entry for each MAC +address to associate with a specific IP address. The IP address can be +any IPv4 address in dotted notation, *i.e.*: - AA-BB-CC-DD-EE-FF = 192.168.1.2 + ``AA-BB-CC-DD-EE-FF = 192.168.1.2`` + +The MAC addresses specified here will automatically be allowed, +unless ``[mac]`` section specifies otherwise. - The MAC addresses specified here will automatically be allowed, - unless ``[mac]`` section specifies otherwise. ``[uuid]`` section .................. - The ``[uuid]`` section contains one entry for each UUID to allow or block. - The value for each entry is a boolean, *i.e.*:: +The ``[uuid]`` section contains one entry for each UUID to allow or block. +The value for each entry is a boolean, *i.e.*:: + + ``xxxxxxxx-aaaa-bbbb-cccc-yyyyyyyyyyyy = enable`` - xxxxxxxx-aaaa-bbbb-cccc-yyyyyyyyyyyy = enable ``[http]`` section .................. @@ -281,8 +283,9 @@ The ``pxe``/``dhcp`` option pair enables the remote HTTP server to identify the boot phase: either a BIOS initialization or an OS boot sequence. When such differentiation is useless, both options may refer to the same path. -``[tftp]`` section -.................. + +``[tftpd]`` section +................... ``address`` Address to listen to incoming TFTP requests. When the BOOTP daemon is @@ -310,11 +313,12 @@ differentiation is useless, both options may refer to the same path. - an absolute path, when the ``root`` option starts with ``/``, - a URL prefix, to access remote files. + ``[filters]`` section ..................... The ``filters`` section allows on-the-fly pathnames transformation. When a TFTP -client requests some specific filenames, the *tftp* server can translate them +client requests some specific filenames, the *tftpd* server can translate them to other ones. This option is useful to serve the very same configuration file (''e.g.'' @@ -332,8 +336,8 @@ braces, such as ``{varname}``. For now, the only supported variable is ``filename``, which is replaced with the actual requested filename. -The *value* part can also contain a special marker, that tells the *tftp* -daemon to read the replacement pattern from a file. This special marker should +The *value* part can also contain a special marker, that tells the *tftpd* +server to read the replacement pattern from a file. This special marker should be written with enclosing brackets, such as ``[file]``. Examples @@ -343,7 +347,7 @@ The following filter:: pxelinux.cfg/* = pybootd/etc/pxe.cfg -tells the *tftp* server that all client requests matching the +tells the *tftpd* server that all client requests matching the ``pxelinux.cfg/*`` pattern should be served the ``pybootd/etc/pxe.cfg`` file instead. This prevents the client to perform the usual time-costing fallback requests using UUID, MAC, and suffix addresses before eventually falling @@ -353,7 +357,7 @@ The following filter:: startup = [dir/{filename}.cfg] -tells the *tftp* server that when the ``startup`` file is requested, it should +tells the *tftpd* server that when the ``startup`` file is requested, it should read out the actual filename from the ``dir/startup.cfg`` file. HTTP-based authentication @@ -382,15 +386,16 @@ this feature. It can be found within the ``tests/`` subdirectory. See the ``config.ini`` file for this test daemon. The test daemon expects the ``pxe`` path to be set to ``/boot`` and the ``dhcp`` path to ``/linux``. + Sample configurations ~~~~~~~~~~~~~~~~~~~~~ Installing a Debian 6.0 machine from the official archive --------------------------------------------------------- -As the *tftp* daemon is able to retrieve remote files using the HTTP protocol, -there is no need to manually download any file from a Debian mirror. The daemon -will forward all file requests to the mirror on behalf of the client being -installed. +As pybootd's *tftpd* server is able to retrieve remote files using the HTTP +protocol, there is no need to manually download any file from a Debian mirror. +The daemon will forward all file requests to the mirror on behalf of the client +being installed. The ``pybootd.ini`` would contain:: @@ -400,7 +405,7 @@ The ``pybootd.ini`` would contain:: ; show informative and error messages only (disable verbose mode) level = info - [bootp] + [bootpd] ; do not force a full PXE boot-up cycle to accept the client allow_simple_dhcp = enable ; First BOOTP/DHCP address to generate @@ -410,7 +415,7 @@ The ``pybootd.ini`` would contain:: ; boot-up executable the client should request through TFTP boot_file = pxelinux.0 - [tftp] + [tftpd] ; URL to install a Debian 6.0 Intel/AMD 64-bit network installation root = http://http.us.debian.org/debian/dists/squeeze/main/installer-amd64/current/images/netboot diff --git a/pybootd/daemons.py b/pybootd/daemons.py index a573610..735f1fd 100755 --- a/pybootd/daemons.py +++ b/pybootd/daemons.py @@ -31,7 +31,9 @@ from .pxed import BootpServer from .tftpd import TftpServer from .util import logger_factory, EasyConfigParser -#pybootd: disable-msg=broad-except +#pylint: disable-msg=broad-except +#pylint: disable-msg=missing-docstring +#pylint: disable-msg=invalid-name class BootpDaemon(Thread): diff --git a/pybootd/etc/pybootd.ini b/pybootd/etc/pybootd.ini index 912f1e5..1fbbe77 100644 --- a/pybootd/etc/pybootd.ini +++ b/pybootd/etc/pybootd.ini @@ -2,11 +2,10 @@ type = stderr level = info -[bootp] +[bootpd] address = 0.0.0.0 ; pool_start should be in a valid subnet -; pool_start = 192.168.25.100 -pool_start = 10.113.116.245 +pool_start = 192.168.25.100 pool_count = 5 domain = localdomain server_name = debug @@ -14,13 +13,14 @@ boot_file = pxelinux.0 lease_time = 86400 access = mac allow_simple_dhcp = enable -dns = 10.130.0.2 +dns = 8.8.8.8 set_gateway = true ; use "nc -l -u 127.0.0.1 -p 12345" to debug ; notify = 192.168.26.201:12345;192.168.26.200:12345 [mac] -00-1E-4F-C4-95-EE = enable +; see doc: byte separator should be defined with dash, not column +00-AA-55-12-34-56 = enable [uuid] 12345678-abcd-ef00-1111-abcdefabcdef = enable @@ -32,10 +32,10 @@ pxe = boot dhcp = linux always_check = disable -[tftp] +[tftpd] ;address = (use address from bootpd) -;root = ./images -root = http://http.us.debian.org/debian/dists/squeeze/main/installer-amd64/current/images/netboot +root = ./images +;root = http://http.us.debian.org/debian/dists/squeeze/main/installer-amd64/current/images/netboot [filters] ;pxelinux.cfg/* = pybootd/etc/pxe.cfg diff --git a/pybootd/pxed.py b/pybootd/pxed.py index a573e29..ad4f5e9 100644 --- a/pybootd/pxed.py +++ b/pybootd/pxed.py @@ -182,6 +182,8 @@ class BootpServer: ACCESS_REMOTE = ['http'] # Access modes, remotely retrieved (ST_IDLE, ST_PXE, ST_DHCP) = range(3) # Current state + BOOTP_SECTION = 'bootpd' + def __init__(self, logger, config): self.sock = [] self.log = logger @@ -190,16 +192,15 @@ class BootpServer: self.ippool = {} # key MAC address string, value assigned IP string self.filepool = {} # key IP string, value pathname self.states = {} # key MAC address string, value client state - self.bootp_section = 'bootp' - self.pool_start = self.config.get(self.bootp_section, 'pool_start') + self.pool_start = self.config.get(self.BOOTP_SECTION, 'pool_start') if not self.pool_start: raise BootpError('Missing pool_start definition') - self.pool_count = int(self.config.get(self.bootp_section, + self.pool_count = int(self.config.get(self.BOOTP_SECTION, 'pool_count', '10')) self.netconfig = get_iface_config(self.pool_start) if not self.netconfig: - host = self.config.get(self.bootp_section, 'address', '0.0.0.0') + host = self.config.get(self.BOOTP_SECTION, 'address', '0.0.0.0') self.netconfig = get_iface_config(host) if not self.netconfig: # the available networks on the host may not match the config... @@ -208,7 +209,7 @@ class BootpServer: keys = sorted(self.netconfig.keys()) self.log.info('Using %s' % ', '.join(map( ':'.join, zip(keys, [self.netconfig[k] for k in keys])))) - nlist = self.config.get(self.bootp_section, 'notify') + nlist = self.config.get(self.BOOTP_SECTION, 'notify') self.notify = [] if nlist: try: @@ -218,7 +219,7 @@ class BootpServer: self.notify.append((n[0], int(n[1]))) except Exception as exc: raise BootpError('Invalid notification URL: %s' % exc) - access = self.config.get(self.bootp_section, 'access') + access = self.config.get(self.BOOTP_SECTION, 'access') if not access: self.acl = None else: @@ -257,8 +258,8 @@ class BootpServer: return self.netconfig def bind(self): - host = self.config.get(self.bootp_section, 'address', '0.0.0.0') - port = self.config.get(self.bootp_section, 'port', + host = self.config.get(self.BOOTP_SECTION, 'address', '0.0.0.0') + port = self.config.get(self.BOOTP_SECTION, 'port', str(BOOTP_PORT_REQUEST)) sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) sock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1) @@ -398,8 +399,8 @@ class BootpServer: if newstate == self.ST_IDLE: sdhcp = 'allow_simple_dhcp' simple_dhcp = \ - self.config.has_option(self.bootp_section, sdhcp) and \ - to_bool(self.config.get(self.bootp_section, sdhcp)) + self.config.has_option(self.BOOTP_SECTION, sdhcp) and \ + to_bool(self.config.get(self.BOOTP_SECTION, sdhcp)) if not simple_dhcp: self.log.info('Request from %s ignored (idle state)' % mac_str) return @@ -497,7 +498,7 @@ class BootpServer: raise BootpError('No more IP available in definined pool') mask = iptoint(self.config.get( - self.bootp_section, 'netmask', self.netconfig['mask'])) + self.BOOTP_SECTION, 'netmask', self.netconfig['mask'])) reply_broadcast = iptoint(ip) & mask reply_broadcast |= (~mask) & ((1 << 32)-1) buf[BOOTP_YIADDR] = inet_aton(ip) @@ -517,12 +518,12 @@ class BootpServer: buf[BOOTP_SIADDR] = inet_aton(server_addr) # sname buf[BOOTP_SNAME] = \ - '.'.join([self.config.get(self.bootp_section, + '.'.join([self.config.get(self.BOOTP_SECTION, 'servername', 'unknown'), - self.config.get(self.bootp_section, + self.config.get(self.BOOTP_SECTION, 'domain', 'localdomain')]).encode() # file - buf[BOOTP_FILE] = self.config.get(self.bootp_section, + buf[BOOTP_FILE] = self.config.get(self.BOOTP_SECTION, 'boot_file', '\x00').encode() if not dhcp_msg_type: @@ -573,18 +574,18 @@ class BootpServer: pkt += spack('!BB4s', DHCP_SERVER, 4, server) mask = inet_aton(self.config.get( - self.bootp_section, 'netmask', self.netconfig['mask'])) + self.BOOTP_SECTION, 'netmask', self.netconfig['mask'])) pkt += spack('!BB4s', DHCP_IP_MASK, 4, mask) - gateway_addr = self.config.get(self.bootp_section, 'gateway', '') + gateway_addr = self.config.get(self.BOOTP_SECTION, 'gateway', '') if gateway_addr: gateway = inet_aton(gateway_addr) else: gateway = server pkt += spack('!BB4s', DHCP_IP_GATEWAY, 4, gateway) - dns = self.config.get(self.bootp_section, + dns = self.config.get(self.BOOTP_SECTION, 'dns', None) if dns: if dns.lower() == 'auto': @@ -595,7 +596,7 @@ class BootpServer: dns_ip = inet_aton(dns_str) pkt += spack('!BB4s', DHCP_IP_DNS, 4, dns_ip) pkt += spack('!BBI', DHCP_LEASE_TIME, 4, - int(self.config.get(self.bootp_section, + int(self.config.get(self.BOOTP_SECTION, 'lease_time', str(24*3600)))) pkt += spack('!BB', DHCP_END, 0) diff --git a/pybootd/tftpd.py b/pybootd/tftpd.py index c548526..fd189d5 100644 --- a/pybootd/tftpd.py +++ b/pybootd/tftpd.py @@ -404,25 +404,28 @@ class TftpServer: Each request is handled in its own thread """ + TFTP_SECTION = 'tftpd' + def __init__(self, logger, config, bootpd=None): self.log = logger self.config = config self.sock = [] self.bootpd = bootpd - self.blocksize = int(self.config.get('tftp', 'blocksize', '512')) - self.timeout = float(self.config.get('tftp', 'timeout', '2.0')) - self.retry = int(self.config.get('tftp', 'blocksize', '5')) - self.root = self.config.get('tftp', 'root', os.getcwd()) + self.blocksize = int(self.config.get(self.TFTP_SECTION, 'blocksize', + '512')) + self.timeout = float(self.config.get(self.TFTP_SECTION, 'timeout', '2.0')) + self.retry = int(self.config.get(self.TFTP_SECTION, 'blocksize', '5')) + self.root = self.config.get(self.TFTP_SECTION, 'root', os.getcwd()) self.fcre, self.filepatterns = self.get_file_filters() self.genfilecre = recompile(r'\[(?P[\w\.\-]+)\]') def bind(self): netconfig = self.bootpd and self.bootpd.get_netconfig() - host = self.config.get('tftp', 'address', + host = self.config.get(self.TFTP_SECTION, 'address', netconfig and netconfig['server']) if not host: raise TftpError(TftpError.NO_SUCH_USER, 'TFTP address no defined') - port = int(self.config.get('tftp', 'port', str(TFTP_PORT))) + port = int(self.config.get(self.TFTP_SECTION, 'port', str(TFTP_PORT))) sock = socket(AF_INET, SOCK_DGRAM) self.sock.append(sock) sock.bind((host, port)) @@ -433,7 +436,7 @@ class TftpServer: if not self.bootpd.is_alive(): self.log.info('Bootp daemon is dead, exiting') break - r, w, e = select(self.sock, [], self.sock) + r = select(self.sock, [], self.sock)[0] for sock in r: data, addr = sock.recvfrom(516) tc = TftpConnection(self) From d053b2ed11b7ffd0a40420ed7a37875119249f39 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Thu, 5 Sep 2019 17:48:12 +0200 Subject: [PATCH 09/17] Fix doc typo --- README.rst | 13 +++++++------ pybootd/daemons.py | 3 +-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index 9145f52..3d9c238 100644 --- a/README.rst +++ b/README.rst @@ -97,15 +97,16 @@ Configuration (which supports DHCP and PXE extensions) and *tftpd*. It is possible to disable either services. -Usage: pybootd.py [options] - PXE boot up server, a tiny BOOTP/DHCP/TFTP server +Usage: pybootd.py [-h] [-c CONFIG] [-p] [-t] [-d] + Tiny BOOTP/DHCP/TFTP/PXE server Options: -h, --help show this help message and exit - -c CONFIG, --config=CONFIG + -c CONFIG, --config CONFIG configuration file -p, --pxe enable BOOTP/DHCP/PXE server only -t, --tftp enable TFTP server only + -d, --debug enable debug mode ``pybootd`` daemon uses a configuration file, in ``.ini`` format, for all other options. @@ -233,7 +234,7 @@ client requests at least an IP address twice: The ``[mac]`` section contains one entry for each MAC address to allow or block. The value for each entry is a boolean, *i.e.*:: - ``AA-BB-CC-DD-EE-FF = enable`` + AA-BB-CC-DD-EE-FF = enable Note that due to a limitation of the configuration parser, ':' byte separator in MAC addresses is not allowed, please use '-' separator. @@ -246,7 +247,7 @@ The ``[static_dhcp]`` section contains one entry for each MAC address to associate with a specific IP address. The IP address can be any IPv4 address in dotted notation, *i.e.*: - ``AA-BB-CC-DD-EE-FF = 192.168.1.2`` + AA-BB-CC-DD-EE-FF = 192.168.1.2 The MAC addresses specified here will automatically be allowed, unless ``[mac]`` section specifies otherwise. @@ -258,7 +259,7 @@ unless ``[mac]`` section specifies otherwise. The ``[uuid]`` section contains one entry for each UUID to allow or block. The value for each entry is a boolean, *i.e.*:: - ``xxxxxxxx-aaaa-bbbb-cccc-yyyyyyyyyyyy = enable`` + xxxxxxxx-aaaa-bbbb-cccc-yyyyyyyyyyyy = enable ``[http]`` section diff --git a/pybootd/daemons.py b/pybootd/daemons.py index 735f1fd..457bd35 100755 --- a/pybootd/daemons.py +++ b/pybootd/daemons.py @@ -18,8 +18,7 @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -"""Boot up server, a tiny BOOTP/DHCP/TFTP/PXE server""" - +"""Tiny BOOTP/DHCP/TFTP/PXE server""" from os.path import isfile from threading import Thread From 9b5184f4619dd1c2113a221931c5a06f5e3a1f79 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Fri, 6 Sep 2019 11:15:42 +0200 Subject: [PATCH 10/17] Remove UTF8 explicit def --- pybootd/__init__.py | 4 +--- pybootd/daemons.py | 9 ++++----- pybootd/pxed.py | 2 -- pybootd/tftpd.py | 2 -- setup.py | 3 +-- 5 files changed, 6 insertions(+), 14 deletions(-) diff --git a/pybootd/__init__.py b/pybootd/__init__.py index 1e81d7e..415bfba 100644 --- a/pybootd/__init__.py +++ b/pybootd/__init__.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2010-2016 Emmanuel Blot +# Copyright (c) 2010-2019 Emmanuel Blot # Copyright (c) 2010-2011 Neotion # # This library is free software; you can redistribute it and/or diff --git a/pybootd/daemons.py b/pybootd/daemons.py index 457bd35..c6a7d64 100755 --- a/pybootd/daemons.py +++ b/pybootd/daemons.py @@ -1,5 +1,4 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- +#!/usr/bin/env python3 # # Copyright (c) 2010-2019 Emmanuel Blot # Copyright (c) 2010-2011 Neotion @@ -20,6 +19,7 @@ """Tiny BOOTP/DHCP/TFTP/PXE server""" +from argparse import ArgumentParser from os.path import isfile from threading import Thread from sys import exit as sysexit, modules, stderr @@ -68,7 +68,6 @@ class TftpDaemon(Thread): def main(): debug = False try: - from argparse import ArgumentParser argparser = ArgumentParser(description=modules[__name__].__doc__) argparser.add_argument('-c', '--config', dest='config', default='pybootd/etc/pybootd.ini', @@ -92,7 +91,7 @@ def main(): cfgparser = EasyConfigParser() with open(pybootd_path(args.config), 'rt') as config: - cfgparser.readfp(config) + cfgparser.read_file(config) logger = logger_factory(logtype=cfgparser.get('logger', 'type', 'stderr'), @@ -119,5 +118,5 @@ def main(): print(format_exc(chain=False), file=stderr) sysexit(1) except KeyboardInterrupt: - print("\nAborting...") + print("\nAborting...", file=stderr) sysexit(2) diff --git a/pybootd/pxed.py b/pybootd/pxed.py index ad4f5e9..7ae5c03 100644 --- a/pybootd/pxed.py +++ b/pybootd/pxed.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright (c) 2010-2019 Emmanuel Blot # Copyright (c) 2010-2011 Neotion # diff --git a/pybootd/tftpd.py b/pybootd/tftpd.py index fd189d5..03990ac 100644 --- a/pybootd/tftpd.py +++ b/pybootd/tftpd.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright (c) 2010-2019 Emmanuel Blot # Copyright (c) 2010-2011 Neotion # diff --git a/setup.py b/setup.py index 8acab9b..570321c 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- +#!/usr/bin/env python3 # # Copyright (c) 2010-2019 Emmanuel Blot # Copyright (c) 2010-2011 Neotion From 22e8079c16a6e3fe1388ab158f2eebf71d198253 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Fri, 6 Sep 2019 11:16:00 +0200 Subject: [PATCH 11/17] Adapt httpd test server to Py3 --- pybootd/tests/httpd.py | 81 +++++++++++++++++++++++++----------------- pybootd/util.py | 21 ++++++----- 2 files changed, 62 insertions(+), 40 deletions(-) diff --git a/pybootd/tests/httpd.py b/pybootd/tests/httpd.py index e074c7f..9b8fede 100755 --- a/pybootd/tests/httpd.py +++ b/pybootd/tests/httpd.py @@ -1,7 +1,6 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- +#!/usr/bin/env python3 # -# Copyright (c) 2010-2016 Emmanuel Blot +# Copyright (c) 2010-2019 Emmanuel Blot # Copyright (c) 2010-2011 Neotion # # This library is free software; you can redistribute it and/or @@ -18,14 +17,22 @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -import sys -import urlparse -from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler -from optparse import OptionParser -from util import logger_factory, to_bool, to_int, EasyConfigParser +"""HTTPd tiny server to exercise the pybootd daemon""" + +from argparse import ArgumentParser, FileType +from http.server import BaseHTTPRequestHandler, HTTPServer +from sys import exit as sysexit, modules, stderr +from traceback import format_exc +from urllib.parse import parse_qs, urlsplit +from pybootd.util import logger_factory, to_bool, to_int, EasyConfigParser -class HttpdDaemon(HTTPServer): +#pylint: disable-msg=broad-except +#pylint: disable-msg=missing-docstring +#pylint: disable-msg=invalid-name + + +class HttpdTestDaemon(HTTPServer): class ReqHandler(BaseHTTPRequestHandler): @@ -33,8 +40,9 @@ class HttpdDaemon(HTTPServer): log = self.server.log log.debug("GET from %s:%d" % self.client_address) log.debug("Request: %s" % self.path) - urlparts = urlparse.urlsplit(self.path) - query = urlparse.parse_qs(urlparts.query) + urlparts = urlsplit(self.path) + query = parse_qs(urlparts.query) + uuid = '' if urlparts.path in ('/boot', '/linux'): if 'uuid' in query: uuids = query['uuid'] @@ -79,30 +87,39 @@ class HttpdDaemon(HTTPServer): self.serve_forever() -if __name__ == "__main__": - usage = 'Usage: %prog [options]\n' \ - ' HTTPd tiny server to exercise the pybootd daemon' - optparser = OptionParser(usage=usage) - optparser.add_option('-c', '--config', dest='config', - help='configuration file') - (options, args) = optparser.parse_args(sys.argv[1:]) - - if not options.config: - raise RuntimeError('Missing configuration file') - - cfgparser = EasyConfigParser() - with open(options.config, 'rt') as config: - cfgparser.readfp(config) - - logger = logger_factory(logtype=cfgparser.get('logger', 'type', 'stderr'), - logfile=cfgparser.get('logger', 'file'), - level=cfgparser.get('logger', 'level', 'info')) - +def main(): + debug = False try: - bt = HttpdDaemon(logger, cfgparser) + argparser = ArgumentParser(description=modules[__name__].__doc__) + argparser.add_argument('-c', '--config', dest='config', required=True, + type=FileType('rt'), + help='configuration file') + argparser.add_argument('-d', '--debug', action='store_true', + help='enable debug mode') + args = argparser.parse_args() + + cfgparser = EasyConfigParser() + cfgparser.read_file(args.config) + + logger = logger_factory(logtype=cfgparser.get('logger', 'type', + 'stderr'), + logfile=cfgparser.get('logger', 'file'), + level=cfgparser.get('logger', 'level', 'info')) + + bt = HttpdTestDaemon(logger, cfgparser) bt.start() while True: import time time.sleep(5) + except Exception as exc: + print('\nError: %s' % exc, file=stderr) + if debug: + print(format_exc(chain=False), file=stderr) + sysexit(1) except KeyboardInterrupt: - print "Aborting..." + print("\nAborting...", file=stderr) + sysexit(2) + + +if __name__ == '__main__': + main() diff --git a/pybootd/util.py b/pybootd/util.py index ff17864..9a665d3 100644 --- a/pybootd/util.py +++ b/pybootd/util.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright (c) 2010-2019 Emmanuel Blot # Copyright (c) 2010-2011 Neotion # @@ -17,7 +15,7 @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -from configparser import SafeConfigParser, InterpolationSyntaxError +from configparser import ConfigParser, InterpolationSyntaxError from logging import (DEBUG, INFO, ERROR, CRITICAL, WARNING, Formatter, FileHandler, StreamHandler, getLogger) from logging.handlers import (BufferingHandler, NTEventLogHandler, @@ -235,7 +233,15 @@ def get_iface_config(address): return nifcfg(address) -class EasyConfigParser(SafeConfigParser): +def is_quoted(str_): + """Tells whether a string is enclosed in simple- or double- quoted + markers""" + str_ = str_.strip() + return (str_.startswith('"') and str_.endswith('"')) or \ + (str_.startswith("'") and str_.endswith("'")) + + +class EasyConfigParser(ConfigParser): """ConfigParser extension to support default config values and do not mess with multi-line option strings""" @@ -251,8 +257,8 @@ class EasyConfigParser(SafeConfigParser): return default if not self.has_option(section, option): return default - return SafeConfigParser.get(self, section, option, raw=raw, vars=vars, - fallback=fallback) + return ConfigParser.get(self, section, option, raw=raw, vars=vars, + fallback=fallback) def write(self, filep): """Write an .ini-format representation of the configuration state, @@ -274,5 +280,4 @@ class EasyConfigParser(SafeConfigParser): if is_quoted(rawval): return rawval # cannot use 'super' here as ConfigParser is outdated - return SafeConfigParser._interpolate(self, section, option, - rawval, vars) + return ConfigParser._interpolate(self, section, option, rawval, vars) From a1cdddd8fa0660498ff3f18945c88a6542a68c89 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Fri, 6 Sep 2019 11:17:49 +0200 Subject: [PATCH 12/17] Add an exception for local configuration scripts --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index badda5a..00bca28 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.egg-info *.pyc *.pyo +**/*-local.ini dist/ build/ From be1195b25fb1309730bb797b4e34892718dd457b Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Fri, 6 Sep 2019 16:20:54 +0200 Subject: [PATCH 13/17] Add a very basic HTTP server Rework daemon initialization and termination Rename HTTPd test server Update documentation --- README.rst | 51 +++++++++++- pybootd/daemons.py | 98 ++++++++++++++++------- pybootd/etc/pybootd.ini | 6 ++ pybootd/httpd.py | 99 ++++++++++++++++++++++++ pybootd/pxed.py | 17 ++-- pybootd/tests/{httpd.py => httptestd.py} | 0 pybootd/tftpd.py | 24 +++--- 7 files changed, 248 insertions(+), 47 deletions(-) create mode 100755 pybootd/httpd.py rename pybootd/tests/{httpd.py => httptestd.py} (100%) diff --git a/README.rst b/README.rst index 3d9c238..ea1a0c5 100644 --- a/README.rst +++ b/README.rst @@ -5,16 +5,20 @@ PyBootd Overview ~~~~~~~~ -PyBootd is a daemon supporting a subset of the BOOTP, DHCP, PXE and TFTP +PyBootd is a daemon supporting a subset of the BOOTP, DHCP, PXE, TFTP and HTTP protocols, with some handy extensions. One of its main goals is to provide a simple solution to boot up any PXE-enabled personal computer, with no other tool required but a standard Python installation. +It is not designed to be feature-complete, but to be used as an easy modifiable +code to develop custom boot solutions + Pybootd can be used for any network boot up, or to install an OS without any physical support such as a USB key or a CD/DVD. + Requirements ~~~~~~~~~~~~ @@ -36,17 +40,21 @@ Permissions - DHCP protocol requires the daemon to listen on port 67. - TFTP protocol requires the daemon to listen on port 69. +- HTTP optional daemon may be run on any port. As these ports are within the server's range (<1024), the superuser privileges are required on Unix hosts (Linux, Mac OS X, ...) to start up these daemons. + Status ~~~~~~ This project is in beta development stage. + Supported features ~~~~~~~~~~~~~~~~~~ + - Access control: 1. None (any remote host can be served) @@ -70,6 +78,20 @@ Supported features - It is possible to use pybootd with only one of the services, either TFTP or DHCP +- A very basic HTTP server can be optionally enabled to serve files over HTTP + for complex hosts that require additional files (such as a root file system) + after the initial boot sequence. + +Warning +~~~~~~~ + +There is no strong checking of permissions nor robust file path management, so +it is recommended NOT to run this daemon on a host with sensitive content. + +Although only read requests are implemented, there is no enforcement or +strong validation of received data and strings from adversary remote clients. + + FAQ ~~~ @@ -104,8 +126,9 @@ Options: -h, --help show this help message and exit -c CONFIG, --config CONFIG configuration file - -p, --pxe enable BOOTP/DHCP/PXE server only - -t, --tftp enable TFTP server only + -p, --pxe only enable BOOTP/DHCP/PXE server + -t, --tftp only enable TFTP server + -H, --http enable HTTP server (default: disabled) -d, --debug enable debug mode ``pybootd`` daemon uses a configuration file, in ``.ini`` format, for all other @@ -315,6 +338,28 @@ differentiation is useless, both options may refer to the same path. - a URL prefix, to access remote files. +``[httpd]`` section +................... + +``address`` + Address to listen to incoming HTTP requests. When the BOOTP daemon is + enabled this option is better omitted, as the address is automatically + received from the BOOTP daemon. + +``port`` + Alternative port for incoming HTTP request, default to 80 + +``root`` + Base directory for the HTTP service. This path is automatically prepended + to the pathname issued from the TFTP client. It can either point to a local + directory for now. + +``check_ip`` + Whether to enforce HTTP client IP or not. When enabled, requests from + clients that have not obtained an IP address from the BOOTP daemon are + rejected. + + ``[filters]`` section ..................... diff --git a/pybootd/daemons.py b/pybootd/daemons.py index c6a7d64..ea28d46 100755 --- a/pybootd/daemons.py +++ b/pybootd/daemons.py @@ -20,12 +20,14 @@ """Tiny BOOTP/DHCP/TFTP/PXE server""" from argparse import ArgumentParser +from collections import OrderedDict from os.path import isfile from threading import Thread from sys import exit as sysexit, modules, stderr from traceback import format_exc from . import pybootd_path, __version__ +from .httpd import HttpServer from .pxed import BootpServer from .tftpd import TftpServer from .util import logger_factory, EasyConfigParser @@ -35,49 +37,72 @@ from .util import logger_factory, EasyConfigParser #pylint: disable-msg=invalid-name -class BootpDaemon(Thread): +class Daemon(Thread): - def __init__(self, logger, config): - super(BootpDaemon, self).__init__(name="BootpDeamon") - self.daemon = True + def __init__(self, debug): + super(Daemon, self).__init__(name=self.__class__.__name__, daemon=True) + self._server = None + self._debug = debug + + def run(self): + try: + self._server.start() + except KeyboardInterrupt: + raise + except Exception as exc: + print('\nError: %s' % exc, stderr) + if self._debug: + print(format_exc(chain=False), file=stderr) + raise + + def stop(self): + self._server.stop() + + +class BootpDaemon(Daemon): + + def __init__(self, logger, config, debug): + super(BootpDaemon, self).__init__(debug) self._server = BootpServer(logger=logger, config=config) def get_netconfig(self): return self._server.get_netconfig() + def is_managed_ip(self, ip): + return self._server.is_managed_ip(ip) + def get_filename(self, ip): return self._server.get_filename(ip) - def run(self): - self._server.bind() - self._server.forever() +class TftpDaemon(Daemon): -class TftpDaemon(Thread): - - def __init__(self, logger, config, bootpd=None): - super(TftpDaemon, self).__init__(name="TftpDeamon") - self.daemon = True + def __init__(self, logger, config, debug, bootpd=None): + super(TftpDaemon, self).__init__(debug) self._server = TftpServer(logger=logger, config=config, bootpd=bootpd) - def run(self): - self._server.bind() - self._server.forever() + +class HttpDaemon(Daemon): + + def __init__(self, logger, config, debug, bootpd=None): + super(HttpDaemon, self).__init__(debug) + self.daemon = True + self._server = HttpServer(logger=logger, config=config, bootpd=bootpd) def main(): debug = False try: argparser = ArgumentParser(description=modules[__name__].__doc__) - argparser.add_argument('-c', '--config', dest='config', + argparser.add_argument('-c', '--config', default='pybootd/etc/pybootd.ini', help='configuration file') - argparser.add_argument('-p', '--pxe', dest='pxe', - action='store_true', - help='enable BOOTP/DHCP/PXE server only') - argparser.add_argument('-t', '--tftp', dest='tftp', - action='store_true', - help='enable TFTP server only') + argparser.add_argument('-p', '--pxe', action='store_true', + help='only enable BOOTP/DHCP/PXE server') + argparser.add_argument('-t', '--tftp', action='store_true', + help='only enable TFTP server') + argparser.add_argument('-H', '--http', action='store_true', + help='enable HTTP server (default: disabled)') argparser.add_argument('-d', '--debug', action='store_true', help='enable debug mode') args = argparser.parse_args() @@ -100,18 +125,33 @@ def main(): 'info')) logger.info('-'.join(('pybootd', __version__))) - daemon = None + daemons = OrderedDict() if not args.tftp: - daemon = BootpDaemon(logger, cfgparser) + daemon = BootpDaemon(logger, cfgparser, debug) daemon.start() + daemons['bootp'] = daemon if not args.pxe: - daemon = TftpDaemon(logger, cfgparser, daemon) + daemon = TftpDaemon(logger, cfgparser, debug, + daemons.get('bootp', None)) daemon.start() - if daemon: - while True: - daemon.join(0.5) + daemons['tftp'] = daemon + if args.http: + daemon = HttpDaemon(logger, cfgparser, debug, daemons.get('bootp')) + daemon.start() + daemons['http'] = daemon + resume = True + while daemons: + zombies = set() + for name, daemon in daemons.items(): + if not resume: + daemon.stop() + daemon.join(0.1) if not daemon.is_alive(): - break + logger.warn('%s daemon terminated', name) + zombies.add(name) + resume = False + for name in zombies: + del daemons[name] except Exception as exc: print('\nError: %s' % exc, file=stderr) if debug: diff --git a/pybootd/etc/pybootd.ini b/pybootd/etc/pybootd.ini index 1fbbe77..0167aae 100644 --- a/pybootd/etc/pybootd.ini +++ b/pybootd/etc/pybootd.ini @@ -37,6 +37,12 @@ always_check = disable root = ./images ;root = http://http.us.debian.org/debian/dists/squeeze/main/installer-amd64/current/images/netboot +[httpd] +; address = (use address from bootpd) +port = 8000 +check_ip = yes +root = /tmp + [filters] ;pxelinux.cfg/* = pybootd/etc/pxe.cfg egg.info = [{filename}] diff --git a/pybootd/httpd.py b/pybootd/httpd.py new file mode 100755 index 0000000..d5fdbed --- /dev/null +++ b/pybootd/httpd.py @@ -0,0 +1,99 @@ +# Copyright (c) 2010-2019 Emmanuel Blot +# Copyright (c) 2010-2011 Neotion +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +"""HTTPd tiny server to exercise the pybootd daemon""" + +from argparse import ArgumentParser, FileType +from functools import partial +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +from os import getcwd +from os.path import isfile, join as joinpath, realpath +from sys import exit as sysexit, modules, stderr +from traceback import format_exc +from urllib.parse import parse_qs, urlsplit +from .util import logger_factory, to_bool, to_int, EasyConfigParser + + +#pylint: disable-msg=broad-except +#pylint: disable-msg=missing-docstring +#pylint: disable-msg=invalid-name + + +class HttpRequestHandler(SimpleHTTPRequestHandler): + + def __init__(self, *args, **kwargs): + super(HttpRequestHandler, self).__init__(*args, **kwargs) + + def do_HEAD(self): + if self._validate(): + super(HttpRequestHandler, self).do_HEAD() + + def do_GET(self): + if self._validate(): + super(HttpRequestHandler, self).do_GET() + + def log_request(self, code='-', size='-'): + self.server.log.info('Request: %s %s', code, size) + + def log_error(self, fmt, *args): + self.server.log.error(fmt, *args) + + def _validate(self): + log = self.server.log + bootpd = self.server.bootpd + if bootpd and self.server.check_ip: + ipaddr = self.client_address[0] + if not bootpd.is_managed_ip(ipaddr): + log.info('Unknown IP: %s', ipaddr) + self.send_error(401, 'Not authorized') + return False + log.debug("Request: %s" % self.path) + path = realpath(joinpath(self.directory, self.path.lstrip('/'))) + if not path.startswith(self.directory): + log.info('Malformed path: %s', path) + self.send_error(403, 'Forbidden') + return False + if not isfile(path): + log.info('Invalid path: %s', path) + self.send_error(404, 'Not found') + return False + return True + +class HttpServer(ThreadingHTTPServer): + + HTTP_SECTION = 'httpd' + + def __init__(self, logger, config, bootpd=None): + self.log = logger + self.config = config + self.bootpd = bootpd + netconfig = bootpd and bootpd.get_netconfig() + address = (self.config.get(self.HTTP_SECTION, 'address', + netconfig and netconfig['server']), + int(self.config.get(self.HTTP_SECTION, 'port', '80'))) + root = realpath(self.config.get(self.HTTP_SECTION, 'root', None)) + self.check_ip = to_bool(self.config.get(self.HTTP_SECTION, 'check_ip', + 'yes')) + self.log.info('Listening to %s:%s' % address) + handler = partial(HttpRequestHandler, directory=root) + super(HttpServer, self).__init__(address, handler) + + def start(self): + self.serve_forever() + + def stop(self): + self.shutdown() diff --git a/pybootd/pxed.py b/pybootd/pxed.py index 7ae5c03..b41c7a9 100644 --- a/pybootd/pxed.py +++ b/pybootd/pxed.py @@ -255,7 +255,10 @@ class BootpServer: def get_netconfig(self): return self.netconfig - def bind(self): + def is_managed_ip(self, address): + return address in self.ippool.values() + + def start(self): host = self.config.get(self.BOOTP_SECTION, 'address', '0.0.0.0') port = self.config.get(self.BOOTP_SECTION, 'port', str(BOOTP_PORT_REQUEST)) @@ -265,11 +268,12 @@ class BootpServer: self.sock.append(sock) self.log.info('Listening to %s:%s' % (host, port)) sock.bind((host, int(port))) - - def forever(self): - while True: + self._resume = True + while self._resume: try: - r = select(self.sock, [], self.sock)[0] + r = select(self.sock, [], self.sock, 0.25)[0] + if not r: + continue for sock in r: data, addr = sock.recvfrom(556) self.handle(sock, addr, data) @@ -277,6 +281,9 @@ class BootpServer: self.log.critical('%s\n%s' % (exc, format_exc())) sleep(1) + def stop(self): + self._resume = False + def parse_options(self, tail): self.log.debug('Parsing DHCP options') dhcp_tags = {} diff --git a/pybootd/tests/httpd.py b/pybootd/tests/httptestd.py similarity index 100% rename from pybootd/tests/httpd.py rename to pybootd/tests/httptestd.py diff --git a/pybootd/tftpd.py b/pybootd/tftpd.py index 03990ac..39ef502 100644 --- a/pybootd/tftpd.py +++ b/pybootd/tftpd.py @@ -416,8 +416,9 @@ class TftpServer: self.root = self.config.get(self.TFTP_SECTION, 'root', os.getcwd()) self.fcre, self.filepatterns = self.get_file_filters() self.genfilecre = recompile(r'\[(?P[\w\.\-]+)\]') + self._resume = False - def bind(self): + def start(self): netconfig = self.bootpd and self.bootpd.get_netconfig() host = self.config.get(self.TFTP_SECTION, 'address', netconfig and netconfig['server']) @@ -427,20 +428,23 @@ class TftpServer: sock = socket(AF_INET, SOCK_DGRAM) self.sock.append(sock) sock.bind((host, port)) - - def forever(self): - while True: - if self.bootpd: - if not self.bootpd.is_alive(): - self.log.info('Bootp daemon is dead, exiting') - break - r = select(self.sock, [], self.sock)[0] + self.log.info('Listening to %s:%s' % (host, port)) + self._resume = True + while self._resume: + r = select(self.sock, [], self.sock, 0.25)[0] + if not r: + continue for sock in r: data, addr = sock.recvfrom(516) tc = TftpConnection(self) - thread = Thread(target=tc.connect, args=(addr, data)) + thread = Thread(target=tc.connect, args=(addr, data), + daemon=True) thread.start() + + def stop(self): + self._resume = False + def filter_file(self, connexion, mo): # extract the position of the matching pattern, then extract the # conversion string from the file convertion sequence From 1ba2f7d609ec8b3e5cf9dcc8dee3007a3b4a64d5 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Fri, 6 Sep 2019 16:23:56 +0200 Subject: [PATCH 14/17] Tweak `setup.py` --- setup.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 570321c..8803537 100644 --- a/setup.py +++ b/setup.py @@ -17,16 +17,17 @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -import os from distutils.core import setup +from os.path import dirname, join as joinpath +from sys import platform def _read(fname): - return open(os.path.join(os.path.dirname(__file__), fname)).read() + return open(joinpath(dirname(__file__), fname)).read() requirements = [] -if os.uname()[0].lower() == 'darwin': +if platform == 'darwin': requirements.append('netifaces (>= 0.5)') From 9992ae524b7cd41190d989695e820506ce6847e7 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Fri, 6 Sep 2019 18:44:11 +0200 Subject: [PATCH 15/17] Fix package generation --- .gitignore | 1 + setup.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 00bca28..c0d2f86 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.egg-info *.pyc *.pyo +MANIFEST **/*-local.ini dist/ build/ diff --git a/setup.py b/setup.py index 8803537..10855cd 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ from distutils.core import setup from os.path import dirname, join as joinpath from sys import platform - +from pybootd import __version__ def _read(fname): return open(joinpath(dirname(__file__), fname)).read() @@ -33,7 +33,7 @@ if platform == 'darwin': setup( name='pybootd', - version='1.6.0', + version=__version__, description='Simplified BOOTP/DHCP/PXE and TFTP server', author='Emmanuel Blot', author_email='emmanuel.blot@free.fr', From 3c9603ab5dc881b57946e4f0ed9f1595b649a161 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Fri, 6 Sep 2019 18:45:12 +0200 Subject: [PATCH 16/17] Should address #13: infinite padding decoding loop --- pybootd/pxed.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pybootd/pxed.py b/pybootd/pxed.py index b41c7a9..e6d7b8e 100644 --- a/pybootd/pxed.py +++ b/pybootd/pxed.py @@ -287,11 +287,16 @@ class BootpServer: def parse_options(self, tail): self.log.debug('Parsing DHCP options') dhcp_tags = {} + padding_count = 0 while tail: tag = tail[0] # padding if tag == 0: + padding_count += 1 + if padding_count > 255: + raise ValueError('Padding overflow') continue + padding_count = 0 if tag == 0xff: return dhcp_tags length = tail[1] From 79d223d3fe7a5bbc04c186cb1d3994000fe02047 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Fri, 6 Sep 2019 18:54:10 +0200 Subject: [PATCH 17/17] Fix #6 --- pybootd/pxed.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pybootd/pxed.py b/pybootd/pxed.py index e6d7b8e..1c13ac4 100644 --- a/pybootd/pxed.py +++ b/pybootd/pxed.py @@ -609,7 +609,6 @@ class BootpServer: int(self.config.get(self.BOOTP_SECTION, 'lease_time', str(24*3600)))) - pkt += spack('!BB', DHCP_END, 0) # do not attempt to produce a PXE-augmented response for # regular DHCP requests @@ -620,12 +619,15 @@ class BootpServer: else: extra_buf = self.build_dhcp_options(hostname) + pkt += extra_buf + pkt += spack('!BB', DHCP_END, 0) + # update the UUID cache if pxe: self.uuidpool[mac_addr] = uuid # send the response - sock.sendto(pkt + extra_buf, addr) + sock.sendto(pkt, addr) # update the current state if currentstate != newstate: