1
1
mirror of https://github.com/eblot/pybootd.git synced 2024-07-14 17:20:30 +03:00

Start porting to Py3 only and fix old syntax

This commit is contained in:
Emmanuel Blot 2019-09-03 18:40:34 +02:00
parent efe3ff3b40
commit d1231d55c0
8 changed files with 137 additions and 101 deletions

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright (c) 2010-2016 Emmanuel Blot <emmanuel.blot@free.fr>
# Copyright (c) 2010-2019 Emmanuel Blot <emmanuel.blot@free.fr>
# Copyright (c) 2010-2011 Neotion
#
# This library is free software; you can redistribute it and/or

View File

@ -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:

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2010-2016 Emmanuel Blot <emmanuel.blot@free.fr>
# Copyright (c) 2010-2019 Emmanuel Blot <emmanuel.blot@free.fr>
# 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...")

View File

@ -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

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2010-2016 Emmanuel Blot <emmanuel.blot@free.fr>
# Copyright (c) 2010-2019 Emmanuel Blot <emmanuel.blot@free.fr>
# 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')

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2010-2016 Emmanuel Blot <emmanuel.blot@free.fr>
# Copyright (c) 2010-2019 Emmanuel Blot <emmanuel.blot@free.fr>
# 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)

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2010-2016 Emmanuel Blot <emmanuel.blot@free.fr>
# Copyright (c) 2010-2019 Emmanuel Blot <emmanuel.blot@free.fr>
# 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)

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2010-2016 Emmanuel Blot <emmanuel.blot@free.fr>
# Copyright (c) 2010-2019 Emmanuel Blot <emmanuel.blot@free.fr>
# 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',