mirror of
https://github.com/eblot/pybootd.git
synced 2024-10-05 19:49:04 +03:00
Initial commit
This commit is contained in:
commit
89405ec150
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
*.egg-info
|
||||
*.pyc
|
||||
*.pyo
|
||||
dist/
|
||||
build/
|
10
AUTHORS
Normal file
10
AUTHORS
Normal 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
17
LICENSE
Normal 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
67
README.rst
Normal 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
78
forward.sh
Executable 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
25
pybootd.py
Executable 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
61
pybootd/__init__.py
Normal 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
102
pybootd/daemons.py
Executable 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
8
pybootd/etc/pxe.cfg
Normal 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
40
pybootd/etc/pybootd.ini
Normal 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
601
pybootd/pxed.py
Normal 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
10
pybootd/tests/config.ini
Normal 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
107
pybootd/tests/httpd.py
Executable 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
445
pybootd/tftpd.py
Normal 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
159
pybootd/util.py
Normal 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
60
setup.py
Normal 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'),
|
||||
)
|
Loading…
Reference in New Issue
Block a user