1
1
mirror of https://github.com/eblot/pybootd.git synced 2024-10-05 19:49:04 +03:00

Initial commit

This commit is contained in:
Emmanuel Blot 2011-05-21 00:21:53 +02:00
commit 89405ec150
16 changed files with 1795 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
*.egg-info
*.pyc
*.pyo
dist/
build/

10
AUTHORS Normal file
View File

@ -0,0 +1,10 @@
Main developers:
* Emmanuel Blot <emmanuel.blot@free.fr>
* Emmanuel Bouaziz <ebouaziz@free.fr>
Contributors:
Main references and prior work:
This work has been initially forked from Milo's MediaMVP project:
http://www.milosoftware.com/mvp/

17
LICENSE Normal file
View File

@ -0,0 +1,17 @@
Copyright (c) 2008-2011 Neotion.
Copyright (c) 2008-2011 Emmanuel Blot <emmanuel.blot@free.fr>
All Rights Reserved.
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

67
README.rst Normal file
View File

@ -0,0 +1,67 @@
=========
PyBootd
=========
--------
Overview
--------
PyBootd is a daemon that supports a subset of the BOOTP, DHCP, PXE and TFTP
protocols, with some handy extensions.
One of its main goals is to provide a simple solution to boot up any
PXE-enabled personal computer, without requiring any other tools but a standard
Python installation.
Pybootd can be used for any network boot up, or to install an OS without any
physical support such as a USB key or CD/DVD.
------------
Requirements
------------
- Python_ 2.6 or above is required. Python_ 3.x is not yet supported.
- Netifaces_ Python module
.. _Python: http://python.org/
.. _Netifaces: http://alastairs-place.net/netifaces/
Permissions:
- DHCP protocol requires the daemon to listen on port 67.
- TFTP protocol requires the daemon to listen on port 69.
As these ports are within the server 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)
2. MAC address ACL
3. UUID based ACL - requires PXE protocol
4. HTTP forwarding - authorization is delegated to a remote server using
simple HTTP GET requests
- Local or remote file serving:
- For example, it is possible to boot up a full Debian system directly from
the Internet, without storing any file on the pybootd host machine
- Network notification of client requests through UDP messages
- File name translation
- Files requested from TFTP clients can be filtered and transformed into
local filenames using filters
- It is possible to use pybootd with only one of the offered services, either
TFTP or DHCP

78
forward.sh Executable file
View File

@ -0,0 +1,78 @@
#!/bin/sh
# Simple script to enable / disable IP forwarding
# Show usage information
usage()
{
NAME=`basename $0`
cat <<EOT
$NAME [options] <on|off>
Enable or disable IP forwarding
-h Print this help message
-i INTERFACE WAN interface name
EOT
}
INTERFACE=""
ENABLE=0
# Parse the command line and update configuration
while [ $# -ge 0 ]; do
case "$1" in
-h)
usage
exit 0
;;
-i)
shift
INTERFACE=$1
;;
-*)
usage
echo "Unsupported option: $1"
exit 1
;;
on)
ENABLE=1
;;
off)
ENABLE=0
;;
'')
break
;;
*)
usage
echo "Unsupported command: $1"
exit 1
;;
esac
shift
done
case "${OSTYPE}" in
darwin*)
if [ -z "${INTERFACE}" ]; then
INTERFACE="en0"
fi
if [ ${ENABLE} -eq 1 ]; then
echo "Enabling IP forwarding through interface $INTERFACE"
sysctl -w net.inet.ip.forwarding=1
natd -interface ${INTERFACE}
ipfw add divert natd ip from any to any via ${INTERFACE}
else
echo "Disabling IP forwarding"
ipfw delete `sudo ipfw show | grep divert | cut -d' ' -f1`
killall natd
sysctl -w net.inet.ip.forwarding=0
fi
;;
linux*)
echo "Forward mode for Linux is not supported yet" >&2
exit 1
;;
*)
echo "Forward mode for OS '${OSTYPE}' is not supported yet" >&2
exit 1
;;
esac

25
pybootd.py Executable file
View File

@ -0,0 +1,25 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2010-2011 Emmanuel Blot <emmanuel.blot@free.fr>
# 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
import sys
from pybootd.daemons import main
if __name__ == "__main__":
main()

61
pybootd/__init__.py Normal file
View File

@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2010-2011 Emmanuel Blot <emmanuel.blot@free.fr>
# 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
import os
import sys
def _get_package_name(default='', version='1.5.0'):
from pkg_resources import WorkingSet
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')
def pybootd_path(path):
newpath = ''
if path.startswith(os.sep):
newpath = path
elif os.path.exists(path):
newpath = path
else:
try:
from pkg_resources import Requirement, resource_filename
newpath = resource_filename(Requirement.parse(PRODUCT_NAME), path)
if not newpath:
from pkg_resources import get_distribution
localpath = get_distribution(PRODUCT_NAME).location
newpath = os.path.join(localpath, path)
except KeyError:
raise IOError('No such file or directory (resource)')
if not os.path.isfile(newpath) and not os.path.isdir(newpath):
raise IOError('No such file or directory (local)')
return newpath

102
pybootd/daemons.py Executable file
View File

