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',