@ -0,0 +1,102 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2010-2011 Emmanuel Blot <emmanuel.blot@free.fr>
# 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
from optparse import OptionParser
from pxed import BootpServer
from pybootd import pybootd_path, PRODUCT_NAME, __version__ as VERSION
from tftpd import TftpServer
from util import logger_factory, EasyConfigParser
import os
import sys
import threading
class BootpDaemon(threading.Thread):
def __init__(self, logger, config):
threading.Thread.__init__(self, name="BootpDeamon")
self.daemon = True
self._server = BootpServer(logger=logger, config=config)
def get_netconfig(self):
return self._server.get_netconfig()
def get_filename(self, ip):
return self._server.get_filename(ip)
def run(self):
self._server.bind()
self._server.forever()
class TftpDaemon(threading.Thread):
def __init__(self, logger, config, bootpd=None):
threading.Thread.__init__(self, name="TftpDeamon")
self.daemon = True
self._server = TftpServer(logger=logger, config=config, bootpd=bootpd)
def run(self):
self._server.bind()
self._server.forever()
def main():
usage = 'Usage: %prog [options]\n' \
' PXE boot up server, a tiny BOOTP/DHCP/TFTP server'
optparser = OptionParser(usage=usage)
optparser.add_option('-c', '--config', dest='config',
default='pybootd/etc/pybootd.ini',
help='configuration file')
optparser.add_option('-p', '--pxe', dest='pxe', action='store_true',
help='enable BOOTP/DHCP/PXE server only')
optparser.add_option('-t', '--tftp', dest='tftp', action='store_true',
help='enable TFTP server only')
(options, args) = optparser.parse_args(sys.argv[1:])
if not options.config:
raise AssertionError('Missing configuration file')
if options.pxe and options.tftp:
raise AssertionError('Cannot exclude both servers')
cfgparser = EasyConfigParser()
with open(pybootd_path(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'))
logger.info('-'.join((PRODUCT_NAME, VERSION)))
try:
if not options.tftp:
bt = BootpDaemon(logger, cfgparser)
bt.start()
else:
bt = None
if not options.pxe:
ft = TftpDaemon(logger, cfgparser, bt)
ft.start()
while True:
import time
time.sleep(5)
except AssertionError, e:
print >> sys.stderr, "Error: %s" % str(e)
sys.exit(1)
except KeyboardInterrupt:
print "Aborting..."

8
pybootd/etc/pxe.cfg Normal file
View File

@ -0,0 +1,8 @@
default pxeboot
label pxeboot
menu label ^PxeBoot
menu default
kernel bzImage
append vga=0x305 fbcon=scrollback:4096k initrd=rootfs.i686.cramfs
prompt 0
timeout 0

40
pybootd/etc/pybootd.ini Normal file
View File

@ -0,0 +1,40 @@
[logger]
type = stderr
level = info
[bootp]
address = 0.0.0.0
; pool_start should be in a valid subnet
pool_start = 192.168.25.100
pool_count = 5
domain = localdomain
server_name = debug
boot_file = pxelinux.0
lease_time = 86400
access = mac
allow_simple_dhcp = enable
dns = 10.130.0.2
boot_file = pxelinux.0
; 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
[uuid]
12345678-abcd-ef00-1111-abcdefabcdef = enable
12345678-abcd-ef00-1111-123412341234 = enable
[http]
location = 127.0.0.1:13400
pxe = boot
dhcp = linux
[tftp]
;address = (use address from bootpd)
;root = ./images
root = http://http.us.debian.org/debian/dists/squeeze/main/installer-amd64/current/images/netboot
[filters]
;pxelinux.cfg/* = pybootd/etc/pxe.cfg
egg.info = [{filename}]

601
pybootd/pxed.py Normal file
View File

@ -0,0 +1,601 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2010-2011 Emmanuel Blot <emmanuel.blot@free.fr>
# 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
import re
import select
import socket
import string
import struct
import sys
import time
from binascii import hexlify
from pybootd import PRODUCT_NAME
from util import hexline, to_bool, iptoint, inttoip, get_iface_config
BOOTP_PORT_REQUEST = 67
BOOTP_PORT_REPLY = 68
BOOTREQUEST = 1
BOOTREPLY = 2
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,
BOOTP_CHADDR,BOOTP_SNAME,BOOTP_FILE,BOOTP_VEND) = range(15)
BOOTP_FLAGS_NONE = 0
BOOTP_FLAGS_BROADCAST = 1<<15
COOKIE='\0x63\0x82\0x53\0x63'
DHCP_OPTIONS = { 0: 'Byte padding',
1: 'Subnet mask',
2: 'Time offset',
3: 'Routers',
4: 'Time servers',
5: 'Name servers',
6: 'Domain name servers',
7: 'Log servers',
8: 'Cookie servers',
9: 'Line printer servers',
10: 'Impress servers',
11: 'Resource location servers',
12: 'Host Name', # + PXE extensions
13: 'Boot file size',
14: 'Dump file',
15: 'Domain name',
16: 'Swap server',
17: 'Root path',
18: 'Extensions path',
# --- IP layer / host ---
19: 'IP forwarding',
20: 'Source routing',
21: 'Policy filter',
22: 'Maximum datagram reassembly size',
23: 'Default IP TTL',
24: 'Path MTU aging timeout',
25: 'Path MTU plateau table',
# --- IP Layer / interface ---
26: 'Interface MTU',
27: 'All subnets local',
28: 'Broadcast address',
29: 'Perform mask discovery',
30: 'Mask supplier',
31: 'Perform router discovery',
32: 'Router solicitation address',
33: 'Static route',
# --- Link layer ---
34: 'Trailer encapsulation',
35: 'ARP cache timeout',
36: 'Ethernet encaspulation',
# --- TCP ---
37: 'TCP default TTL',
38: 'TCP keepalive interval',
39: 'TCP keepalive garbage',
# --- Application & Services ---
40: 'Network Information Service domain',
41: 'Network Information servers',
42: 'Network Time Protocol servers',
43: 'Vendor specific',
44: 'NetBIOS over TCP/IP name server',
45: 'NetBIOS over TCP/IP datagram server',
46: 'NetBIOS over TCP/IP node type',
47: 'NetBIOS over TCP/IP scope',
48: 'X Window system font server',
49: 'X Window system display manager',
50: 'Requested IP address',
51: 'IP address lease time',
52: 'Option overload',
53: 'DHCP message',
54: 'Server ID',
55: 'Param request list',
56: 'Error message',
57: 'Message length',
58: 'Renewal time',
59: 'Rebinding time',
60: 'Class ID',
61: 'GUID',
64: 'Network Information Service+ domain',
65: 'Network Information Service+ servers',
66: 'TFTP server name',
67: 'Bootfile name',
68: 'Mobile IP home agent',
69: 'Simple Mail Transport Protocol servers',
70: 'Post Office Protocol servers',
71: 'Network News Transport Protocol servers',
72: 'World Wide Web servers',
73: 'Finger servers',
74: 'Internet Relay Chat server',
93: 'System architecture',
94: 'Network type',
97: 'UUID',
255: 'End of DHCP options' }
DHCP_DISCOVER = 1
DHCP_OFFER = 2
DHCP_REQUEST = 3
DHCP_DECLINE = 4
DHCP_ACK = 5
DHCP_NAK = 6
DHCP_RELEASE = 7
DHCP_INFORM = 8
DHCP_RENEWING = 100
DHCP_IP_MASK = 1
DHCP_IP_GATEWAY = 3
DHCP_IP_DNS = 6
DHCP_LEASE_TIME = 51
DHCP_MSG = 53
DHCP_SERVER = 54
DHCP_END = 255
PXE_DISCOVERY_CONTROL = 6
DISCOVERY_MCAST_ADDR = 7
PXE_BOOT_SERVERS = 8
PXE_BOOT_MENU = 9
PXE_MENU_PROMPT = 10
class BootpError(Exception):
"""Bootp error"""
pass
class BootpServer:
"""BOOTP Server
Implements bootstrap protocol"""
ACCESS_LOCAL = ['uuid', 'mac'] # Access modes, defined locally
ACCESS_REMOTE = ['http'] # Access modes, remotely retrieved
(ST_IDLE, ST_PXE, ST_DHCP) = range(3) # Current state
def __init__(self, logger, config):
self.sock = []
self.log = logger
self.config = config
self.uuidpool = {} # key MAC address value, value UUID value
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.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,
'pool_count', '10'))
self.netconfig = get_iface_config(self.pool_start)
if not self.netconfig:
raise BootpError('Unable to detect network configuration')
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')
self.notify = []
if nlist:
try:
nlist = nlist.split(';')
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))
access = self.config.get(self.bootp_section, 'access')
if not access:
self.acl = None
else:
access = access.lower()
if access not in self.ACCESS_LOCAL + self.ACCESS_REMOTE:
raise BootpError('Invalid access mode: %s' % access)
if not self.config.has_section(access):
raise BootpError("Missing access section '%s'" % access)
self.acl = {}
if access in self.ACCESS_LOCAL:
for entry in self.config.options(access):
self.acl[entry.upper()] = \
to_bool(self.config.get(access, entry))
self.access = access
# Private
def _notify(self, notice, uuid_str, mac_str, ip):
if uuid_str:
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)
for n in self.notify:
self.log.info('Notifying %s with %s' % (n, msg))
notify_sock.sendto(msg, n)
# Public
def get_netconfig(self):
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',
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)
self.sock.append(sock)
self.log.info('Listening to %s:%s' % (host, port))
sock.bind((host, int(port)))
def forever(self):
while True:
try:
r,w,e = select.select(self.sock, [], self.sock)
for sock in r:
data, addr = sock.recvfrom(556)
self.handle(sock, addr, data)
except Exception, e:
import traceback
self.log.critical('%s\n%s' % (str(e), traceback.format_exc()))
time.sleep(1)
def parse_options(self, tail):
self.log.debug('Parsing DHCP options')
dhcp_tags = {}
while tail:
tag = ord(tail[0])
# padding
if tag == 0:
continue
if tag == 0xff:
return dhcp_tags
length = ord(tail[1])
(value, ) = struct.unpack('!%ss' % length, tail[2:2+length])
tail = tail[2+length:]
try:
option = DHCP_OPTIONS[tag]
self.log.debug(" option %d: '%s', size:%d %s" % \
(tag, option, length, hexline(value)))
except KeyError:
self.log.error(' unknown option %d, size:%d %s:' % \
(tag, length, hexline(value)))
return None
dhcp_tags[tag] = value
def build_pxe_options(self, options, server):
try:
buf = ''
uuid = options[97]
buf += struct.pack('!BB%ds' % len(uuid),
97, len(uuid), uuid)
clientclass = options[60]
clientclass = clientclass[:clientclass.find(':')]
buf += struct.pack('!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,
0, 1, server)
srvstr = 'Python'
vendor += struct.pack('!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,
1+len(prompt), len(prompt), prompt)
buf += struct.pack('!BB%ds' % len(vendor), 43,
len(vendor), vendor)
buf += struct.pack('!BBB', 255, 0, 0)
return buf
except KeyError, e:
self.log.error('Missing options, cancelling: ' + str(e))
return None
def build_dhcp_options(self, clientname):
buf = ''
if not clientname:
return buf
buf += struct.pack('!BB%ds' % len(clientname),
12, len(clientname), clientname)
return buf
def handle(self, sock, addr, data):
self.log.info('Sender: %s on socket %s' % (addr, sock.getsockname()))
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]))
if buf[BOOTP_OP] != BOOTREQUEST:
self.log.warn('Not a BOOTREQUEST')
return
options = self.parse_options(tail)
if options is None:
self.log.warn('Error in option parsing, ignore request')
return
# Extras (DHCP options)
try:
dhcp_msg_type = ord(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])
# is the UUID received (PXE mode)
if 97 in options and len(options[97]) == 17:
uuid = options[97][1:]
pxe = True
self.log.info('PXE UUID has been received')
# or retrieved from the cache (DHCP mode)
else:
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()
if uuid_str:
self.log.info('UUID is %s for MAC %s' % (uuid_str, mac_str))
hostname = ''
filename = ''
# Basic state machine
currentstate = self.states.setdefault(mac_str, self.ST_IDLE)
newstate = currentstate
if currentstate == self.ST_IDLE:
if pxe and (dhcp_msg_type == DHCP_DISCOVER):
# BIOS is booting up, and try to locate a DHCP server
newstate = self.ST_PXE
elif currentstate == self.ST_PXE:
if not pxe and (dhcp_msg_type == DHCP_REQUEST):
# OS is booting up, and confirm a previous DHCP dicovery
newstate = self.ST_DHCP
else: # currentstate == self.ST_DHCP
if pxe:
# OS was running but the BIOS is performing a DHCP request:
# board has been restarted
newstate = self.ST_PXE
# 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:
return
# if access control is enable
if self.access:
# 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'))
parameters = {'mac' : mac_str}
if uuid:
parameters['uuid'] = uuid_str
if not pxe and mac_str in self.ippool:
parameters['ip'] = self.ippool[mac_str]
item = uuid_str or mac_str
# only bother the authentication host when a state change is
# required.
if currentstate != newstate:
query = urllib.urlencode(parameters)
urlparts = (self.access, netloc, path, query, '')
url = urlparse.urlunsplit(urlparts)
self.log.info('Requesting URL: %s' % url)
import urllib2
import httplib
try:
up = urllib2.urlopen(url, timeout=timeout)
for l in up:
try:
# Look for extra definition within the reply
k, v = [x.strip() for x in l.split(':')]
k = k.lower()
if k == 'client':
hostname = v
if k == 'file':
filename = v
except ValueError:
pass
except urllib2.HTTPError, e:
self.log.error('HTTP Error: %s' % str(e))
self.states[mac_str] = self.ST_IDLE
return
except urllib2.URLError, e:
self.log.critical('Internal error: %s' % str(e))
self.states[mac_str] = self.ST_IDLE
return
except httplib.HTTPException, e:
self.log.error('Server error: %s' % type(e))
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]
if not item:
self.log.info('Missing %s identifier, '
'ignoring %s request' % (self.access, mac_str))
return
if not item in self.acl:
self.log.info('%s is not in ACL list, '
'ignoring %s request' % (item, mac_str))
return
if not self.acl[item]:
self.log.info('%s access is disabled, '
'ignoring %s request' % (item, mac_str))
return
else:
item = locals()['%s_str' % self.access]
self.log.info('%s access is authorized, '
'request will be satisfied' % item)
# construct reply
buf[BOOTP_OP] = BOOTREPLY
self.log.info('Client IP: %s' % socket.inet_ntoa(buf[7]))
if buf[BOOTP_CIADDR] == '\x00\x00\x00\x00':
self.log.debug('Client needs its address')
ipaddr = iptoint(self.pool_start)
ip = None
if mac_str in self.ippool:
ip = self.ippool[mac_str]
self.log.info('Lease for MAC %s already defined as IP %s' % \
(mac_str, ip))
else:
for idx in xrange(self.pool_count):
ipkey = inttoip(ipaddr+idx)
self.log.debug('Check for IP %s' % ipkey)
if ipkey not in self.ippool.values():
self.ippool[mac_str] = ipkey
ip = ipkey
break
if not ip:
raise BootpError('No more IP available in definined pool')
mask = iptoint(self.netconfig['mask'])
reply_broadcast = iptoint(ip) & mask
reply_broadcast |= (~mask)&((1<<32)-1)
buf[BOOTP_YIADDR] = socket.inet_aton(ip)
buf[BOOTP_SECS] = 0
buf[BOOTP_FLAGS] = BOOTP_FLAGS_NONE
addr = (inttoip(reply_broadcast), addr[1])
self.log.debug('Reply to: %s:%s' % addr)
else:
buf[BOOTP_YIADDR] = buf[BOOTP_CIADDR]
ip = socket.inet_ntoa(buf[BOOTP_YIADDR])
buf[BOOTP_SIADDR] = buf[BOOTP_GIADDR] = socket.inet_aton(server_addr)
# sname
buf[BOOTP_SNAME] = \
'.'.join([self.config.get(self.bootp_section,
'servername', 'unknown'),
self.config.get(self.bootp_section,
'domain', 'localdomain')])
# file
buf[BOOTP_FILE] = self.config.get(self.bootp_section,
'boot_file', '\x00')
if not dhcp_msg_type:
self.log.warn('No DHCP message type found, discarding request')
return
if dhcp_msg_type == DHCP_DISCOVER:
self.log.debug('DHCP DISCOVER')
dhcp_reply = DHCP_OFFER
self.log.info('Offering lease for MAC %s: IP %s' % \
(mac_str, ip))
elif dhcp_msg_type == DHCP_REQUEST:
self.log.debug('DHCP REQUEST')
dhcp_reply = DHCP_ACK
self.log.info('New lease for MAC %s: IP %s' % \
(mac_str, ip))
elif dhcp_msg_type == DHCP_RELEASE:
self.log.info('DHCP RELEASE')
if not self.notify:
return
elif dhcp_msg_type == DHCP_INFORM:
self.log.info('DHCP INFORM')
return
else:
self.log.error('Unmanaged DHCP message: %d' % dhcp_msg_type)
return
# notify the sequencer
if self.notify:
if DHCP_REQUEST == dhcp_msg_type:
if 97 in options:
self._notify('BOOT', uuid_str, mac_str, ip)
else:
self._notify('LEASE', uuid_str, mac_str, ip)
elif DHCP_RELEASE == dhcp_msg_type:
self._notify('RELEASE', uuid_str, mac_str, ip)
return
# Store the filename
if filename:
self.log.info("Filename for IP %s is '%s'" % (ip, filename))
self.filepool[ip] = filename
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)
mask = socket.inet_aton(self.netconfig['mask'])
pkt += struct.pack('!BB4s', DHCP_IP_MASK, 4, mask)
pkt += struct.pack('!BB4s', DHCP_IP_GATEWAY, 4, server)
dns = self.config.get(self.bootp_section,
'dns', None)
if dns:
if dns.lower() == 'auto':
dns = self.get_dns_server() or socket.inet_ntoa(server)
dns = socket.inet_aton(dns)
pkt += struct.pack('!BB4s', DHCP_IP_DNS, 4, dns)
pkt += struct.pack('!BBI', DHCP_LEASE_TIME, 4,
int(self.config.get(self.bootp_section, 'lease_time',
str(24*3600))))
pkt += struct.pack('!BB', DHCP_END, 0)
# do not attempt to produce a PXE-augmented response for
# regular DHCP requests
if pxe:
extra_buf = self.build_pxe_options(options, server)
if not extra_buf:
return
else:
extra_buf = self.build_dhcp_options(hostname)
# update the UUID cache
if pxe:
self.uuidpool[mac_addr] = uuid
# send the response
sock.sendto(pkt + extra_buf, addr)
# update the current state
if currentstate != newstate:
self.log.info('Moving from state %d to state %d' % \
(currentstate, newstate))
self.states[mac_str] = newstate
def get_dns_server(self):
nscre = re.compile('nameserver\s+(\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3})\s')
try:
with open('/etc/resolv.conf', 'r') as resolv:
for line in resolv:
mo = nscre.match(line)
if mo:
dns = mo.group(1)
self.log.info('Found primary nameserver: %s' % dns)
return dns
except Exception, e:
pass
self.log.info('No nameserver found')
return None
def get_filename(self, ip):
"""Returns the filename defined for a host"""
filename = self.filepool.get(ip, '')
self.log.info("Filename for IP %s is '%s'" % (ip, filename))
return filename

10
pybootd/tests/config.ini Normal file
View File

@ -0,0 +1,10 @@
[logger]
type = stderr
level = debug
[httpd]
port = 13400
[uuid]
12345678-abcd-ef00-1111-abcdefabcdef = enable
12345678-abcd-ef00-1111-123412341234 = enable

107
pybootd/tests/httpd.py Executable file
View File

@ -0,0 +1,107 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2010-2011 Emmanuel Blot <emmanuel.blot@free.fr>
# 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
import sys
import urlparse
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
from optparse import OptionParser
from util import logger_factory, to_bool, to_int, EasyConfigParser
class HttpdDaemon(HTTPServer):
class ReqHandler(BaseHTTPRequestHandler):
def do_GET(self):
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)
if urlparts.path in ('/boot', '/linux'):
if 'uuid' in query:
uuids = query['uuid']
for uuid in uuids:
uuid = uuid.upper().strip()
authorized = self.server.uuids.get(uuid, False)
log.info('UUID %s is %s' % \
(uuid, authorized and 'authorized' or 'rejected'))
if authorized:
break
else:
authorized = False
log.warn('Request does not specify a UUID')
if authorized:
response = '\n\n' # HTTP protocol, line feed after headers
# dummy generation of a tester number
tester = sum([to_int('0x%s' %x) for x in uuid.split('-')])
clientname = 'Tester-%03d' % (tester & 0xFF)
log.info("UUID %s is assigned as %s" % (uuid, clientname))
response += 'Client: %s\n' % clientname
filename = 'define_filename_here'
if urlparts.path == '/linux':
response += 'File: %s\n' % filename
self.send_response(200, 'Ok')
self.wfile.write(response)
return
self.send_error(401, 'Not authorized')
return
def __init__(self, logger, parser):
address = ('localhost', int(parser.get('httpd', 'port', '80')))
HTTPServer.__init__(self, address, self.ReqHandler)
self.log = logger
self.uuids = {}
access = 'uuid'
if parser.has_section(access):
for entry in parser.options(access):
self.uuids[entry.upper().strip()] = \
to_bool(parser.get(access, entry))
def start(self):
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 AssertionError('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'))
try:
bt = HttpdDaemon(logger, cfgparser)
bt.start()
while True:
import time
time.sleep(5)
except KeyboardInterrupt:
print "Aborting..."

445
pybootd/tftpd.py Normal file
View File

@ -0,0 +1,445 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2010-2011 Emmanuel Blot <emmanuel.blot@free.fr>
# 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
import os
import re
import select
import socket
import string
import struct
import sys
import time
import thread
import urllib2
import urlparse
from cStringIO import StringIO
from pybootd import pybootd_path
from util import hexline
__all__ = ['TftpServer']
TFTP_PORT = 69
class TftpError(AssertionError):
"""Any TFTP error"""
pass
class TftpConnection(object):
RRQ = 1
WRQ = 2
DATA = 3
ACK = 4
ERR = 5
OACK = 6
HDRSIZE = 4 # number of bytes for OPCODE and BLOCK in header
def __init__(self, server, port=0):
self.log = server.log
self.server = server
self.client_addr = None
self.sock = None
self.active = 0 # 0: inactive, 1: active
self.blockNumber = 0
self.lastpkt = ''
self.mode = ''
self.filename = ''
self.file = None
self.time = 0
self.blocksize = self.server.blocksize
self.timeout = self.server.timeout
self._bind('', port)
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)
if host or port:
self.sock.bind((host, port))
def send(self, pkt=''):
self.log.debug('send')
self.sock.sendto(pkt, self.client_addr)
self.lastpkt = pkt
def recv(self):
self.log.debug('recv')
fno = self.sock.fileno()
client_addr = self.client_addr
timeout = self.timeout
retry = self.server.retry
while retry:
r,w,e = select.select([fno], [], [fno], timeout)
if not r:
# We timed out -- retransmit
retry = retry - 1
self.retransmit()
else:
# Read data packet
pktsize = self.blocksize + self.HDRSIZE
data, addr = self.sock.recvfrom(pktsize)
if addr == client_addr:
break
else:
raise TftpError(4, 'Transfer timed out')
# end while
return self.parse(data)
def _filter_file(self, mo):
self.log.debug('_filter_file')
return self.server.filter_file(self, mo)
def _dynreplace(self, matchobj):
"""Dispatch a dynamic replacement function"""
self.log.debug('_dynreplace')
func = getattr(self, '_dynreplace_%s' % matchobj.group(1))
return func(matchobj)
def _dynreplace_filename(self, matchobj):
"""Replace the 'filename' keyword with the client related filename"""
self.log.debug('_dynreplace_filename')
if not self.server.bootpd:
return matchobj.string
client_ip = self.client_addr[0]
path = self.server.bootpd.get_filename(client_ip)
return path
def parse(self, data, unpack=struct.unpack):
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 = 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)
else:
try:
resource = pybootd_path(resource)
except IOError:
if not self.server.genfilecre.match(resource):
if resource.startswith('^%s' % os.sep):
resource = os.path.join( \
os.path.dirname(sys.argv[0]),
resource.lstrip('^%s' % os.sep))
elif self.server.root:
if self.server.root.startswith(os.sep):
# Absolute root directory
resource = os.path.join(self.server.root,
resource)
else:
# Relative root directory, from the daemon path
daemonpath = os.path.dirname(sys.argv[0])
if not daemonpath.startswith(os.sep):
daemonpath = os.path.normpath( \
os.path.join(os.getcwd(), daemonpath))
resource = os.path.join(daemonpath,
self.server.root, resource)
resource = os.path.normpath(resource)
self.log.info("Resource '%s'" % resource)
pkt['filename'] = resource
pkt['mode'] = mode
while options:
key, value, options = options.split('\000', 2)
if key == 'blksize':
self.blocksize = int(value)
elif key == 'timeout':
self.timeout = float(value)
pkt[key] = value
elif opcode == self.ACK:
block = pkt['block'] = unpack('!h', buf[2:4])[0]
elif opcode == self.DATA:
block = pkt['block'] = unpack('!h', buf[2:4])[0]
data = pkt['data'] = buf[4:]
elif opcode == self.ERR:
errnum = pkt['errnum'] = unpack('!h', buf[2:4])[0]
errtxt = pkt['errtxt'] = buf[4:-1]
else:
raise TftpError(4, 'Unknown packet type')
return pkt
def retransmit(self):
if self.lastpkt:
self.log.debug('Retransmit')
self.sock.sendto(self.lastpkt, self.client_addr)
def connect(self, addr, data):
self.log.debug('connect new connection %s:%d' % addr)
self.client_addr = addr
RRQ = self.RRQ
WRQ = self.WRQ
DATA = self.DATA
ACK = self.ACK
ERR = self.ERR
self.log.info('Client: %s:%d' % addr)
try:
pkt = self.parse(data)
opcode = pkt['opcode']
if opcode not in (RRQ, WRQ):
raise TftpError(4, 'Bad request')
# Start lock-step transfer
self.active = 1
if opcode == RRQ:
self.handle_rrq(pkt)
else:
self.handle_wrq(pkt)
# Loop until done
while self.active:
self.log.debug('Still active: %s:%s' % addr)
pkt = self.recv()
opcode = pkt['opcode']
if opcode == DATA:
self.recv_data(pkt)
elif opcode == ACK:
self.recv_ack(pkt)
elif opcode == ERR:
self.recv_err(pkt)
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:
import traceback
self.log.error(traceback.format_exc())
self.log.debug('Ending connection %s:%s' % addr)
def recv_ack(self, pkt):
self.log.debug('recv_ack')
if pkt['block'] == self.blockNumber:
# We received the correct ACK
self.handle_ack(pkt)
else:
self.log.warn('Expecting ACK for block %d, received %d' % \
(pkt['block'], self.blockNumber))
def recv_data(self, pkt):
self.log.debug('recv_data')
if pkt['block'] == self.blockNumber:
# We received the correct DATA packet
self.active = ( self.blocksize == len(pkt['data']) )
self.handle_data(pkt)
def recv_err(self, pkt):
self.log.debug('recv_err')
self.handle_err(pkt)
self.retransmit()
def send_data(self, data, pack=struct.pack):
self.log.debug('send_data')
if not self.time:
self.time = time.time()
blocksize = self.blocksize
block = self.blockNumber = self.blockNumber + 1
lendata = len(data)
format = '!hh%ds' % lendata
pkt = pack(format, self.DATA, block, data)
self.send(pkt)
self.active = (len(data) == blocksize)
if not self.active and self.time:
total = time.time()-self.time
self.time = 0
try:
name = self.file.name
size = os.stat(name)[6]
try:
self.log.info('File %s send in %.1f s (%.2f MB/s)' % \
(name, total, size/(total*1024*1024)))
except ZeroDivisionError:
self.log.warn('File %s send in no time' % name)
except AttributeError:
# StringIO does not have a 'name' attribute
pass
except Exception:
import traceback
traceback.print_exc()
pass
def send_ack(self, pack=struct.pack):
self.log.debug('send_ack')
block = self.blockNumber
self.blockNumber = self.blockNumber + 1
format = '!hh'
pkt = pack(format, self.ACK, block)
self.send(pkt)
def send_error(self, errnum, errtext, pack=struct.pack):
self.log.debug('send_error')
errtext = errtext + '\000'
format = '!hh%ds' % len(errtext)
outdata = pack(format, self.ERR, errnum, errtext)
self.sock.sendto(outdata, self.client_addr)
def send_oack(self, options, pack=struct.pack):
self.log.debug('send_oack')
pkt = pack('!h', self.OACK)
for k, v in options:
pkt += k + '\x00' + v + '\x00'
self.send(pkt)
# clear out the last packet buffer to prevent from retransmitting it
self.lastpkt = ''
def handle_rrq(self, pkt):
self.log.debug('handle_rrq')
resource = pkt['filename']
mode = pkt['mode']
genfile = self.server.genfilecre.match(resource)
if 'tsize' in pkt and int(pkt['tsize']) == 0:
if genfile:
filesize = len(genfile.group('name'))
else:
try:
if self.is_url(resource):
rp = urllib2.urlopen(resource)
meta = rp.info()
filesize = int(meta.getheaders('Content-Length')[0])
else:
filesize = os.stat(resource)[6]
except Exception:
self.active = False
self.send_error(1, 'Cannot access resource')
self.log.warn('Cannot stat resource %s' % resource)
return
self.log.info('Send size request file %s size: %d' % \
(resource, filesize))
options = [('tsize', str(filesize))]
if 'blksize' in pkt:
options.append(('blksize', pkt['blksize']))
self.send_oack(options)
if genfile:
self.log.info('Generating file content: %s', genfile.group('name'))
self.file = StringIO(resource[1:-1])
else:
try:
if self.is_url(resource):
self.log.info("Sending resource '%s'" % resource)
self.file = urllib2.urlopen(resource)
else:
resource = os.path.realpath(resource)
self.log.info("Sending file '%s'" % resource)
self.file = open(resource, 'rb')
except Exception:
self.send_error(1, 'Cannot open resource')
self.log.warn('Cannot open file for reading %s: %s' % \
sys.exc_info()[:2])
return
if not 'tsize' in pkt:
self.send_data(self.file.read(self.blocksize))
def handle_wrq(self, pkt):
self.log.debug('handle_wrq')
resource = pkt['filename']
mode = pkt['mode']
if self.is_url(resource):
self.log.error('Writing to URL is not yet supported')
return
try:
self.log.info('Receiving file: %s' % resource)
self.file = open(resource, 'wb')
except:
self.send_error(1, 'Cannot open file')
self.log.error('Cannot open file for writing %s: %s' % \
sys.exc_info()[:2])
return
self.send_ack()
def handle_ack(self, pkt):
self.log.debug('handle_ack')
if self.active:
self.send_data(self.file.read(self.blocksize))
def handle_data(self, pkt):
self.log.debug('handle_data')
self.send_ack()
data = pkt['data']
self.file.write(data)
def handle_err(self, pkt):
self.log.info('Error packet: %s' % hexline(pkt['errtxt']))
@staticmethod
def is_url(path):
return urlparse.urlsplit(path)['scheme'] and True or False
class TftpServer:
"""TFTP Server
Implements a threaded TFTP Server.
Each request is handled in its own thread
"""
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.fcre, self.filepatterns = self.get_file_filters()
self.genfilecre = re.compile(r'\[(?P<name>[\w\.\-]+)\]')
def bind(self):
netconfig = self.bootpd and self.bootpd.get_netconfig()
host = self.config.get('tftp', 'address',
netconfig and netconfig['server'])
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)
self.sock.append(sock)
sock.bind((host, port))
def forever(self):
while True:
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))
def filter_file(self, connexion, mo):
# extract the position of the matching pattern, then extract the
# conversion string from the file convertion sequence
groupdict = mo.groupdict()
for group in groupdict:
filename = groupdict[group]
if not filename:
continue
filepattern = self.filepatterns[group]
return re.sub(r'\{(\w+)\}', connexion._dynreplace, filepattern)
raise TftpError('Internal error, file matching pattern issue')
def get_file_filters(self):
patterns = []
replacements = {}
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('?', '.')
pname = 'p%d' % pos
replacements[pname] = value
patterns.append('(?P<%s>%s)' % (pname, pattern))
xre = '^(?:\./)?(?:%s)$' % '|'.join(patterns)
return (re.compile(xre), replacements)

159
pybootd/util.py Normal file
View File

@ -0,0 +1,159 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2010-2011 Emmanuel Blot <emmanuel.blot@free.fr>
# 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
from ConfigParser import SafeConfigParser
import logging
import re
import socket
import struct
import sys
# String values evaluated as a true boolean values
TRUE_BOOLEANS = ['on','true','enable','enabled','yes','high','ok','1']
# String values evaluated as a false boolean values
FALSE_BOOLEANS = ['off','false','disable','disabled','no','low','ko','0']
def to_int(value):
"""Parse a string and convert it into a value"""
if not value:
return 0
if isinstance(value, int):
return value
if isinstance(value, long):
return int(value)
mo = re.match('(?i)^\s*(\d+)\s*(?:([KM])B?)?\s*$', value)
if mo:
mult = { 'k': (1<<10), 'm': (1<<20) }
value = int(mo.group(1))
value *= mo.group(2) and mult[mo.group(2).lower()] or 1
return value
return int(value.strip(), value.startswith('0x') and 16 or 10)
def to_bool(value, permissive=True):
if value is None:
return False
if isinstance(value, bool):
return value
if value.lower() in TRUE_BOOLEANS:
return True
if permissive or (value.lower() in FALSE_BOOLEANS):
return False
raise AssertionError('"Invalid boolean value: "%s"' % value)
def hexline(data):
"""Convert a binary buffer into a hexadecimal representation"""
LOGFILTER=''.join([(len(repr(chr(x)))==3) and chr(x) or \
'.' for x in range(256)])
src = ''.join(data)
hexa = ' '.join(["%02x"%ord(x) for x in src])
printable = src.translate(LOGFILTER)
return "(%d) %s : %s" % (len(data), hexa, printable)
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)
logtype = logtype.lower()
if logtype == 'file':
hdlr = logging.FileHandler(logfile)
elif logtype in ('winlog', 'eventlog', 'nteventlog'):
# Requires win32 extensions
hdlr = logging.handlers.NTEventLogHandler(logid,
logtype='Application')
elif logtype in ('syslog', 'unix'):
hdlr = logging.handlers.SysLogHandler('/dev/log')
elif logtype in ('stderr'):
hdlr = logging.StreamHandler(sys.stderr)
else:
hdlr = logging.handlers.BufferingHandler(0)
if not format:
format = 'PXEd[%(module)s] %(levelname)s: %(message)s'
if logtype in ('file', 'stderr'):
format = '%(asctime)s ' + format
datefmt = ''
if logtype == 'stderr':
datefmt = '%X'
level = level.upper()
if level in ('DEBUG', 'ALL'):
logger.setLevel(logging.DEBUG)
elif level == 'INFO':
logger.setLevel(logging.INFO)
elif level == 'ERROR':
logger.setLevel(logging.ERROR)
elif level == 'CRITICAL':
logger.setLevel(logging.CRITICAL)
else:
logger.setLevel(logging.WARNING)
formatter = logging.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
def iptoint(ipstr):
return struct.unpack('!I', socket.inet_aton(ipstr))[0]
def inttoip(ipval):
return socket.inet_ntoa(struct.pack('!I', ipval))
def get_iface_config(address):
if not address:
return None
try:
import netifaces
except ImportError:
raise AssertionError("netifaces module is not installed")
pool = iptoint(address)
for iface in netifaces.interfaces():
ifinfo = netifaces.ifaddresses(iface)
if netifaces.AF_INET not in ifinfo:
continue
for inetinfo in netifaces.ifaddresses(iface)[netifaces.AF_INET]:
addr = iptoint(inetinfo['addr'])
mask = iptoint(inetinfo['netmask'])
ip = addr & mask
ip_client = pool & mask
delta = ip ^ ip_client
if not delta:
config = { 'ifname': iface,
'server': inttoip(addr),
'net': inttoip(ip),
'mask': inttoip(mask) }
return config
return None
class EasyConfigParser(SafeConfigParser):
"ConfigParser extension to support default config values"
def get(self, section, option, default=None):
if not self.has_section(section):
return default
if not self.has_option(section, option):
return default
return SafeConfigParser.get(self, section, option)

60
setup.py Normal file
View File

@ -0,0 +1,60 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2010-2011 Emmanuel Blot <emmanuel.blot@free.fr>
# 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
from distutils.core import setup
def _read(fname):
import os
return open(os.path.join(os.path.dirname(__file__), fname)).read()
setup(
name='pybootd',
version='1.5.0',
description='Simplified BOOTP/DHCP/PXE/TFTP server',
author='Emmanuel Blot',
author_email='emmanuel.blot@free.fr',
license='LGPL v2',
keywords = 'bootp ftdp dhcp pxe netboot',
url='http://github.com/eblot/pybootd',
download_url='https://github.com/eblot/pybootd/tarball/master',
packages=['pybootd'],
requires=['netifaces (>= 0.5)'],
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: No Input/Output',
'Intended Audience :: Developers',
'Intended Audience :: System Administrators',
'License :: OSI Approved :: GNU Library or '
'Lesser General Public License (LGPL)',
'Operating System :: MacOS :: MacOS X',
'Operating System :: POSIX',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Topic :: Internet'
'Topic :: System :: Installation/Setup',
'Topic :: System :: Networking',
'Topic :: Utilities'
],
package_data = {
'': ['etc/*.ini', 'etc/*.cfg'],
},
long_description=_read('README.rst'),
)