mirror of
https://github.com/eblot/pybootd.git
synced 2024-09-11 14:06:57 +03:00
Merge branch 'python3'
This commit is contained in:
commit
bbf0b15edb
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,7 @@
|
|||||||
*.egg-info
|
*.egg-info
|
||||||
*.pyc
|
*.pyc
|
||||||
*.pyo
|
*.pyo
|
||||||
|
MANIFEST
|
||||||
|
**/*-local.ini
|
||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
|
137
README.rst
137
README.rst
@ -5,50 +5,56 @@ PyBootd
|
|||||||
Overview
|
Overview
|
||||||
~~~~~~~~
|
~~~~~~~~
|
||||||
|
|
||||||
PyBootd is a daemon supporting a subset of the BOOTP, DHCP, PXE and TFTP
|
PyBootd is a daemon supporting a subset of the BOOTP, DHCP, PXE, TFTP and HTTP
|
||||||
protocols, with some handy extensions.
|
protocols, with some handy extensions.
|
||||||
|
|
||||||
One of its main goals is to provide a simple solution to boot up any
|
One of its main goals is to provide a simple solution to boot up any
|
||||||
PXE-enabled personal computer, with no other tool required but a standard
|
PXE-enabled personal computer, with no other tool required but a standard
|
||||||
Python installation.
|
Python installation.
|
||||||
|
|
||||||
|
It is not designed to be feature-complete, but to be used as an easy modifiable
|
||||||
|
code to develop custom boot solutions
|
||||||
|
|
||||||
Pybootd can be used for any network boot up, or to install an OS without any
|
Pybootd can be used for any network boot up, or to install an OS without any
|
||||||
physical support such as a USB key or a CD/DVD.
|
physical support such as a USB key or a CD/DVD.
|
||||||
|
|
||||||
|
|
||||||
Requirements
|
Requirements
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
Python
|
Python
|
||||||
------
|
------
|
||||||
|
|
||||||
- Python_ 2.7 or above is required. Python_ 3.x is not yet supported.
|
- Python_ 3.5+ or above is required. Python_ 2.x is not longer supported.
|
||||||
- Six_ compatibility module
|
|
||||||
- Netifaces_ Python module is required on OS X; on Linux only, iproute2_ can be
|
- Netifaces_ Python module is required on OS X; on Linux only, iproute2_ can be
|
||||||
used as an alternative
|
used as an alternative
|
||||||
- Optional: python-pkg-resources_ Python module
|
- Optional: python_pkg_resources_ Python module
|
||||||
|
|
||||||
.. _Python: http://python.org/
|
.. _Python: http://python.org/
|
||||||
.. _Netifaces: http://alastairs-place.net/netifaces/
|
.. _Netifaces: http://alastairs-place.net/netifaces/
|
||||||
.. _iproute2: http://www.linuxfoundation.org/collaborate/workgroups/networking/iproute2
|
.. _iproute2: http://www.linuxfoundation.org/collaborate/workgroups/networking/iproute2
|
||||||
.. _python-pkg-resources: http://pythonhosted.org/distribute/pkg_resources.html
|
.. _python_pkg_resources: http://pythonhosted.org/distribute/pkg_resources.html
|
||||||
.. _Six: http://pythonhosted.org/six
|
|
||||||
|
|
||||||
Permissions
|
Permissions
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
- DHCP protocol requires the daemon to listen on port 67.
|
- DHCP protocol requires the daemon to listen on port 67.
|
||||||
- TFTP protocol requires the daemon to listen on port 69.
|
- TFTP protocol requires the daemon to listen on port 69.
|
||||||
|
- HTTP optional daemon may be run on any port.
|
||||||
|
|
||||||
As these ports are within the server's range (<1024), the superuser privileges
|
As these ports are within the server's range (<1024), the superuser privileges
|
||||||
are required on Unix hosts (Linux, Mac OS X, ...) to start up these daemons.
|
are required on Unix hosts (Linux, Mac OS X, ...) to start up these daemons.
|
||||||
|
|
||||||
|
|
||||||
Status
|
Status
|
||||||
~~~~~~
|
~~~~~~
|
||||||
|
|
||||||
This project is in beta development stage.
|
This project is in beta development stage.
|
||||||
|
|
||||||
|
|
||||||
Supported features
|
Supported features
|
||||||
~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
- Access control:
|
- Access control:
|
||||||
|
|
||||||
1. None (any remote host can be served)
|
1. None (any remote host can be served)
|
||||||
@ -72,6 +78,20 @@ Supported features
|
|||||||
- It is possible to use pybootd with only one of the services, either TFTP or
|
- It is possible to use pybootd with only one of the services, either TFTP or
|
||||||
DHCP
|
DHCP
|
||||||
|
|
||||||
|
- A very basic HTTP server can be optionally enabled to serve files over HTTP
|
||||||
|
for complex hosts that require additional files (such as a root file system)
|
||||||
|
after the initial boot sequence.
|
||||||
|
|
||||||
|
Warning
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
There is no strong checking of permissions nor robust file path management, so
|
||||||
|
it is recommended NOT to run this daemon on a host with sensitive content.
|
||||||
|
|
||||||
|
Although only read requests are implemented, there is no enforcement or
|
||||||
|
strong validation of received data and strings from adversary remote clients.
|
||||||
|
|
||||||
|
|
||||||
FAQ
|
FAQ
|
||||||
~~~
|
~~~
|
||||||
|
|
||||||
@ -81,7 +101,7 @@ Common errors
|
|||||||
``pybootd.pxed.BootpError: Unable to detect network configuration``
|
``pybootd.pxed.BootpError: Unable to detect network configuration``
|
||||||
This error is often triggered when the ``pool_start`` address is not
|
This error is often triggered when the ``pool_start`` address is not
|
||||||
part of a valid network. Double check the network configuration and
|
part of a valid network. Double check the network configuration and
|
||||||
fix up the ``[bootp]`` section so that it matches the actual
|
fix up the ``[bootpd]`` section so that it matches the actual
|
||||||
network. If you don't want to allocate addresses dynamically from
|
network. If you don't want to allocate addresses dynamically from
|
||||||
the pool (with ``pool_count = 0``), you still need to specify
|
the pool (with ``pool_count = 0``), you still need to specify
|
||||||
``pool_start`` to some address in the local network you want to
|
``pool_start`` to some address in the local network you want to
|
||||||
@ -95,19 +115,21 @@ Common errors
|
|||||||
Configuration
|
Configuration
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
``pybootd`` has a few option switches. The server offers two services: bootp
|
``pybootd`` has a few option switches. The server offers two services: *bootpd*
|
||||||
(which supports Dhcp and PXE extensions) and tftp. It is possible to disable
|
(which supports DHCP and PXE extensions) and *tftpd*. It is possible to disable
|
||||||
either services.
|
either services.
|
||||||
|
|
||||||
Usage: pybootd.py [options]
|
Usage: pybootd.py [-h] [-c CONFIG] [-p] [-t] [-d]
|
||||||
PXE boot up server, a tiny BOOTP/DHCP/TFTP server
|
Tiny BOOTP/DHCP/TFTP/PXE server
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-c CONFIG, --config=CONFIG
|
-c CONFIG, --config CONFIG
|
||||||
configuration file
|
configuration file
|
||||||
-p, --pxe enable BOOTP/DHCP/PXE server only
|
-p, --pxe only enable BOOTP/DHCP/PXE server
|
||||||
-t, --tftp enable TFTP server only
|
-t, --tftp only enable TFTP server
|
||||||
|
-H, --http enable HTTP server (default: disabled)
|
||||||
|
-d, --debug enable debug mode
|
||||||
|
|
||||||
``pybootd`` daemon uses a configuration file, in ``.ini`` format, for all other
|
``pybootd`` daemon uses a configuration file, in ``.ini`` format, for all other
|
||||||
options.
|
options.
|
||||||
@ -145,8 +167,8 @@ client requests at least an IP address twice:
|
|||||||
``file``
|
``file``
|
||||||
The path to the output log file, if ``type`` is set to ``file``.
|
The path to the output log file, if ``type`` is set to ``file``.
|
||||||
|
|
||||||
``[bootp]`` section
|
``[bootpd]`` section
|
||||||
...................
|
....................
|
||||||
|
|
||||||
``access``
|
``access``
|
||||||
Type of access control list. If this option is not defined, all BOOTP
|
Type of access control list. If this option is not defined, all BOOTP
|
||||||
@ -228,36 +250,40 @@ client requests at least an IP address twice:
|
|||||||
``servername``
|
``servername``
|
||||||
Name of the BOOTP server.
|
Name of the BOOTP server.
|
||||||
|
|
||||||
|
|
||||||
``[mac]`` section
|
``[mac]`` section
|
||||||
.................
|
.................
|
||||||
|
|
||||||
The ``[mac]`` section contains one entry for each MAC address to allow or
|
The ``[mac]`` section contains one entry for each MAC address to allow or
|
||||||
block. The value for each entry is a boolean, *i.e.*::
|
block. The value for each entry is a boolean, *i.e.*::
|
||||||
|
|
||||||
AA-BB-CC-DD-EE-FF = enable
|
AA-BB-CC-DD-EE-FF = enable
|
||||||
|
|
||||||
|
Note that due to a limitation of the configuration parser, ':' byte separator
|
||||||
|
in MAC addresses is not allowed, please use '-' separator.
|
||||||
|
|
||||||
Note that due to a limitation of the configuration parser, ':' byte separator
|
|
||||||
in MAC addresses is not allowed, please use '-' separator.
|
|
||||||
|
|
||||||
``[static_dhcp]`` section
|
``[static_dhcp]`` section
|
||||||
.........................
|
.........................
|
||||||
|
|
||||||
The ``[static_dhcp]`` section contains one entry for each MAC
|
The ``[static_dhcp]`` section contains one entry for each MAC
|
||||||
address to associate with a specific IP address. The IP address can be
|
address to associate with a specific IP address. The IP address can be
|
||||||
any IPv4 address in dotted notation, *i.e.*:
|
any IPv4 address in dotted notation, *i.e.*:
|
||||||
|
|
||||||
AA-BB-CC-DD-EE-FF = 192.168.1.2
|
AA-BB-CC-DD-EE-FF = 192.168.1.2
|
||||||
|
|
||||||
|
The MAC addresses specified here will automatically be allowed,
|
||||||
|
unless ``[mac]`` section specifies otherwise.
|
||||||
|
|
||||||
The MAC addresses specified here will automatically be allowed,
|
|
||||||
unless ``[mac]`` section specifies otherwise.
|
|
||||||
|
|
||||||
``[uuid]`` section
|
``[uuid]`` section
|
||||||
..................
|
..................
|
||||||
|
|
||||||
The ``[uuid]`` section contains one entry for each UUID to allow or block.
|
The ``[uuid]`` section contains one entry for each UUID to allow or block.
|
||||||
The value for each entry is a boolean, *i.e.*::
|
The value for each entry is a boolean, *i.e.*::
|
||||||
|
|
||||||
|
xxxxxxxx-aaaa-bbbb-cccc-yyyyyyyyyyyy = enable
|
||||||
|
|
||||||
xxxxxxxx-aaaa-bbbb-cccc-yyyyyyyyyyyy = enable
|
|
||||||
|
|
||||||
``[http]`` section
|
``[http]`` section
|
||||||
..................
|
..................
|
||||||
@ -281,8 +307,9 @@ The ``pxe``/``dhcp`` option pair enables the remote HTTP server to identify
|
|||||||
the boot phase: either a BIOS initialization or an OS boot sequence. When such
|
the boot phase: either a BIOS initialization or an OS boot sequence. When such
|
||||||
differentiation is useless, both options may refer to the same path.
|
differentiation is useless, both options may refer to the same path.
|
||||||
|
|
||||||
``[tftp]`` section
|
|
||||||
..................
|
``[tftpd]`` section
|
||||||
|
...................
|
||||||
|
|
||||||
``address``
|
``address``
|
||||||
Address to listen to incoming TFTP requests. When the BOOTP daemon is
|
Address to listen to incoming TFTP requests. When the BOOTP daemon is
|
||||||
@ -310,11 +337,34 @@ differentiation is useless, both options may refer to the same path.
|
|||||||
- an absolute path, when the ``root`` option starts with ``/``,
|
- an absolute path, when the ``root`` option starts with ``/``,
|
||||||
- a URL prefix, to access remote files.
|
- a URL prefix, to access remote files.
|
||||||
|
|
||||||
|
|
||||||
|
``[httpd]`` section
|
||||||
|
...................
|
||||||
|
|
||||||
|
``address``
|
||||||
|
Address to listen to incoming HTTP requests. When the BOOTP daemon is
|
||||||
|
enabled this option is better omitted, as the address is automatically
|
||||||
|
received from the BOOTP daemon.
|
||||||
|
|
||||||
|
``port``
|
||||||
|
Alternative port for incoming HTTP request, default to 80
|
||||||
|
|
||||||
|
``root``
|
||||||
|
Base directory for the HTTP service. This path is automatically prepended
|
||||||
|
to the pathname issued from the TFTP client. It can either point to a local
|
||||||
|
directory for now.
|
||||||
|
|
||||||
|
``check_ip``
|
||||||
|
Whether to enforce HTTP client IP or not. When enabled, requests from
|
||||||
|
clients that have not obtained an IP address from the BOOTP daemon are
|
||||||
|
rejected.
|
||||||
|
|
||||||
|
|
||||||
``[filters]`` section
|
``[filters]`` section
|
||||||
.....................
|
.....................
|
||||||
|
|
||||||
The ``filters`` section allows on-the-fly pathnames transformation. When a TFTP
|
The ``filters`` section allows on-the-fly pathnames transformation. When a TFTP
|
||||||
client requests some specific filenames, the *tftp* server can translate them
|
client requests some specific filenames, the *tftpd* server can translate them
|
||||||
to other ones.
|
to other ones.
|
||||||
|
|
||||||
This option is useful to serve the very same configuration file (''e.g.''
|
This option is useful to serve the very same configuration file (''e.g.''
|
||||||
@ -332,8 +382,8 @@ braces, such as ``{varname}``.
|
|||||||
For now, the only supported variable is ``filename``, which is replaced with
|
For now, the only supported variable is ``filename``, which is replaced with
|
||||||
the actual requested filename.
|
the actual requested filename.
|
||||||
|
|
||||||
The *value* part can also contain a special marker, that tells the *tftp*
|
The *value* part can also contain a special marker, that tells the *tftpd*
|
||||||
daemon to read the replacement pattern from a file. This special marker should
|
server to read the replacement pattern from a file. This special marker should
|
||||||
be written with enclosing brackets, such as ``[file]``.
|
be written with enclosing brackets, such as ``[file]``.
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
@ -343,7 +393,7 @@ The following filter::
|
|||||||
|
|
||||||
pxelinux.cfg/* = pybootd/etc/pxe.cfg
|
pxelinux.cfg/* = pybootd/etc/pxe.cfg
|
||||||
|
|
||||||
tells the *tftp* server that all client requests matching the
|
tells the *tftpd* server that all client requests matching the
|
||||||
``pxelinux.cfg/*`` pattern should be served the ``pybootd/etc/pxe.cfg`` file
|
``pxelinux.cfg/*`` pattern should be served the ``pybootd/etc/pxe.cfg`` file
|
||||||
instead. This prevents the client to perform the usual time-costing fallback
|
instead. This prevents the client to perform the usual time-costing fallback
|
||||||
requests using UUID, MAC, and suffix addresses before eventually falling
|
requests using UUID, MAC, and suffix addresses before eventually falling
|
||||||
@ -353,7 +403,7 @@ The following filter::
|
|||||||
|
|
||||||
startup = [dir/{filename}.cfg]
|
startup = [dir/{filename}.cfg]
|
||||||
|
|
||||||
tells the *tftp* server that when the ``startup`` file is requested, it should
|
tells the *tftpd* server that when the ``startup`` file is requested, it should
|
||||||
read out the actual filename from the ``dir/startup.cfg`` file.
|
read out the actual filename from the ``dir/startup.cfg`` file.
|
||||||
|
|
||||||
HTTP-based authentication
|
HTTP-based authentication
|
||||||
@ -382,15 +432,16 @@ this feature. It can be found within the ``tests/`` subdirectory. See the
|
|||||||
``config.ini`` file for this test daemon. The test daemon expects the ``pxe``
|
``config.ini`` file for this test daemon. The test daemon expects the ``pxe``
|
||||||
path to be set to ``/boot`` and the ``dhcp`` path to ``/linux``.
|
path to be set to ``/boot`` and the ``dhcp`` path to ``/linux``.
|
||||||
|
|
||||||
|
|
||||||
Sample configurations
|
Sample configurations
|
||||||
~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Installing a Debian 6.0 machine from the official archive
|
Installing a Debian 6.0 machine from the official archive
|
||||||
---------------------------------------------------------
|
---------------------------------------------------------
|
||||||
As the *tftp* daemon is able to retrieve remote files using the HTTP protocol,
|
As pybootd's *tftpd* server is able to retrieve remote files using the HTTP
|
||||||
there is no need to manually download any file from a Debian mirror. The daemon
|
protocol, there is no need to manually download any file from a Debian mirror.
|
||||||
will forward all file requests to the mirror on behalf of the client being
|
The daemon will forward all file requests to the mirror on behalf of the client
|
||||||
installed.
|
being installed.
|
||||||
|
|
||||||
The ``pybootd.ini`` would contain::
|
The ``pybootd.ini`` would contain::
|
||||||
|
|
||||||
@ -400,7 +451,7 @@ The ``pybootd.ini`` would contain::
|
|||||||
; show informative and error messages only (disable verbose mode)
|
; show informative and error messages only (disable verbose mode)
|
||||||
level = info
|
level = info
|
||||||
|
|
||||||
[bootp]
|
[bootpd]
|
||||||
; do not force a full PXE boot-up cycle to accept the client
|
; do not force a full PXE boot-up cycle to accept the client
|
||||||
allow_simple_dhcp = enable
|
allow_simple_dhcp = enable
|
||||||
; First BOOTP/DHCP address to generate
|
; First BOOTP/DHCP address to generate
|
||||||
@ -410,7 +461,7 @@ The ``pybootd.ini`` would contain::
|
|||||||
; boot-up executable the client should request through TFTP
|
; boot-up executable the client should request through TFTP
|
||||||
boot_file = pxelinux.0
|
boot_file = pxelinux.0
|
||||||
|
|
||||||
[tftp]
|
[tftpd]
|
||||||
; URL to install a Debian 6.0 Intel/AMD 64-bit network installation
|
; URL to install a Debian 6.0 Intel/AMD 64-bit network installation
|
||||||
root = http://http.us.debian.org/debian/dists/squeeze/main/installer-amd64/current/images/netboot
|
root = http://http.us.debian.org/debian/dists/squeeze/main/installer-amd64/current/images/netboot
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
# Copyright (c) 2010-2016 Emmanuel Blot <emmanuel.blot@free.fr>
|
# Copyright (c) 2010-2019 Emmanuel Blot <emmanuel.blot@free.fr>
|
||||||
# Copyright (c) 2010-2011 Neotion
|
# Copyright (c) 2010-2011 Neotion
|
||||||
#
|
#
|
||||||
# This library is free software; you can redistribute it and/or
|
# This library is free software; you can redistribute it and/or
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# Copyright (c) 2010-2019 Emmanuel Blot <emmanuel.blot@free.fr>
|
||||||
#
|
|
||||||
# Copyright (c) 2010-2016 Emmanuel Blot <emmanuel.blot@free.fr>
|
|
||||||
# Copyright (c) 2010-2011 Neotion
|
# Copyright (c) 2010-2011 Neotion
|
||||||
#
|
#
|
||||||
# This library is free software; you can redistribute it and/or
|
# This library is free software; you can redistribute it and/or
|
||||||
@ -18,33 +16,8 @@
|
|||||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
|
|
||||||
|
__version__ = '1.7.0'
|
||||||
def _get_package_name(default='', version='1.5.0'):
|
|
||||||
try:
|
|
||||||
from pkg_resources import WorkingSet
|
|
||||||
except ImportError:
|
|
||||||
ws = []
|
|
||||||
else:
|
|
||||||
ws = WorkingSet()
|
|
||||||
_path, _ = os.path.split(os.path.dirname(
|
|
||||||
sys.modules['pybootd'].__file__))
|
|
||||||
_path = os.path.normpath(_path)
|
|
||||||
if 'nt' not in os.name:
|
|
||||||
for dist in ws:
|
|
||||||
if os.path.samefile(os.path.normpath(dist.location), _path):
|
|
||||||
return dist.project_name, dist.version
|
|
||||||
else: # tweak for windows
|
|
||||||
_path = os.path.abspath(_path).lower()
|
|
||||||
for dist in ws:
|
|
||||||
if 'pybootd' in dist.location:
|
|
||||||
if _path == os.path.abspath(dist.location).lower():
|
|
||||||
return dist.project_name, dist.version
|
|
||||||
return default, version
|
|
||||||
|
|
||||||
|
|
||||||
PRODUCT_NAME, __version__ = _get_package_name('pybootd')
|
|
||||||
|
|
||||||
|
|
||||||
def pybootd_path(path):
|
def pybootd_path(path):
|
||||||
@ -61,9 +34,9 @@ def pybootd_path(path):
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
raise IOError('pkg_resources module not available')
|
raise IOError('pkg_resources module not available')
|
||||||
try:
|
try:
|
||||||
newpath = resource_filename(Requirement.parse(PRODUCT_NAME), path)
|
newpath = resource_filename(Requirement.parse('pybootd'), path)
|
||||||
if not newpath:
|
if not newpath:
|
||||||
localpath = get_distribution(PRODUCT_NAME).location
|
localpath = get_distribution('pybootd').location
|
||||||
newpath = os.path.join(localpath, path)
|
newpath = os.path.join(localpath, path)
|
||||||
except DistributionNotFound:
|
except DistributionNotFound:
|
||||||
newpath = path
|
newpath = path
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (c) 2010-2016 Emmanuel Blot <emmanuel.blot@free.fr>
|
# Copyright (c) 2010-2019 Emmanuel Blot <emmanuel.blot@free.fr>
|
||||||
# Copyright (c) 2010-2011 Neotion
|
# Copyright (c) 2010-2011 Neotion
|
||||||
#
|
#
|
||||||
# This library is free software; you can redistribute it and/or
|
# This library is free software; you can redistribute it and/or
|
||||||
@ -18,69 +17,98 @@
|
|||||||
# License along with this library; if not, write to the Free Software
|
# License along with this library; if not, write to the Free Software
|
||||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
|
||||||
"""Boot up server, a tiny BOOTP/DHCP/TFTP/PXE server"""
|
"""Tiny BOOTP/DHCP/TFTP/PXE server"""
|
||||||
|
|
||||||
|
from argparse import ArgumentParser
|
||||||
import os
|
from collections import OrderedDict
|
||||||
import sys
|
from os.path import isfile
|
||||||
from pxed import BootpServer
|
|
||||||
from pybootd import pybootd_path, PRODUCT_NAME, __version__ as VERSION
|
|
||||||
from six import print_
|
|
||||||
from tftpd import TftpServer
|
|
||||||
from util import logger_factory, EasyConfigParser
|
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
from sys import exit as sysexit, modules, stderr
|
||||||
|
from traceback import format_exc
|
||||||
|
|
||||||
|
from . import pybootd_path, __version__
|
||||||
|
from .httpd import HttpServer
|
||||||
|
from .pxed import BootpServer
|
||||||
|
from .tftpd import TftpServer
|
||||||
|
from .util import logger_factory, EasyConfigParser
|
||||||
|
|
||||||
|
#pylint: disable-msg=broad-except
|
||||||
|
#pylint: disable-msg=missing-docstring
|
||||||
|
#pylint: disable-msg=invalid-name
|
||||||
|
|
||||||
|
|
||||||
class BootpDaemon(Thread):
|
class Daemon(Thread):
|
||||||
|
|
||||||
def __init__(self, logger, config):
|
def __init__(self, debug):
|
||||||
super(BootpDaemon, self).__init__(name="BootpDeamon")
|
super(Daemon, self).__init__(name=self.__class__.__name__, daemon=True)
|
||||||
self.daemon = True
|
self._server = None
|
||||||
|
self._debug = debug
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
self._server.start()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
print('\nError: %s' % exc, stderr)
|
||||||
|
if self._debug:
|
||||||
|
print(format_exc(chain=False), file=stderr)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._server.stop()
|
||||||
|
|
||||||
|
|
||||||
|
class BootpDaemon(Daemon):
|
||||||
|
|
||||||
|
def __init__(self, logger, config, debug):
|
||||||
|
super(BootpDaemon, self).__init__(debug)
|
||||||
self._server = BootpServer(logger=logger, config=config)
|
self._server = BootpServer(logger=logger, config=config)
|
||||||
|
|
||||||
def get_netconfig(self):
|
def get_netconfig(self):
|
||||||
return self._server.get_netconfig()
|
return self._server.get_netconfig()
|
||||||
|
|
||||||
|
def is_managed_ip(self, ip):
|
||||||
|
return self._server.is_managed_ip(ip)
|
||||||
|
|
||||||
def get_filename(self, ip):
|
def get_filename(self, ip):
|
||||||
return self._server.get_filename(ip)
|
return self._server.get_filename(ip)
|
||||||
|
|
||||||
def run(self):
|
|
||||||
self._server.bind()
|
|
||||||
self._server.forever()
|
|
||||||
|
|
||||||
|
class TftpDaemon(Daemon):
|
||||||
|
|
||||||
class TftpDaemon(Thread):
|
def __init__(self, logger, config, debug, bootpd=None):
|
||||||
|
super(TftpDaemon, self).__init__(debug)
|
||||||
def __init__(self, logger, config, bootpd=None):
|
|
||||||
super(TftpDaemon, self).__init__(name="TftpDeamon")
|
|
||||||
self.daemon = True
|
|
||||||
self._server = TftpServer(logger=logger, config=config, bootpd=bootpd)
|
self._server = TftpServer(logger=logger, config=config, bootpd=bootpd)
|
||||||
|
|
||||||
def run(self):
|
|
||||||
self._server.bind()
|
class HttpDaemon(Daemon):
|
||||||
self._server.forever()
|
|
||||||
|
def __init__(self, logger, config, debug, bootpd=None):
|
||||||
|
super(HttpDaemon, self).__init__(debug)
|
||||||
|
self.daemon = True
|
||||||
|
self._server = HttpServer(logger=logger, config=config, bootpd=bootpd)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
debug = False
|
debug = False
|
||||||
try:
|
try:
|
||||||
from argparse import ArgumentParser
|
argparser = ArgumentParser(description=modules[__name__].__doc__)
|
||||||
argparser = ArgumentParser(description=sys.modules[__name__].__doc__)
|
argparser.add_argument('-c', '--config',
|
||||||
argparser.add_argument('-c', '--config', dest='config',
|
|
||||||
default='pybootd/etc/pybootd.ini',
|
default='pybootd/etc/pybootd.ini',
|
||||||
help='configuration file')
|
help='configuration file')
|
||||||
argparser.add_argument('-p', '--pxe', dest='pxe',
|
argparser.add_argument('-p', '--pxe', action='store_true',
|
||||||
action='store_true',
|
help='only enable BOOTP/DHCP/PXE server')
|
||||||
help='enable BOOTP/DHCP/PXE server only')
|
argparser.add_argument('-t', '--tftp', action='store_true',
|
||||||
argparser.add_argument('-t', '--tftp', dest='tftp',
|
help='only enable TFTP server')
|
||||||
action='store_true',
|
argparser.add_argument('-H', '--http', action='store_true',
|
||||||
help='enable TFTP server only')
|
help='enable HTTP server (default: disabled)')
|
||||||
argparser.add_argument('-d', '--debug', action='store_true',
|
argparser.add_argument('-d', '--debug', action='store_true',
|
||||||
help='enable debug mode')
|
help='enable debug mode')
|
||||||
args = argparser.parse_args()
|
args = argparser.parse_args()
|
||||||
debug = args.debug
|
debug = args.debug
|
||||||
|
|
||||||
if not os.path.isfile(args.config):
|
if not isfile(args.config):
|
||||||
argparser.error('Invalid configuration file')
|
argparser.error('Invalid configuration file')
|
||||||
|
|
||||||
if args.pxe and args.tftp:
|
if args.pxe and args.tftp:
|
||||||
@ -88,32 +116,47 @@ def main():
|
|||||||
|
|
||||||
cfgparser = EasyConfigParser()
|
cfgparser = EasyConfigParser()
|
||||||
with open(pybootd_path(args.config), 'rt') as config:
|
with open(pybootd_path(args.config), 'rt') as config:
|
||||||
cfgparser.readfp(config)
|
cfgparser.read_file(config)
|
||||||
|
|
||||||
logger = logger_factory(logtype=cfgparser.get('logger', 'type',
|
logger = logger_factory(logtype=cfgparser.get('logger', 'type',
|
||||||
'stderr'),
|
'stderr'),
|
||||||
logfile=cfgparser.get('logger', 'file'),
|
logfile=cfgparser.get('logger', 'file'),
|
||||||
level=cfgparser.get('logger', 'level',
|
level=cfgparser.get('logger', 'level',
|
||||||
'info'))
|
'info'))
|
||||||
logger.info('-'.join((PRODUCT_NAME, VERSION)))
|
logger.info('-'.join(('pybootd', __version__)))
|
||||||
|
|
||||||
daemon = None
|
daemons = OrderedDict()
|
||||||
if not args.tftp:
|
if not args.tftp:
|
||||||
daemon = BootpDaemon(logger, cfgparser)
|
daemon = BootpDaemon(logger, cfgparser, debug)
|
||||||
daemon.start()
|
daemon.start()
|
||||||
|
daemons['bootp'] = daemon
|
||||||
if not args.pxe:
|
if not args.pxe:
|
||||||
daemon = TftpDaemon(logger, cfgparser, daemon)
|
daemon = TftpDaemon(logger, cfgparser, debug,
|
||||||
|
daemons.get('bootp', None))
|
||||||
daemon.start()
|
daemon.start()
|
||||||
if daemon:
|
daemons['tftp'] = daemon
|
||||||
while True:
|
if args.http:
|
||||||
daemon.join(0.5)
|
daemon = HttpDaemon(logger, cfgparser, debug, daemons.get('bootp'))
|
||||||
|
daemon.start()
|
||||||
|
daemons['http'] = daemon
|
||||||
|
resume = True
|
||||||
|
while daemons:
|
||||||
|
zombies = set()
|
||||||
|
for name, daemon in daemons.items():
|
||||||
|
if not resume:
|
||||||
|
daemon.stop()
|
||||||
|
daemon.join(0.1)
|
||||||
if not daemon.is_alive():
|
if not daemon.is_alive():
|
||||||
break
|
logger.warn('%s daemon terminated', name)
|
||||||
except Exception as e:
|
zombies.add(name)
|
||||||
print_('\nError: %s' % e, file=sys.stderr)
|
resume = False
|
||||||
|
for name in zombies:
|
||||||
|
del daemons[name]
|
||||||
|
except Exception as exc:
|
||||||
|
print('\nError: %s' % exc, file=stderr)
|
||||||
if debug:
|
if debug:
|
||||||
import traceback
|
print(format_exc(chain=False), file=stderr)
|
||||||
print_(traceback.format_exc(), file=sys.stderr)
|
sysexit(1)
|
||||||
sys.exit(1)
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print_("Aborting...")
|
print("\nAborting...", file=stderr)
|
||||||
|
sysexit(2)
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
type = stderr
|
type = stderr
|
||||||
level = info
|
level = info
|
||||||
|
|
||||||
[bootp]
|
[bootpd]
|
||||||
address = 0.0.0.0
|
address = 0.0.0.0
|
||||||
; pool_start should be in a valid subnet
|
; pool_start should be in a valid subnet
|
||||||
pool_start = 192.168.25.100
|
pool_start = 192.168.25.100
|
||||||
@ -13,14 +13,14 @@ boot_file = pxelinux.0
|
|||||||
lease_time = 86400
|
lease_time = 86400
|
||||||
access = mac
|
access = mac
|
||||||
allow_simple_dhcp = enable
|
allow_simple_dhcp = enable
|
||||||
dns = 10.130.0.2
|
dns = 8.8.8.8
|
||||||
boot_file = pxelinux.0
|
|
||||||
set_gateway = true
|
set_gateway = true
|
||||||
; use "nc -l -u 127.0.0.1 -p 12345" to debug
|
; use "nc -l -u 127.0.0.1 -p 12345" to debug
|
||||||
; notify = 192.168.26.201:12345;192.168.26.200:12345
|
; notify = 192.168.26.201:12345;192.168.26.200:12345
|
||||||
|
|
||||||
[mac]
|
[mac]
|
||||||
00-1E-4F-C4-95-EE = enable
|
; see doc: byte separator should be defined with dash, not column
|
||||||
|
00-AA-55-12-34-56 = enable
|
||||||
|
|
||||||
[uuid]
|
[uuid]
|
||||||
12345678-abcd-ef00-1111-abcdefabcdef = enable
|
12345678-abcd-ef00-1111-abcdefabcdef = enable
|
||||||
@ -32,10 +32,16 @@ pxe = boot
|
|||||||
dhcp = linux
|
dhcp = linux
|
||||||
always_check = disable
|
always_check = disable
|
||||||
|
|
||||||
[tftp]
|
[tftpd]
|
||||||
;address = (use address from bootpd)
|
;address = (use address from bootpd)
|
||||||
;root = ./images
|
root = ./images
|
||||||
root = http://http.us.debian.org/debian/dists/squeeze/main/installer-amd64/current/images/netboot
|
;root = http://http.us.debian.org/debian/dists/squeeze/main/installer-amd64/current/images/netboot
|
||||||
|
|
||||||
|
[httpd]
|
||||||
|
; address = (use address from bootpd)
|
||||||
|
port = 8000
|
||||||
|
check_ip = yes
|
||||||
|
root = /tmp
|
||||||
|
|
||||||
[filters]
|
[filters]
|
||||||
;pxelinux.cfg/* = pybootd/etc/pxe.cfg
|
;pxelinux.cfg/* = pybootd/etc/pxe.cfg
|
||||||
|
99
pybootd/httpd.py
Executable file
99
pybootd/httpd.py
Executable file
@ -0,0 +1,99 @@
|
|||||||
|
# Copyright (c) 2010-2019 Emmanuel Blot <emmanuel.blot@free.fr>
|
||||||
|
# Copyright (c) 2010-2011 Neotion
|
||||||
|
#
|
||||||
|
# This library is free software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU Lesser General Public
|
||||||
|
# License as published by the Free Software Foundation; either
|
||||||
|
# version 2 of the License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
# Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public
|
||||||
|
# License along with this library; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
|
||||||
|
"""HTTPd tiny server to exercise the pybootd daemon"""
|
||||||
|
|
||||||
|
from argparse import ArgumentParser, FileType
|
||||||
|
from functools import partial
|
||||||
|
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
from os import getcwd
|
||||||
|
from os.path import isfile, join as joinpath, realpath
|
||||||
|
from sys import exit as sysexit, modules, stderr
|
||||||
|
from traceback import format_exc
|
||||||
|
from urllib.parse import parse_qs, urlsplit
|
||||||
|
from .util import logger_factory, to_bool, to_int, EasyConfigParser
|
||||||
|
|
||||||
|
|
||||||
|
#pylint: disable-msg=broad-except
|
||||||
|
#pylint: disable-msg=missing-docstring
|
||||||
|
#pylint: disable-msg=invalid-name
|
||||||
|
|
||||||
|
|
||||||
|
class HttpRequestHandler(SimpleHTTPRequestHandler):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(HttpRequestHandler, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def do_HEAD(self):
|
||||||
|
if self._validate():
|
||||||
|
super(HttpRequestHandler, self).do_HEAD()
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
if self._validate():
|
||||||
|
super(HttpRequestHandler, self).do_GET()
|
||||||
|
|
||||||
|
def log_request(self, code='-', size='-'):
|
||||||
|
self.server.log.info('Request: %s %s', code, size)
|
||||||
|
|
||||||
|
def log_error(self, fmt, *args):
|
||||||
|
self.server.log.error(fmt, *args)
|
||||||
|
|
||||||
|
def _validate(self):
|
||||||
|
log = self.server.log
|
||||||
|
bootpd = self.server.bootpd
|
||||||
|
if bootpd and self.server.check_ip:
|
||||||
|
ipaddr = self.client_address[0]
|
||||||
|
if not bootpd.is_managed_ip(ipaddr):
|
||||||
|
log.info('Unknown IP: %s', ipaddr)
|
||||||
|
self.send_error(401, 'Not authorized')
|
||||||
|
return False
|
||||||
|
log.debug("Request: %s" % self.path)
|
||||||
|
path = realpath(joinpath(self.directory, self.path.lstrip('/')))
|
||||||
|
if not path.startswith(self.directory):
|
||||||
|
log.info('Malformed path: %s', path)
|
||||||
|
self.send_error(403, 'Forbidden')
|
||||||
|
return False
|
||||||
|
if not isfile(path):
|
||||||
|
log.info('Invalid path: %s', path)
|
||||||
|
self.send_error(404, 'Not found')
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
class HttpServer(ThreadingHTTPServer):
|
||||||
|
|
||||||
|
HTTP_SECTION = 'httpd'
|
||||||
|
|
||||||
|
def __init__(self, logger, config, bootpd=None):
|
||||||
|
self.log = logger
|
||||||
|
self.config = config
|
||||||
|
self.bootpd = bootpd
|
||||||
|
netconfig = bootpd and bootpd.get_netconfig()
|
||||||
|
address = (self.config.get(self.HTTP_SECTION, 'address',
|
||||||
|
netconfig and netconfig['server']),
|
||||||
|
int(self.config.get(self.HTTP_SECTION, 'port', '80')))
|
||||||
|
root = realpath(self.config.get(self.HTTP_SECTION, 'root', None))
|
||||||
|
self.check_ip = to_bool(self.config.get(self.HTTP_SECTION, 'check_ip',
|
||||||
|
'yes'))
|
||||||
|
self.log.info('Listening to %s:%s' % address)
|
||||||
|
handler = partial(HttpRequestHandler, directory=root)
|
||||||
|
super(HttpServer, self).__init__(address, handler)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self.serve_forever()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.shutdown()
|
276
pybootd/pxed.py
276
pybootd/pxed.py
@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# Copyright (c) 2010-2019 Emmanuel Blot <emmanuel.blot@free.fr>
|
||||||
#
|
|
||||||
# Copyright (c) 2010-2016 Emmanuel Blot <emmanuel.blot@free.fr>
|
|
||||||
# Copyright (c) 2010-2011 Neotion
|
# Copyright (c) 2010-2011 Neotion
|
||||||
#
|
#
|
||||||
# This library is free software; you can redistribute it and/or
|
# This library is free software; you can redistribute it and/or
|
||||||
@ -17,27 +15,40 @@
|
|||||||
# License along with this library; if not, write to the Free Software
|
# License along with this library; if not, write to the Free Software
|
||||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
# 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 binascii import hexlify
|
||||||
from pybootd import PRODUCT_NAME
|
from re import compile as recompile
|
||||||
|
from select import select
|
||||||
|
from socket import (inet_aton, inet_ntoa, socket,
|
||||||
|
AF_INET, SOCK_DGRAM, IPPROTO_UDP, SOL_SOCKET,
|
||||||
|
SO_BROADCAST, SO_REUSEADDR)
|
||||||
|
from struct import calcsize as scalc, pack as spack, unpack as sunpack
|
||||||
|
from time import sleep
|
||||||
|
from traceback import format_exc
|
||||||
|
from urllib.error import HTTPError, URLError
|
||||||
|
from urllib.parse import urlencode, urlunsplit
|
||||||
|
from urllib.request import urlopen
|
||||||
from .util import hexline, to_bool, iptoint, inttoip, get_iface_config
|
from .util import hexline, to_bool, iptoint, inttoip, get_iface_config
|
||||||
|
|
||||||
|
#pylint: disable-msg=broad-except
|
||||||
|
#pylint: disable-msg=invalid-name
|
||||||
|
#pylint: disable-msg=missing-docstring
|
||||||
|
#pylint: disable-msg=too-many-return-statements
|
||||||
|
#pylint: disable-msg=too-many-branches
|
||||||
|
#pylint: disable-msg=too-many-locals
|
||||||
|
#pylint: disable-msg=too-many-statements
|
||||||
|
#pylint: disable-msg=too-many-nested-blocks
|
||||||
|
|
||||||
|
|
||||||
BOOTP_PORT_REQUEST = 67
|
BOOTP_PORT_REQUEST = 67
|
||||||
BOOTP_PORT_REPLY = 68
|
BOOTP_PORT_REPLY = 68
|
||||||
|
|
||||||
BOOTREQUEST = 1
|
BOOTREQUEST = 1
|
||||||
BOOTREPLY = 2
|
BOOTREPLY = 2
|
||||||
|
|
||||||
BOOTPFormat = '!4bIHH4s4s4s4s16s64s128s64s'
|
BOOTPFORMAT = '!4bIHH4s4s4s4s16s64s128s64s'
|
||||||
BOOTPFormatSize = struct.calcsize(BOOTPFormat)
|
BOOTPFORMATSIZE = scalc(BOOTPFORMAT)
|
||||||
DHCPFormat = '!4bIHH4s4s4s4s16s64s128s4s'
|
DHCPFORMAT = '!4bIHH4s4s4s4s16s64s128s4s'
|
||||||
DHCPFormatSize = struct.calcsize(DHCPFormat)
|
DHCPFORMATSIZE = scalc(DHCPFORMAT)
|
||||||
|
|
||||||
(BOOTP_OP, BOOTP_HTYPE, BOOTP_HLEN, BOOTP_HOPS, BOOTP_XID, BOOTP_SECS,
|
(BOOTP_OP, BOOTP_HTYPE, BOOTP_HLEN, BOOTP_HOPS, BOOTP_XID, BOOTP_SECS,
|
||||||
BOOTP_FLAGS, BOOTP_CIADDR, BOOTP_YIADDR, BOOTP_SIADDR, BOOTP_GIADDR,
|
BOOTP_FLAGS, BOOTP_CIADDR, BOOTP_YIADDR, BOOTP_SIADDR, BOOTP_GIADDR,
|
||||||
@ -46,7 +57,7 @@ DHCPFormatSize = struct.calcsize(DHCPFormat)
|
|||||||
BOOTP_FLAGS_NONE = 0
|
BOOTP_FLAGS_NONE = 0
|
||||||
BOOTP_FLAGS_BROADCAST = 1<<15
|
BOOTP_FLAGS_BROADCAST = 1<<15
|
||||||
|
|
||||||
COOKIE='\0x63\0x82\0x53\0x63'
|
COOKIE = r'\0x63\0x82\0x53\0x63'
|
||||||
|
|
||||||
DHCP_OPTIONS = {0: 'Byte padding',
|
DHCP_OPTIONS = {0: 'Byte padding',
|
||||||
1: 'Subnet mask',
|
1: 'Subnet mask',
|
||||||
@ -169,6 +180,8 @@ class BootpServer:
|
|||||||
ACCESS_REMOTE = ['http'] # Access modes, remotely retrieved
|
ACCESS_REMOTE = ['http'] # Access modes, remotely retrieved
|
||||||
(ST_IDLE, ST_PXE, ST_DHCP) = range(3) # Current state
|
(ST_IDLE, ST_PXE, ST_DHCP) = range(3) # Current state
|
||||||
|
|
||||||
|
BOOTP_SECTION = 'bootpd'
|
||||||
|
|
||||||
def __init__(self, logger, config):
|
def __init__(self, logger, config):
|
||||||
self.sock = []
|
self.sock = []
|
||||||
self.log = logger
|
self.log = logger
|
||||||
@ -177,26 +190,24 @@ class BootpServer:
|
|||||||
self.ippool = {} # key MAC address string, value assigned IP string
|
self.ippool = {} # key MAC address string, value assigned IP string
|
||||||
self.filepool = {} # key IP string, value pathname
|
self.filepool = {} # key IP string, value pathname
|
||||||
self.states = {} # key MAC address string, value client state
|
self.states = {} # key MAC address string, value client state
|
||||||
name_ = PRODUCT_NAME.split('-')
|
self.pool_start = self.config.get(self.BOOTP_SECTION, 'pool_start')
|
||||||
name_[0] = 'bootp'
|
|
||||||
self.bootp_section = '_'.join(name_)
|
|
||||||
self.pool_start = self.config.get(self.bootp_section, 'pool_start')
|
|
||||||
if not self.pool_start:
|
if not self.pool_start:
|
||||||
raise BootpError('Missing pool_start definition')
|
raise BootpError('Missing pool_start definition')
|
||||||
self.pool_count = int(self.config.get(self.bootp_section,
|
self.pool_count = int(self.config.get(self.BOOTP_SECTION,
|
||||||
'pool_count', '10'))
|
'pool_count', '10'))
|
||||||
|
|
||||||
self.netconfig = get_iface_config(self.pool_start)
|
self.netconfig = get_iface_config(self.pool_start)
|
||||||
if not self.netconfig:
|
if not self.netconfig:
|
||||||
host = self.config.get(self.bootp_section, 'address', '0.0.0.0')
|
host = self.config.get(self.BOOTP_SECTION, 'address', '0.0.0.0')
|
||||||
self.netconfig = get_iface_config(host)
|
self.netconfig = get_iface_config(host)
|
||||||
if not self.netconfig:
|
if not self.netconfig:
|
||||||
raise BootpError('Unable to detect network configuration')
|
# the available networks on the host may not match the config...
|
||||||
|
raise BootpError('Unable to detect a matching network config')
|
||||||
|
|
||||||
keys = sorted(self.netconfig.keys())
|
keys = sorted(self.netconfig.keys())
|
||||||
self.log.info('Using %s' % ', '.join(map(
|
self.log.info('Using %s' % ', '.join(map(
|
||||||
':'.join, zip(keys, [self.netconfig[k] for k in keys]))))
|
':'.join, zip(keys, [self.netconfig[k] for k in keys]))))
|
||||||
nlist = self.config.get(self.bootp_section, 'notify')
|
nlist = self.config.get(self.BOOTP_SECTION, 'notify')
|
||||||
self.notify = []
|
self.notify = []
|
||||||
if nlist:
|
if nlist:
|
||||||
try:
|
try:
|
||||||
@ -204,9 +215,9 @@ class BootpServer:
|
|||||||
for n in nlist:
|
for n in nlist:
|
||||||
n = n.strip().split(':')
|
n = n.strip().split(':')
|
||||||
self.notify.append((n[0], int(n[1])))
|
self.notify.append((n[0], int(n[1])))
|
||||||
except Exception, e:
|
except Exception as exc:
|
||||||
raise BootpError('Invalid notification URL: %s' % str(e))
|
raise BootpError('Invalid notification URL: %s' % exc)
|
||||||
access = self.config.get(self.bootp_section, 'access')
|
access = self.config.get(self.BOOTP_SECTION, 'access')
|
||||||
if not access:
|
if not access:
|
||||||
self.acl = None
|
self.acl = None
|
||||||
else:
|
else:
|
||||||
@ -235,7 +246,7 @@ class BootpServer:
|
|||||||
msg = ','.join([notice, uuid_str, mac_str, ip])
|
msg = ','.join([notice, uuid_str, mac_str, ip])
|
||||||
else:
|
else:
|
||||||
msg = ','.join([notice, mac_str, ip])
|
msg = ','.join([notice, mac_str, ip])
|
||||||
notify_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
notify_sock = socket(AF_INET, SOCK_DGRAM)
|
||||||
for n in self.notify:
|
for n in self.notify:
|
||||||
self.log.info('Notifying %s with %s' % (n, msg))
|
self.log.info('Notifying %s with %s' % (n, msg))
|
||||||
notify_sock.sendto(msg, n)
|
notify_sock.sendto(msg, n)
|
||||||
@ -244,42 +255,52 @@ class BootpServer:
|
|||||||
def get_netconfig(self):
|
def get_netconfig(self):
|
||||||
return self.netconfig
|
return self.netconfig
|
||||||
|
|
||||||
def bind(self):
|
def is_managed_ip(self, address):
|
||||||
host = self.config.get(self.bootp_section, 'address', '0.0.0.0')
|
return address in self.ippool.values()
|
||||||
port = self.config.get(self.bootp_section, 'port',
|
|
||||||
|
def start(self):
|
||||||
|
host = self.config.get(self.BOOTP_SECTION, 'address', '0.0.0.0')
|
||||||
|
port = self.config.get(self.BOOTP_SECTION, 'port',
|
||||||
str(BOOTP_PORT_REQUEST))
|
str(BOOTP_PORT_REQUEST))
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM,
|
sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
|
||||||
socket.IPPROTO_UDP)
|
sock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)
|
||||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
|
||||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
||||||
self.sock.append(sock)
|
self.sock.append(sock)
|
||||||
self.log.info('Listening to %s:%s' % (host, port))
|
self.log.info('Listening to %s:%s' % (host, port))
|
||||||
sock.bind((host, int(port)))
|
sock.bind((host, int(port)))
|
||||||
|
self._resume = True
|
||||||
def forever(self):
|
while self._resume:
|
||||||
while True:
|
|
||||||
try:
|
try:
|
||||||
r, w, e = select.select(self.sock, [], self.sock)
|
r = select(self.sock, [], self.sock, 0.25)[0]
|
||||||
|
if not r:
|
||||||
|
continue
|
||||||
for sock in r:
|
for sock in r:
|
||||||
data, addr = sock.recvfrom(556)
|
data, addr = sock.recvfrom(556)
|
||||||
self.handle(sock, addr, data)
|
self.handle(sock, addr, data)
|
||||||
except Exception, e:
|
except Exception as exc:
|
||||||
import traceback
|
self.log.critical('%s\n%s' % (exc, format_exc()))
|
||||||
self.log.critical('%s\n%s' % (str(e), traceback.format_exc()))
|
sleep(1)
|
||||||
time.sleep(1)
|
|
||||||
|
def stop(self):
|
||||||
|
self._resume = False
|
||||||
|
|
||||||
def parse_options(self, tail):
|
def parse_options(self, tail):
|
||||||
self.log.debug('Parsing DHCP options')
|
self.log.debug('Parsing DHCP options')
|
||||||
dhcp_tags = {}
|
dhcp_tags = {}
|
||||||
|
padding_count = 0
|
||||||
while tail:
|
while tail:
|
||||||
tag = ord(tail[0])
|
tag = tail[0]
|
||||||
# padding
|
# padding
|
||||||
if tag == 0:
|
if tag == 0:
|
||||||
|
padding_count += 1
|
||||||
|
if padding_count > 255:
|
||||||
|
raise ValueError('Padding overflow')
|
||||||
continue
|
continue
|
||||||
|
padding_count = 0
|
||||||
if tag == 0xff:
|
if tag == 0xff:
|
||||||
return dhcp_tags
|
return dhcp_tags
|
||||||
length = ord(tail[1])
|
length = tail[1]
|
||||||
(value, ) = struct.unpack('!%ss' % length, tail[2:2+length])
|
(value, ) = sunpack('!%ss' % length, tail[2:2+length])
|
||||||
tail = tail[2+length:]
|
tail = tail[2+length:]
|
||||||
try:
|
try:
|
||||||
option = DHCP_OPTIONS[tag]
|
option = DHCP_OPTIONS[tag]
|
||||||
@ -292,47 +313,45 @@ class BootpServer:
|
|||||||
dhcp_tags[tag] = value
|
dhcp_tags[tag] = value
|
||||||
|
|
||||||
def build_pxe_options(self, options, server):
|
def build_pxe_options(self, options, server):
|
||||||
|
buf = b''
|
||||||
try:
|
try:
|
||||||
buf = ''
|
|
||||||
uuid = options[97]
|
uuid = options[97]
|
||||||
buf += struct.pack('!BB%ds' % len(uuid),
|
buf += spack('!BB%ds' % len(uuid),
|
||||||
97, len(uuid), uuid)
|
97, len(uuid), uuid)
|
||||||
clientclass = options[60]
|
clientclass = options[60]
|
||||||
clientclass = clientclass[:clientclass.find(':')]
|
clientclass = clientclass[:clientclass.find(':')]
|
||||||
buf += struct.pack('!BB%ds' % len(clientclass),
|
buf += spack('!BB%ds' % len(clientclass),
|
||||||
60, len(clientclass), clientclass)
|
60, len(clientclass), clientclass)
|
||||||
vendor = ''
|
vendor = ''
|
||||||
vendor += struct.pack('!BBB', PXE_DISCOVERY_CONTROL, 1, 0x0A)
|
vendor += spack('!BBB', PXE_DISCOVERY_CONTROL, 1, 0x0A)
|
||||||
vendor += struct.pack('!BBHB4s', PXE_BOOT_SERVERS, 2+1+4,
|
vendor += spack('!BBHB4s', PXE_BOOT_SERVERS, 2+1+4,
|
||||||
0, 1, server)
|
0, 1, server)
|
||||||
srvstr = 'Python'
|
srvstr = 'Python'
|
||||||
vendor += struct.pack('!BBHB%ds' % len(srvstr), PXE_BOOT_MENU,
|
vendor += spack('!BBHB%ds' % len(srvstr), PXE_BOOT_MENU,
|
||||||
2+1+len(srvstr), 0, len(srvstr), srvstr)
|
2+1+len(srvstr), 0, len(srvstr), srvstr)
|
||||||
prompt = 'Stupid PXE'
|
prompt = 'Stupid PXE'
|
||||||
vendor += struct.pack('!BBB%ds' % len(prompt), PXE_MENU_PROMPT,
|
vendor += spack('!BBB%ds' % len(prompt), PXE_MENU_PROMPT,
|
||||||
1+len(prompt), len(prompt), prompt)
|
1+len(prompt), len(prompt), prompt)
|
||||||
buf += struct.pack('!BB%ds' % len(vendor), 43,
|
buf += spack('!BB%ds' % len(vendor), 43,
|
||||||
len(vendor), vendor)
|
len(vendor), vendor)
|
||||||
buf += struct.pack('!BBB', 255, 0, 0)
|
buf += spack('!BBB', 255, 0, 0)
|
||||||
return buf
|
return buf
|
||||||
except KeyError, e:
|
except KeyError as exc:
|
||||||
self.log.error('Missing options, cancelling: ' + str(e))
|
self.log.error('Missing options, cancelling: %s' % exc)
|
||||||
return None
|
return b''
|
||||||
|
|
||||||
def build_dhcp_options(self, clientname):
|
def build_dhcp_options(self, clientname):
|
||||||
buf = ''
|
|
||||||
if not clientname:
|
if not clientname:
|
||||||
return buf
|
return b''
|
||||||
buf += struct.pack('!BB%ds' % len(clientname),
|
return spack('!BB%ds' % len(clientname),
|
||||||
12, len(clientname), clientname)
|
12, len(clientname), clientname)
|
||||||
return buf
|
|
||||||
|
|
||||||
def handle(self, sock, addr, data):
|
def handle(self, sock, addr, data):
|
||||||
self.log.info('Sender: %s on socket %s' % (addr, sock.getsockname()))
|
self.log.info('Sender: %s on socket %s' % (addr, sock.getsockname()))
|
||||||
if len(data) < DHCPFormatSize:
|
if len(data) < DHCPFORMATSIZE:
|
||||||
self.log.error('Cannot be a DHCP or BOOTP request - too small!')
|
self.log.error('Cannot be a DHCP or BOOTP request - too small!')
|
||||||
tail = data[DHCPFormatSize:]
|
tail = data[DHCPFORMATSIZE:]
|
||||||
buf = list(struct.unpack(DHCPFormat, data[:DHCPFormatSize]))
|
buf = list(sunpack(DHCPFORMAT, data[:DHCPFORMATSIZE]))
|
||||||
if buf[BOOTP_OP] != BOOTREQUEST:
|
if buf[BOOTP_OP] != BOOTREQUEST:
|
||||||
self.log.warn('Not a BOOTREQUEST')
|
self.log.warn('Not a BOOTREQUEST')
|
||||||
return
|
return
|
||||||
@ -343,13 +362,13 @@ class BootpServer:
|
|||||||
|
|
||||||
# Extras (DHCP options)
|
# Extras (DHCP options)
|
||||||
try:
|
try:
|
||||||
dhcp_msg_type = ord(options[53][0])
|
dhcp_msg_type = options[53][0]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
dhcp_msg_type = None
|
dhcp_msg_type = None
|
||||||
|
|
||||||
server_addr = self.netconfig['server']
|
server_addr = self.netconfig['server']
|
||||||
mac_addr = buf[BOOTP_CHADDR][:6]
|
mac_addr = buf[BOOTP_CHADDR][:6]
|
||||||
mac_str = ':'.join(['%02X' % ord(x) for x in mac_addr])
|
mac_str = ':'.join(['%02X' % x for x in mac_addr])
|
||||||
# is the UUID received (PXE mode)
|
# is the UUID received (PXE mode)
|
||||||
if 97 in options and len(options[97]) == 17:
|
if 97 in options and len(options[97]) == 17:
|
||||||
uuid = options[97][1:]
|
uuid = options[97][1:]
|
||||||
@ -360,9 +379,9 @@ class BootpServer:
|
|||||||
uuid = self.uuidpool.get(mac_addr, None)
|
uuid = self.uuidpool.get(mac_addr, None)
|
||||||
pxe = False
|
pxe = False
|
||||||
self.log.info('PXE UUID not present in request')
|
self.log.info('PXE UUID not present in request')
|
||||||
uuid_str = uuid and ('%s-%s-%s-%s-%s' % tuple(
|
uuid_str = uuid and ('%s-%s-%s-%s-%s' % tuple([hexlify(x)
|
||||||
[hexlify(x) for x in uuid[0:4], uuid[4:6], uuid[6:8],
|
for x in (uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:16])
|
||||||
uuid[8:10], uuid[10:16]])).upper()
|
])).upper()
|
||||||
if uuid_str:
|
if uuid_str:
|
||||||
self.log.info('UUID is %s for MAC %s' % (uuid_str, mac_str))
|
self.log.info('UUID is %s for MAC %s' % (uuid_str, mac_str))
|
||||||
|
|
||||||
@ -388,12 +407,12 @@ class BootpServer:
|
|||||||
|
|
||||||
# if the state has not evolved from idle, there is nothing to do
|
# if the state has not evolved from idle, there is nothing to do
|
||||||
if newstate == self.ST_IDLE:
|
if newstate == self.ST_IDLE:
|
||||||
self.log.info('Request from %s ignored (idle state)' % mac_str)
|
|
||||||
sdhcp = 'allow_simple_dhcp'
|
sdhcp = 'allow_simple_dhcp'
|
||||||
simple_dhcp = \
|
simple_dhcp = \
|
||||||
self.config.has_option(self.bootp_section, sdhcp) and \
|
self.config.has_option(self.BOOTP_SECTION, sdhcp) and \
|
||||||
to_bool(self.config.get(self.bootp_section, sdhcp))
|
to_bool(self.config.get(self.BOOTP_SECTION, sdhcp))
|
||||||
if not simple_dhcp:
|
if not simple_dhcp:
|
||||||
|
self.log.info('Request from %s ignored (idle state)' % mac_str)
|
||||||
return
|
return
|
||||||
if not dhcp_msg_type:
|
if not dhcp_msg_type:
|
||||||
# Legacy DHCP: assuming discover by default
|
# Legacy DHCP: assuming discover by default
|
||||||
@ -404,8 +423,6 @@ class BootpServer:
|
|||||||
# remote access is always validated on each request
|
# remote access is always validated on each request
|
||||||
if self.access in self.ACCESS_REMOTE:
|
if self.access in self.ACCESS_REMOTE:
|
||||||
# need to query a host to grant or reject access
|
# need to query a host to grant or reject access
|
||||||
import urlparse
|
|
||||||
import urllib
|
|
||||||
netloc = self.config.get(self.access, 'location')
|
netloc = self.config.get(self.access, 'location')
|
||||||
path = self.config.get(self.access, pxe and 'pxe' or 'dhcp')
|
path = self.config.get(self.access, pxe and 'pxe' or 'dhcp')
|
||||||
timeout = int(self.config.get(self.access, 'timeout', '5'))
|
timeout = int(self.config.get(self.access, 'timeout', '5'))
|
||||||
@ -422,14 +439,12 @@ class BootpServer:
|
|||||||
if to_bool(always_check):
|
if to_bool(always_check):
|
||||||
checkhost = True
|
checkhost = True
|
||||||
if checkhost:
|
if checkhost:
|
||||||
query = urllib.urlencode(parameters)
|
query = urlencode(parameters)
|
||||||
urlparts = (self.access, netloc, path, query, '')
|
urlparts = (self.access, netloc, path, query, '')
|
||||||
url = urlparse.urlunsplit(urlparts)
|
url = urlunsplit(urlparts)
|
||||||
self.log.info('Requesting URL: %s' % url)
|
self.log.info('Requesting URL: %s' % url)
|
||||||
import urllib2
|
|
||||||
import httplib
|
|
||||||
try:
|
try:
|
||||||
up = urllib2.urlopen(url, timeout=timeout)
|
up = urlopen(url, timeout=timeout)
|
||||||
for l in up:
|
for l in up:
|
||||||
try:
|
try:
|
||||||
# Look for extra definition within the reply
|
# Look for extra definition within the reply
|
||||||
@ -441,16 +456,12 @@ class BootpServer:
|
|||||||
filename = v
|
filename = v
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
except urllib2.HTTPError, e:
|
except HTTPError as exc:
|
||||||
self.log.error('HTTP Error: %s' % str(e))
|
self.log.error('HTTP Error: %s' % exc)
|
||||||
self.states[mac_str] = self.ST_IDLE
|
self.states[mac_str] = self.ST_IDLE
|
||||||
return
|
return
|
||||||
except urllib2.URLError, e:
|
except URLError as exc:
|
||||||
self.log.critical('Internal error: %s' % str(e))
|
self.log.critical('Internal error: %s' % exc)
|
||||||
self.states[mac_str] = self.ST_IDLE
|
|
||||||
return
|
|
||||||
except httplib.HTTPException, e:
|
|
||||||
self.log.error('Server error: %s' % type(e))
|
|
||||||
self.states[mac_str] = self.ST_IDLE
|
self.states[mac_str] = self.ST_IDLE
|
||||||
return
|
return
|
||||||
# local access is only validated if mac address is not yet known
|
# local access is only validated if mac address is not yet known
|
||||||
@ -476,9 +487,9 @@ class BootpServer:
|
|||||||
# construct reply
|
# construct reply
|
||||||
buf[BOOTP_HOPS] = 0
|
buf[BOOTP_HOPS] = 0
|
||||||
buf[BOOTP_OP] = BOOTREPLY
|
buf[BOOTP_OP] = BOOTREPLY
|
||||||
self.log.info('Client IP: %s' % socket.inet_ntoa(buf[7]))
|
ciaddr = buf[BOOTP_CIADDR]
|
||||||
if buf[BOOTP_CIADDR] == '\x00\x00\x00\x00':
|
if not sunpack('!I', ciaddr)[0]:
|
||||||
self.log.debug('Client needs its address')
|
self.log.info('Client needs its address')
|
||||||
ipaddr = iptoint(self.pool_start)
|
ipaddr = iptoint(self.pool_start)
|
||||||
ip = None
|
ip = None
|
||||||
if mac_str in self.ippool:
|
if mac_str in self.ippool:
|
||||||
@ -486,7 +497,7 @@ class BootpServer:
|
|||||||
self.log.info('Lease for MAC %s already defined as IP %s' %
|
self.log.info('Lease for MAC %s already defined as IP %s' %
|
||||||
(mac_str, ip))
|
(mac_str, ip))
|
||||||
else:
|
else:
|
||||||
for idx in xrange(self.pool_count):
|
for idx in range(self.pool_count):
|
||||||
ipkey = inttoip(ipaddr+idx)
|
ipkey = inttoip(ipaddr+idx)
|
||||||
self.log.debug('Check for IP %s' % ipkey)
|
self.log.debug('Check for IP %s' % ipkey)
|
||||||
if ipkey not in self.ippool.values():
|
if ipkey not in self.ippool.values():
|
||||||
@ -497,32 +508,33 @@ class BootpServer:
|
|||||||
raise BootpError('No more IP available in definined pool')
|
raise BootpError('No more IP available in definined pool')
|
||||||
|
|
||||||
mask = iptoint(self.config.get(
|
mask = iptoint(self.config.get(
|
||||||
self.bootp_section, 'netmask', self.netconfig['mask']))
|
self.BOOTP_SECTION, 'netmask', self.netconfig['mask']))
|
||||||
reply_broadcast = iptoint(ip) & mask
|
reply_broadcast = iptoint(ip) & mask
|
||||||
reply_broadcast |= (~mask) & ((1 << 32)-1)
|
reply_broadcast |= (~mask) & ((1 << 32)-1)
|
||||||
buf[BOOTP_YIADDR] = socket.inet_aton(ip)
|
buf[BOOTP_YIADDR] = inet_aton(ip)
|
||||||
buf[BOOTP_SECS] = 0
|
buf[BOOTP_SECS] = 0
|
||||||
buf[BOOTP_FLAGS] = BOOTP_FLAGS_BROADCAST
|
buf[BOOTP_FLAGS] = BOOTP_FLAGS_BROADCAST
|
||||||
|
|
||||||
relay = buf[BOOTP_GIADDR]
|
relay = buf[BOOTP_GIADDR]
|
||||||
if relay != b'\x00\x00\x00\x00':
|
if sunpack('!I', relay)[0]:
|
||||||
addr = (socket.inet_ntoa(relay), addr[1])
|
addr = (inet_ntoa(relay), addr[1])
|
||||||
else:
|
else:
|
||||||
addr = (inttoip(reply_broadcast), addr[1])
|
addr = (inttoip(reply_broadcast), addr[1])
|
||||||
self.log.info('Reply to: %s:%s' % addr)
|
self.log.info('Reply to: %s:%s' % addr)
|
||||||
else:
|
else:
|
||||||
buf[BOOTP_YIADDR] = buf[BOOTP_CIADDR]
|
self.log.info('Client IP: %s' % inet_ntoa(ciaddr))
|
||||||
ip = socket.inet_ntoa(buf[BOOTP_YIADDR])
|
buf[BOOTP_YIADDR] = ciaddr
|
||||||
buf[BOOTP_SIADDR] = socket.inet_aton(server_addr)
|
ip = inet_ntoa(buf[BOOTP_YIADDR])
|
||||||
|
buf[BOOTP_SIADDR] = inet_aton(server_addr)
|
||||||
# sname
|
# sname
|
||||||
buf[BOOTP_SNAME] = \
|
buf[BOOTP_SNAME] = \
|
||||||
'.'.join([self.config.get(self.bootp_section,
|
'.'.join([self.config.get(self.BOOTP_SECTION,
|
||||||
'servername', 'unknown'),
|
'servername', 'unknown'),
|
||||||
self.config.get(self.bootp_section,
|
self.config.get(self.BOOTP_SECTION,
|
||||||
'domain', 'localdomain')])
|
'domain', 'localdomain')]).encode()
|
||||||
# file
|
# file
|
||||||
buf[BOOTP_FILE] = self.config.get(self.bootp_section,
|
buf[BOOTP_FILE] = self.config.get(self.BOOTP_SECTION,
|
||||||
'boot_file', '\x00')
|
'boot_file', '\x00').encode()
|
||||||
|
|
||||||
if not dhcp_msg_type:
|
if not dhcp_msg_type:
|
||||||
self.log.warn('No DHCP message type found, discarding request')
|
self.log.warn('No DHCP message type found, discarding request')
|
||||||
@ -566,38 +578,37 @@ class BootpServer:
|
|||||||
else:
|
else:
|
||||||
self.log.debug('No filename defined for IP %s' % ip)
|
self.log.debug('No filename defined for IP %s' % ip)
|
||||||
|
|
||||||
pkt = struct.pack(DHCPFormat, *buf)
|
pkt = spack(DHCPFORMAT, *buf)
|
||||||
pkt += struct.pack('!BBB', DHCP_MSG, 1, dhcp_reply)
|
pkt += spack('!BBB', DHCP_MSG, 1, dhcp_reply)
|
||||||
server = socket.inet_aton(server_addr)
|
server = inet_aton(server_addr)
|
||||||
pkt += struct.pack('!BB4s', DHCP_SERVER, 4, server)
|
pkt += spack('!BB4s', DHCP_SERVER, 4, server)
|
||||||
|
|
||||||
mask = socket.inet_aton(self.config.get(
|
mask = inet_aton(self.config.get(
|
||||||
self.bootp_section, 'netmask', self.netconfig['mask']))
|
self.BOOTP_SECTION, 'netmask', self.netconfig['mask']))
|
||||||
|
|
||||||
pkt += struct.pack('!BB4s', DHCP_IP_MASK, 4, mask)
|
pkt += spack('!BB4s', DHCP_IP_MASK, 4, mask)
|
||||||
|
|
||||||
gateway_addr = self.config.get(self.bootp_section, 'gateway', '')
|
gateway_addr = self.config.get(self.BOOTP_SECTION, 'gateway', '')
|
||||||
if gateway_addr:
|
if gateway_addr:
|
||||||
gateway = socket.inet_aton(gateway_addr)
|
gateway = inet_aton(gateway_addr)
|
||||||
else:
|
else:
|
||||||
gateway = server
|
gateway = server
|
||||||
pkt += struct.pack('!BB4s', DHCP_IP_GATEWAY, 4, gateway)
|
pkt += spack('!BB4s', DHCP_IP_GATEWAY, 4, gateway)
|
||||||
|
|
||||||
dns = self.config.get(self.bootp_section,
|
dns = self.config.get(self.BOOTP_SECTION,
|
||||||
'dns', None)
|
'dns', None)
|
||||||
if dns:
|
if dns:
|
||||||
if dns.lower() == 'auto':
|
if dns.lower() == 'auto':
|
||||||
dns_list = self.get_dns_servers() or [socket.inet_ntoa(server)]
|
dns_list = self.get_dns_servers() or [inet_ntoa(server)]
|
||||||
else:
|
else:
|
||||||
dns_list = dns.split(';')
|
dns_list = dns.split(';')
|
||||||
for dns_str in dns_list:
|
for dns_str in dns_list:
|
||||||
dns_ip = socket.inet_aton(dns_str)
|
dns_ip = inet_aton(dns_str)
|
||||||
pkt += struct.pack('!BB4s', DHCP_IP_DNS, 4, dns_ip)
|
pkt += spack('!BB4s', DHCP_IP_DNS, 4, dns_ip)
|
||||||
pkt += struct.pack('!BBI', DHCP_LEASE_TIME, 4,
|
pkt += spack('!BBI', DHCP_LEASE_TIME, 4,
|
||||||
int(self.config.get(self.bootp_section,
|
int(self.config.get(self.BOOTP_SECTION,
|
||||||
'lease_time',
|
'lease_time',
|
||||||
str(24*3600))))
|
str(24*3600))))
|
||||||
pkt += struct.pack('!BB', DHCP_END, 0)
|
|
||||||
|
|
||||||
# do not attempt to produce a PXE-augmented response for
|
# do not attempt to produce a PXE-augmented response for
|
||||||
# regular DHCP requests
|
# regular DHCP requests
|
||||||
@ -608,12 +619,15 @@ class BootpServer:
|
|||||||
else:
|
else:
|
||||||
extra_buf = self.build_dhcp_options(hostname)
|
extra_buf = self.build_dhcp_options(hostname)
|
||||||
|
|
||||||
|
pkt += extra_buf
|
||||||
|
pkt += spack('!BB', DHCP_END, 0)
|
||||||
|
|
||||||
# update the UUID cache
|
# update the UUID cache
|
||||||
if pxe:
|
if pxe:
|
||||||
self.uuidpool[mac_addr] = uuid
|
self.uuidpool[mac_addr] = uuid
|
||||||
|
|
||||||
# send the response
|
# send the response
|
||||||
sock.sendto(pkt + extra_buf, addr)
|
sock.sendto(pkt, addr)
|
||||||
|
|
||||||
# update the current state
|
# update the current state
|
||||||
if currentstate != newstate:
|
if currentstate != newstate:
|
||||||
@ -622,7 +636,7 @@ class BootpServer:
|
|||||||
self.states[mac_str] = newstate
|
self.states[mac_str] = newstate
|
||||||
|
|
||||||
def get_dns_servers(self):
|
def get_dns_servers(self):
|
||||||
nscre = re.compile('nameserver\s+(\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3})\s')
|
nscre = recompile(r'nameserver\s+(\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3})\s')
|
||||||
result = []
|
result = []
|
||||||
try:
|
try:
|
||||||
with open('/etc/resolv.conf', 'r') as resolv:
|
with open('/etc/resolv.conf', 'r') as resolv:
|
||||||
@ -632,7 +646,7 @@ class BootpServer:
|
|||||||
dns = mo.group(1)
|
dns = mo.group(1)
|
||||||
self.log.info('Found nameserver: %s' % dns)
|
self.log.info('Found nameserver: %s' % dns)
|
||||||
result.append(dns)
|
result.append(dns)
|
||||||
except Exception, e:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
if not result:
|
if not result:
|
||||||
self.log.info('No nameserver found')
|
self.log.info('No nameserver found')
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (c) 2010-2016 Emmanuel Blot <emmanuel.blot@free.fr>
|
# Copyright (c) 2010-2019 Emmanuel Blot <emmanuel.blot@free.fr>
|
||||||
# Copyright (c) 2010-2011 Neotion
|
# Copyright (c) 2010-2011 Neotion
|
||||||
#
|
#
|
||||||
# This library is free software; you can redistribute it and/or
|
# This library is free software; you can redistribute it and/or
|
||||||
@ -18,14 +17,22 @@
|
|||||||
# License along with this library; if not, write to the Free Software
|
# License along with this library; if not, write to the Free Software
|
||||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
|
||||||
import sys
|
"""HTTPd tiny server to exercise the pybootd daemon"""
|
||||||
import urlparse
|
|
||||||
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
|
from argparse import ArgumentParser, FileType
|
||||||
from optparse import OptionParser
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
from util import logger_factory, to_bool, to_int, EasyConfigParser
|
from sys import exit as sysexit, modules, stderr
|
||||||
|
from traceback import format_exc
|
||||||
|
from urllib.parse import parse_qs, urlsplit
|
||||||
|
from pybootd.util import logger_factory, to_bool, to_int, EasyConfigParser
|
||||||
|
|
||||||
|
|
||||||
class HttpdDaemon(HTTPServer):
|
#pylint: disable-msg=broad-except
|
||||||
|
#pylint: disable-msg=missing-docstring
|
||||||
|
#pylint: disable-msg=invalid-name
|
||||||
|
|
||||||
|
|
||||||
|
class HttpdTestDaemon(HTTPServer):
|
||||||
|
|
||||||
class ReqHandler(BaseHTTPRequestHandler):
|
class ReqHandler(BaseHTTPRequestHandler):
|
||||||
|
|
||||||
@ -33,8 +40,9 @@ class HttpdDaemon(HTTPServer):
|
|||||||
log = self.server.log
|
log = self.server.log
|
||||||
log.debug("GET from %s:%d" % self.client_address)
|
log.debug("GET from %s:%d" % self.client_address)
|
||||||
log.debug("Request: %s" % self.path)
|
log.debug("Request: %s" % self.path)
|
||||||
urlparts = urlparse.urlsplit(self.path)
|
urlparts = urlsplit(self.path)
|
||||||
query = urlparse.parse_qs(urlparts.query)
|
query = parse_qs(urlparts.query)
|
||||||
|
uuid = ''
|
||||||
if urlparts.path in ('/boot', '/linux'):
|
if urlparts.path in ('/boot', '/linux'):
|
||||||
if 'uuid' in query:
|
if 'uuid' in query:
|
||||||
uuids = query['uuid']
|
uuids = query['uuid']
|
||||||
@ -79,30 +87,39 @@ class HttpdDaemon(HTTPServer):
|
|||||||
self.serve_forever()
|
self.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
def main():
|
||||||
usage = 'Usage: %prog [options]\n' \
|
debug = False
|
||||||
' 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:
|
try:
|
||||||
bt = HttpdDaemon(logger, cfgparser)
|
argparser = ArgumentParser(description=modules[__name__].__doc__)
|
||||||
|
argparser.add_argument('-c', '--config', dest='config', required=True,
|
||||||
|
type=FileType('rt'),
|
||||||
|
help='configuration file')
|
||||||
|
argparser.add_argument('-d', '--debug', action='store_true',
|
||||||
|
help='enable debug mode')
|
||||||
|
args = argparser.parse_args()
|
||||||
|
|
||||||
|
cfgparser = EasyConfigParser()
|
||||||
|
cfgparser.read_file(args.config)
|
||||||
|
|
||||||
|
logger = logger_factory(logtype=cfgparser.get('logger', 'type',
|
||||||
|
'stderr'),
|
||||||
|
logfile=cfgparser.get('logger', 'file'),
|
||||||
|
level=cfgparser.get('logger', 'level', 'info'))
|
||||||
|
|
||||||
|
bt = HttpdTestDaemon(logger, cfgparser)
|
||||||
bt.start()
|
bt.start()
|
||||||
while True:
|
while True:
|
||||||
import time
|
import time
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
|
except Exception as exc:
|
||||||
|
print('\nError: %s' % exc, file=stderr)
|
||||||
|
if debug:
|
||||||
|
print(format_exc(chain=False), file=stderr)
|
||||||
|
sysexit(1)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print "Aborting..."
|
print("\nAborting...", file=stderr)
|
||||||
|
sysexit(2)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
211
pybootd/tftpd.py
211
pybootd/tftpd.py
@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# Copyright (c) 2010-2019 Emmanuel Blot <emmanuel.blot@free.fr>
|
||||||
#
|
|
||||||
# Copyright (c) 2010-2016 Emmanuel Blot <emmanuel.blot@free.fr>
|
|
||||||
# Copyright (c) 2010-2011 Neotion
|
# Copyright (c) 2010-2011 Neotion
|
||||||
#
|
#
|
||||||
# This library is free software; you can redistribute it and/or
|
# This library is free software; you can redistribute it and/or
|
||||||
@ -18,29 +16,43 @@
|
|||||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
from configparser import NoSectionError
|
||||||
import select
|
from io import StringIO
|
||||||
import socket
|
from re import compile as recompile, sub as resub
|
||||||
import string
|
from select import select
|
||||||
import struct
|
from socket import socket, AF_INET, SOCK_DGRAM
|
||||||
import sys
|
from struct import pack as spack, unpack as sunpack
|
||||||
import time
|
from sys import argv, exc_info
|
||||||
import thread
|
from threading import Thread
|
||||||
import urllib2
|
from time import time as now
|
||||||
import urlparse
|
from traceback import format_exc
|
||||||
from ConfigParser import NoSectionError
|
from urllib.parse import urlparse, urlsplit
|
||||||
from cStringIO import StringIO
|
from urllib.request import urlopen
|
||||||
from pybootd import pybootd_path
|
from . import pybootd_path
|
||||||
from util import hexline
|
from .util import hexline
|
||||||
|
|
||||||
|
#pylint: disable-msg=broad-except
|
||||||
|
#pylint: disable-msg=invalid-name
|
||||||
|
#pylint: disable-msg=missing-docstring
|
||||||
|
|
||||||
__all__ = ['TftpServer']
|
|
||||||
|
|
||||||
TFTP_PORT = 69
|
TFTP_PORT = 69
|
||||||
|
|
||||||
|
|
||||||
class TftpError(AssertionError):
|
class TftpError(RuntimeError):
|
||||||
|
(NOT_DEF,
|
||||||
|
FILE_NOT_FOUND,
|
||||||
|
ACCESS_ERROR,
|
||||||
|
ALLOC_EXCEED,
|
||||||
|
ILLEGAL,
|
||||||
|
UNKNOWN_ID,
|
||||||
|
ALREADY_EXIST,
|
||||||
|
NO_SUCH_USER) = range(8)
|
||||||
|
|
||||||
"""Any TFTP error"""
|
"""Any TFTP error"""
|
||||||
pass
|
def __init__(self, code, msg):
|
||||||
|
super(TftpError, self).__init__(msg)
|
||||||
|
self.code = code
|
||||||
|
|
||||||
|
|
||||||
class TftpConnection(object):
|
class TftpConnection(object):
|
||||||
@ -50,6 +62,7 @@ class TftpConnection(object):
|
|||||||
ACK = 4
|
ACK = 4
|
||||||
ERR = 5
|
ERR = 5
|
||||||
OACK = 6
|
OACK = 6
|
||||||
|
|
||||||
HDRSIZE = 4 # number of bytes for OPCODE and BLOCK in header
|
HDRSIZE = 4 # number of bytes for OPCODE and BLOCK in header
|
||||||
|
|
||||||
def __init__(self, server, port=0):
|
def __init__(self, server, port=0):
|
||||||
@ -70,7 +83,7 @@ class TftpConnection(object):
|
|||||||
|
|
||||||
def _bind(self, host='', port=TFTP_PORT):
|
def _bind(self, host='', port=TFTP_PORT):
|
||||||
self.log.debug('bind %s:%d' % (host, port))
|
self.log.debug('bind %s:%d' % (host, port))
|
||||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
self.sock = socket(AF_INET, SOCK_DGRAM)
|
||||||
if host or port:
|
if host or port:
|
||||||
self.sock.bind((host, port))
|
self.sock.bind((host, port))
|
||||||
|
|
||||||
@ -86,7 +99,7 @@ class TftpConnection(object):
|
|||||||
timeout = self.timeout
|
timeout = self.timeout
|
||||||
retry = self.server.retry
|
retry = self.server.retry
|
||||||
while retry:
|
while retry:
|
||||||
r, w, e = select.select([fno], [], [fno], timeout)
|
r = select([fno], [], [fno], timeout)[0]
|
||||||
if not r:
|
if not r:
|
||||||
# We timed out -- retransmit
|
# We timed out -- retransmit
|
||||||
retry = retry - 1
|
retry = retry - 1
|
||||||
@ -98,7 +111,7 @@ class TftpConnection(object):
|
|||||||
if addr == client_addr:
|
if addr == client_addr:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
raise TftpError(4, 'Transfer timed out')
|
raise TftpError(TftpError.ALLOC_EXCEED, 'Transfer timed out')
|
||||||
# end while
|
# end while
|
||||||
return self.parse(data)
|
return self.parse(data)
|
||||||
|
|
||||||
@ -121,14 +134,15 @@ class TftpConnection(object):
|
|||||||
path = self.server.bootpd.get_filename(client_ip)
|
path = self.server.bootpd.get_filename(client_ip)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
def parse(self, data, unpack=struct.unpack):
|
def parse(self, data, unpack=sunpack):
|
||||||
self.log.debug('parse')
|
self.log.debug('parse')
|
||||||
buf = buffer(data)
|
buf = data
|
||||||
pkt = {}
|
pkt = {}
|
||||||
opcode = pkt['opcode'] = unpack('!h', buf[:2])[0]
|
opcode = pkt['opcode'] = unpack('!h', buf[:2])[0]
|
||||||
if (opcode == self.RRQ) or (opcode == self.WRQ):
|
if (opcode == self.RRQ) or (opcode == self.WRQ):
|
||||||
resource, mode, options = string.split(data[2:], '\000', 2)
|
resource, mode, options = data[2:].split(b'\x00', 2)
|
||||||
resource = self.server.fcre.sub(self._filter_file, resource)
|
resource = self.server.fcre.sub(self._filter_file,
|
||||||
|
resource.decode())
|
||||||
if self.server.root and self.is_url(self.server.root):
|
if self.server.root and self.is_url(self.server.root):
|
||||||
resource = '%s/%s' % (self.server.root, resource)
|
resource = '%s/%s' % (self.server.root, resource)
|
||||||
else:
|
else:
|
||||||
@ -138,7 +152,7 @@ class TftpConnection(object):
|
|||||||
if not self.server.genfilecre.match(resource):
|
if not self.server.genfilecre.match(resource):
|
||||||
if resource.startswith('^%s' % os.sep):
|
if resource.startswith('^%s' % os.sep):
|
||||||
resource = os.path.join(
|
resource = os.path.join(
|
||||||
os.path.dirname(sys.argv[0]),
|
os.path.dirname(argv[0]),
|
||||||
resource.lstrip('^%s' % os.sep))
|
resource.lstrip('^%s' % os.sep))
|
||||||
elif self.server.root:
|
elif self.server.root:
|
||||||
if self.server.root.startswith(os.sep):
|
if self.server.root.startswith(os.sep):
|
||||||
@ -147,7 +161,7 @@ class TftpConnection(object):
|
|||||||
resource)
|
resource)
|
||||||
else:
|
else:
|
||||||
# Relative root directory, from the daemon path
|
# Relative root directory, from the daemon path
|
||||||
daemonpath = os.path.dirname(sys.argv[0])
|
daemonpath = os.path.dirname(argv[0])
|
||||||
if not daemonpath.startswith(os.sep):
|
if not daemonpath.startswith(os.sep):
|
||||||
daemonpath = os.path.normpath(
|
daemonpath = os.path.normpath(
|
||||||
os.path.join(os.getcwd(), daemonpath))
|
os.path.join(os.getcwd(), daemonpath))
|
||||||
@ -158,7 +172,7 @@ class TftpConnection(object):
|
|||||||
pkt['filename'] = resource
|
pkt['filename'] = resource
|
||||||
pkt['mode'] = mode
|
pkt['mode'] = mode
|
||||||
while options:
|
while options:
|
||||||
key, value, options = options.split('\000', 2)
|
key, value, options = options.split(b'\x00', 2)
|
||||||
if key == 'blksize':
|
if key == 'blksize':
|
||||||
self.blocksize = int(value)
|
self.blocksize = int(value)
|
||||||
elif key == 'timeout':
|
elif key == 'timeout':
|
||||||
@ -173,7 +187,7 @@ class TftpConnection(object):
|
|||||||
errnum = pkt['errnum'] = unpack('!h', buf[2:4])[0]
|
errnum = pkt['errnum'] = unpack('!h', buf[2:4])[0]
|
||||||
errtxt = pkt['errtxt'] = buf[4:-1]
|
errtxt = pkt['errtxt'] = buf[4:-1]
|
||||||
else:
|
else:
|
||||||
raise TftpError(4, 'Unknown packet type')
|
raise TftpError(TftpError.ILLEGAL, 'Unknown packet type')
|
||||||
return pkt
|
return pkt
|
||||||
|
|
||||||
def retransmit(self):
|
def retransmit(self):
|
||||||
@ -194,7 +208,7 @@ class TftpConnection(object):
|
|||||||
pkt = self.parse(data)
|
pkt = self.parse(data)
|
||||||
opcode = pkt['opcode']
|
opcode = pkt['opcode']
|
||||||
if opcode not in (RRQ, WRQ):
|
if opcode not in (RRQ, WRQ):
|
||||||
raise TftpError(4, 'Bad request')
|
raise TftpError(TftpError.ILLEGAL, 'Bad request')
|
||||||
|
|
||||||
# Start lock-step transfer
|
# Start lock-step transfer
|
||||||
self.active = 1
|
self.active = 1
|
||||||
@ -215,13 +229,12 @@ class TftpConnection(object):
|
|||||||
elif opcode == ERR:
|
elif opcode == ERR:
|
||||||
self.recv_err(pkt)
|
self.recv_err(pkt)
|
||||||
else:
|
else:
|
||||||
raise TftpError(5, 'Invalid opcode')
|
raise TftpError(TftpError.ILLEGAL, 'Invalid opcode')
|
||||||
self.log.debug('End of active: %s:%s' % addr)
|
self.log.debug('End of active: %s:%s' % addr)
|
||||||
except TftpError, detail:
|
except TftpError as exc:
|
||||||
self.send_error(detail[0], detail[1])
|
self.send_error(exc.code, str(exc))
|
||||||
except:
|
except Exception:
|
||||||
import traceback
|
self.log.error(format_exc(chain=False))
|
||||||
self.log.error(traceback.format_exc())
|
|
||||||
self.log.debug('Ending connection %s:%s' % addr)
|
self.log.debug('Ending connection %s:%s' % addr)
|
||||||
|
|
||||||
def recv_ack(self, pkt):
|
def recv_ack(self, pkt):
|
||||||
@ -245,19 +258,19 @@ class TftpConnection(object):
|
|||||||
self.handle_err(pkt)
|
self.handle_err(pkt)
|
||||||
self.retransmit()
|
self.retransmit()
|
||||||
|
|
||||||
def send_data(self, data, pack=struct.pack):
|
def send_data(self, data, pack=spack):
|
||||||
self.log.debug('send_data')
|
self.log.debug('send_data')
|
||||||
if not self.time:
|
if not self.time:
|
||||||
self.time = time.time()
|
self.time = now()
|
||||||
blocksize = self.blocksize
|
blocksize = self.blocksize
|
||||||
block = self.blockNumber = self.blockNumber + 1
|
block = self.blockNumber = self.blockNumber + 1
|
||||||
lendata = len(data)
|
lendata = len(data)
|
||||||
format = '!hh%ds' % lendata
|
fmt = '!hh%ds' % lendata
|
||||||
pkt = pack(format, self.DATA, block, data)
|
pkt = pack(fmt, self.DATA, block, data)
|
||||||
self.send(pkt)
|
self.send(pkt)
|
||||||
self.active = (len(data) == blocksize)
|
self.active = (len(data) == blocksize)
|
||||||
if not self.active and self.time:
|
if not self.active and self.time:
|
||||||
total = time.time()-self.time
|
total = now()-self.time
|
||||||
self.time = 0
|
self.time = 0
|
||||||
try:
|
try:
|
||||||
name = self.file.name
|
name = self.file.name
|
||||||
@ -270,31 +283,30 @@ class TftpConnection(object):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
# StringIO does not have a 'name' attribute
|
# StringIO does not have a 'name' attribute
|
||||||
pass
|
pass
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
import traceback
|
self.log.error('Error: %s' % exc)
|
||||||
traceback.print_exc()
|
self.log.warn('%s', format_exc(chain=False))
|
||||||
pass
|
|
||||||
|
|
||||||
def send_ack(self, pack=struct.pack):
|
def send_ack(self, pack=spack):
|
||||||
self.log.debug('send_ack')
|
self.log.debug('send_ack')
|
||||||
block = self.blockNumber
|
block = self.blockNumber
|
||||||
self.blockNumber = self.blockNumber + 1
|
self.blockNumber = self.blockNumber + 1
|
||||||
format = '!hh'
|
fmt = '!hh'
|
||||||
pkt = pack(format, self.ACK, block)
|
pkt = pack(fmt, self.ACK, block)
|
||||||
self.send(pkt)
|
self.send(pkt)
|
||||||
|
|
||||||
def send_error(self, errnum, errtext, pack=struct.pack):
|
def send_error(self, errnum, errtext, pack=spack):
|
||||||
self.log.debug('send_error')
|
self.log.debug('send_error')
|
||||||
errtext = errtext + '\000'
|
errtext = errtext.encode() + b'\x00'
|
||||||
format = '!hh%ds' % len(errtext)
|
fmt = '!hh%ds' % len(errtext)
|
||||||
outdata = pack(format, self.ERR, errnum, errtext)
|
outdata = pack(fmt, self.ERR, errnum, errtext)
|
||||||
self.sock.sendto(outdata, self.client_addr)
|
self.sock.sendto(outdata, self.client_addr)
|
||||||
|
|
||||||
def send_oack(self, options, pack=struct.pack):
|
def send_oack(self, options, pack=spack):
|
||||||
self.log.debug('send_oack')
|
self.log.debug('send_oack')
|
||||||
pkt = pack('!h', self.OACK)
|
pkt = pack('!h', self.OACK)
|
||||||
for k, v in options:
|
for k, v in options:
|
||||||
pkt += k + '\x00' + v + '\x00'
|
pkt += k.encode() + b'\x00' + v.encode() + b'\x00'
|
||||||
self.send(pkt)
|
self.send(pkt)
|
||||||
# clear out the last packet buffer to prevent from retransmitting it
|
# clear out the last packet buffer to prevent from retransmitting it
|
||||||
self.lastpkt = ''
|
self.lastpkt = ''
|
||||||
@ -310,14 +322,15 @@ class TftpConnection(object):
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
if self.is_url(resource):
|
if self.is_url(resource):
|
||||||
rp = urllib2.urlopen(resource)
|
rp = urlopen(resource)
|
||||||
meta = rp.info()
|
meta = rp.info()
|
||||||
filesize = int(meta.getheaders('Content-Length')[0])
|
filesize = int(meta.getheaders('Content-Length')[0])
|
||||||
else:
|
else:
|
||||||
filesize = os.stat(resource)[6]
|
filesize = os.stat(resource)[6]
|
||||||
except Exception:
|
except Exception:
|
||||||
self.active = False
|
self.active = False
|
||||||
self.send_error(1, 'Cannot access resource')
|
self.send_error(TftpError.FILE_NOT_FOUND,
|
||||||
|
'Cannot access resource')
|
||||||
self.log.warn('Cannot stat resource %s' % resource)
|
self.log.warn('Cannot stat resource %s' % resource)
|
||||||
return
|
return
|
||||||
self.log.info('Send size request file %s size: %d' %
|
self.log.info('Send size request file %s size: %d' %
|
||||||
@ -332,16 +345,17 @@ class TftpConnection(object):
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
if self.is_url(resource):
|
if self.is_url(resource):
|
||||||
|
self.file = urlopen(resource)
|
||||||
self.log.info("Sending resource '%s'" % resource)
|
self.log.info("Sending resource '%s'" % resource)
|
||||||
self.file = urllib2.urlopen(resource)
|
|
||||||
else:
|
else:
|
||||||
resource = os.path.realpath(resource)
|
resource = os.path.realpath(resource)
|
||||||
self.log.info("Sending file '%s'" % resource)
|
|
||||||
self.file = open(resource, 'rb')
|
self.file = open(resource, 'rb')
|
||||||
except Exception:
|
self.log.info("Sending file '%s'" % resource)
|
||||||
self.send_error(1, 'Cannot open resource')
|
except Exception as exc:
|
||||||
|
self.send_error(TftpError.FILE_NOT_FOUND,
|
||||||
|
'Cannot open resource')
|
||||||
self.log.warn('Cannot open file for reading %s: %s' %
|
self.log.warn('Cannot open file for reading %s: %s' %
|
||||||
sys.exc_info()[:2])
|
(resource, exc))
|
||||||
return
|
return
|
||||||
if 'tsize' not in pkt:
|
if 'tsize' not in pkt:
|
||||||
self.send_data(self.file.read(self.blocksize))
|
self.send_data(self.file.read(self.blocksize))
|
||||||
@ -357,9 +371,9 @@ class TftpConnection(object):
|
|||||||
self.log.info('Receiving file: %s' % resource)
|
self.log.info('Receiving file: %s' % resource)
|
||||||
self.file = open(resource, 'wb')
|
self.file = open(resource, 'wb')
|
||||||
except:
|
except:
|
||||||
self.send_error(1, 'Cannot open file')
|
self.send_error(TftpError.FILE_NOT_FOUND, 'Cannot open file')
|
||||||
self.log.error('Cannot open file for writing %s: %s' %
|
self.log.error('Cannot open file for writing %s: %s' %
|
||||||
sys.exc_info()[:2])
|
exc_info()[:2])
|
||||||
return
|
return
|
||||||
self.send_ack()
|
self.send_ack()
|
||||||
|
|
||||||
@ -379,7 +393,7 @@ class TftpConnection(object):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_url(path):
|
def is_url(path):
|
||||||
return bool(urlparse.urlsplit(path).scheme)
|
return bool(urlsplit(path).scheme)
|
||||||
|
|
||||||
|
|
||||||
class TftpServer:
|
class TftpServer:
|
||||||
@ -388,40 +402,48 @@ class TftpServer:
|
|||||||
Each request is handled in its own thread
|
Each request is handled in its own thread
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
TFTP_SECTION = 'tftpd'
|
||||||
|
|
||||||
def __init__(self, logger, config, bootpd=None):
|
def __init__(self, logger, config, bootpd=None):
|
||||||
self.log = logger
|
self.log = logger
|
||||||
self.config = config
|
self.config = config
|
||||||
self.sock = []
|
self.sock = []
|
||||||
self.bootpd = bootpd
|
self.bootpd = bootpd
|
||||||
self.blocksize = int(self.config.get('tftp', 'blocksize', '512'))
|
self.blocksize = int(self.config.get(self.TFTP_SECTION, 'blocksize',
|
||||||
self.timeout = float(self.config.get('tftp', 'timeout', '2.0'))
|
'512'))
|
||||||
self.retry = int(self.config.get('tftp', 'blocksize', '5'))
|
self.timeout = float(self.config.get(self.TFTP_SECTION, 'timeout', '2.0'))
|
||||||
self.root = self.config.get('tftp', 'root', os.getcwd())
|
self.retry = int(self.config.get(self.TFTP_SECTION, 'blocksize', '5'))
|
||||||
|
self.root = self.config.get(self.TFTP_SECTION, 'root', os.getcwd())
|
||||||
self.fcre, self.filepatterns = self.get_file_filters()
|
self.fcre, self.filepatterns = self.get_file_filters()
|
||||||
self.genfilecre = re.compile(r'\[(?P<name>[\w\.\-]+)\]')
|
self.genfilecre = recompile(r'\[(?P<name>[\w\.\-]+)\]')
|
||||||
|
self._resume = False
|
||||||
|
|
||||||
def bind(self):
|
def start(self):
|
||||||
netconfig = self.bootpd and self.bootpd.get_netconfig()
|
netconfig = self.bootpd and self.bootpd.get_netconfig()
|
||||||
host = self.config.get('tftp', 'address',
|
host = self.config.get(self.TFTP_SECTION, 'address',
|
||||||
netconfig and netconfig['server'])
|
netconfig and netconfig['server'])
|
||||||
if not host:
|
if not host:
|
||||||
raise TftpError('TFTP address no defined')
|
raise TftpError(TftpError.NO_SUCH_USER, 'TFTP address no defined')
|
||||||
port = int(self.config.get('tftp', 'port', str(TFTP_PORT)))
|
port = int(self.config.get(self.TFTP_SECTION, 'port', str(TFTP_PORT)))
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
sock = socket(AF_INET, SOCK_DGRAM)
|
||||||
self.sock.append(sock)
|
self.sock.append(sock)
|
||||||
sock.bind((host, port))
|
sock.bind((host, port))
|
||||||
|
self.log.info('Listening to %s:%s' % (host, port))
|
||||||
def forever(self):
|
self._resume = True
|
||||||
while True:
|
while self._resume:
|
||||||
if self.bootpd:
|
r = select(self.sock, [], self.sock, 0.25)[0]
|
||||||
if not self.bootpd.is_alive():
|
if not r:
|
||||||
self.log.info('Bootp daemon is dead, exiting')
|
continue
|
||||||
break
|
|
||||||
r, w, e = select.select(self.sock, [], self.sock)
|
|
||||||
for sock in r:
|
for sock in r:
|
||||||
data, addr = sock.recvfrom(516)
|
data, addr = sock.recvfrom(516)
|
||||||
t = TftpConnection(self)
|
tc = TftpConnection(self)
|
||||||
thread.start_new_thread(t.connect, (addr, data))
|
thread = Thread(target=tc.connect, args=(addr, data),
|
||||||
|
daemon=True)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._resume = False
|
||||||
|
|
||||||
def filter_file(self, connexion, mo):
|
def filter_file(self, connexion, mo):
|
||||||
# extract the position of the matching pattern, then extract the
|
# extract the position of the matching pattern, then extract the
|
||||||
@ -432,8 +454,9 @@ class TftpServer:
|
|||||||
if not filename:
|
if not filename:
|
||||||
continue
|
continue
|
||||||
filepattern = self.filepatterns[group]
|
filepattern = self.filepatterns[group]
|
||||||
return re.sub(r'\{(\w+)\}', connexion._dynreplace, filepattern)
|
return resub(r'\{(\w+)\}', connexion._dynreplace, filepattern)
|
||||||
raise TftpError('Internal error, file matching pattern issue')
|
raise TftpError(TftpError.NOT_DEF,
|
||||||
|
'Internal error, file matching pattern issue')
|
||||||
|
|
||||||
def get_file_filters(self):
|
def get_file_filters(self):
|
||||||
patterns = []
|
patterns = []
|
||||||
@ -442,12 +465,12 @@ class TftpServer:
|
|||||||
for pos, pattern in enumerate(self.config.options('filters'), 1):
|
for pos, pattern in enumerate(self.config.options('filters'), 1):
|
||||||
value = self.config.get('filters', pattern).strip()
|
value = self.config.get('filters', pattern).strip()
|
||||||
pattern = pattern.strip('\r\n \t')
|
pattern = pattern.strip('\r\n \t')
|
||||||
pattern = pattern.replace('.', '\.')
|
pattern = pattern.replace(r'.', r'\.')
|
||||||
pattern = pattern.replace('*', '.*').replace('?', '.')
|
pattern = pattern.replace(r'*', r'.*').replace(r'?', r'.')
|
||||||
pname = 'p%d' % pos
|
pname = 'p%d' % pos
|
||||||
replacements[pname] = value
|
replacements[pname] = value
|
||||||
patterns.append('(?P<%s>%s)' % (pname, pattern))
|
patterns.append(r'(?P<%s>%s)' % (pname, pattern))
|
||||||
xre = '^(?:\./)?(?:%s)$' % '|'.join(patterns)
|
xre = r'^(?:\./)?(?:%s)$' % r'|'.join(patterns)
|
||||||
except NoSectionError:
|
except NoSectionError:
|
||||||
xre = '^$'
|
xre = r'^$'
|
||||||
return (re.compile(xre), replacements)
|
return (recompile(xre), replacements)
|
||||||
|
137
pybootd/util.py
137
pybootd/util.py
@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# Copyright (c) 2010-2019 Emmanuel Blot <emmanuel.blot@free.fr>
|
||||||
#
|
|
||||||
# Copyright (c) 2010-2016 Emmanuel Blot <emmanuel.blot@free.fr>
|
|
||||||
# Copyright (c) 2010-2011 Neotion
|
# Copyright (c) 2010-2011 Neotion
|
||||||
#
|
#
|
||||||
# This library is free software; you can redistribute it and/or
|
# This library is free software; you can redistribute it and/or
|
||||||
@ -17,23 +15,24 @@
|
|||||||
# License along with this library; if not, write to the Free Software
|
# License along with this library; if not, write to the Free Software
|
||||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
|
||||||
from array import array
|
from configparser import ConfigParser, InterpolationSyntaxError
|
||||||
import commands
|
from logging import (DEBUG, INFO, ERROR, CRITICAL, WARNING,
|
||||||
import logging
|
Formatter, FileHandler, StreamHandler, getLogger)
|
||||||
import re
|
from logging.handlers import (BufferingHandler, NTEventLogHandler,
|
||||||
import socket
|
SysLogHandler)
|
||||||
import struct
|
from re import match
|
||||||
import sys
|
from socket import inet_aton, inet_ntoa
|
||||||
from ConfigParser import SafeConfigParser
|
from subprocess import run
|
||||||
from six import PY3, integer_types, binary_type
|
from struct import pack as spack, unpack as sunpack
|
||||||
|
from sys import platform, stderr
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import netifaces
|
import netifaces as nif
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import os
|
if platform == 'darwin':
|
||||||
if os.uname()[0].lower() == 'darwin':
|
|
||||||
raise ImportError('netifaces package is not installed')
|
raise ImportError('netifaces package is not installed')
|
||||||
netifaces = None
|
nif = None
|
||||||
|
|
||||||
|
|
||||||
# String values evaluated as true boolean values
|
# String values evaluated as true boolean values
|
||||||
TRUE_BOOLEANS = ['on', 'high', 'true', 'enable', 'enabled', 'yes', '1']
|
TRUE_BOOLEANS = ['on', 'high', 'true', 'enable', 'enabled', 'yes', '1']
|
||||||
@ -56,9 +55,9 @@ def to_int(value):
|
|||||||
"""
|
"""
|
||||||
if not value:
|
if not value:
|
||||||
return 0
|
return 0
|
||||||
if isinstance(value, integer_types):
|
if isinstance(value, int):
|
||||||
return int(value)
|
return int(value)
|
||||||
mo = re.match('^\s*(\d+)\s*(?:([KMkm]i?)?B?)?\s*$', value)
|
mo = match(r'^\s*(\d+)\s*(?:([KMkm]i?)?B?)?\s*$', value)
|
||||||
if mo:
|
if mo:
|
||||||
mult = {'K': (1000),
|
mult = {'K': (1000),
|
||||||
'KI': (1 << 10),
|
'KI': (1 << 10),
|
||||||
@ -108,7 +107,7 @@ def hexline(data, sep=' '):
|
|||||||
of the buffer data
|
of the buffer data
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if isinstance(data, (binary_type, array)):
|
if isinstance(data, bytes):
|
||||||
src = bytearray(data)
|
src = bytearray(data)
|
||||||
elif isinstance(data, bytearray):
|
elif isinstance(data, bytearray):
|
||||||
src = data
|
src = data
|
||||||
@ -128,20 +127,19 @@ def hexline(data, sep=' '):
|
|||||||
def logger_factory(logtype='syslog', logfile=None, level='WARNING',
|
def logger_factory(logtype='syslog', logfile=None, level='WARNING',
|
||||||
logid='PXEd', format=None):
|
logid='PXEd', format=None):
|
||||||
# this code has been copied from Trac (MIT modified license)
|
# this code has been copied from Trac (MIT modified license)
|
||||||
logger = logging.getLogger(logid)
|
logger = getLogger(logid)
|
||||||
logtype = logtype.lower()
|
logtype = logtype.lower()
|
||||||
if logtype == 'file':
|
if logtype == 'file':
|
||||||
hdlr = logging.FileHandler(logfile)
|
hdlr = FileHandler(logfile)
|
||||||
elif logtype in ('winlog', 'eventlog', 'nteventlog'):
|
elif logtype in ('winlog', 'eventlog', 'nteventlog'):
|
||||||
# Requires win32 extensions
|
# Requires win32 extensions
|
||||||
hdlr = logging.handlers.NTEventLogHandler(logid,
|
hdlr = NTEventLogHandler(logid, logtype='Application')
|
||||||
logtype='Application')
|
|
||||||
elif logtype in ('syslog', 'unix'):
|
elif logtype in ('syslog', 'unix'):
|
||||||
hdlr = logging.handlers.SysLogHandler('/dev/log')
|
hdlr = SysLogHandler('/dev/log')
|
||||||
elif logtype in ('stderr'):
|
elif logtype in ('stderr'):
|
||||||
hdlr = logging.StreamHandler(sys.stderr)
|
hdlr = StreamHandler(stderr)
|
||||||
else:
|
else:
|
||||||
hdlr = logging.handlers.BufferingHandler(0)
|
hdlr = BufferingHandler(0)
|
||||||
|
|
||||||
if not format:
|
if not format:
|
||||||
format = 'PXEd[%(module)s] %(levelname)s: %(message)s'
|
format = 'PXEd[%(module)s] %(levelname)s: %(message)s'
|
||||||
@ -152,45 +150,36 @@ def logger_factory(logtype='syslog', logfile=None, level='WARNING',
|
|||||||
datefmt = '%X'
|
datefmt = '%X'
|
||||||
level = level.upper()
|
level = level.upper()
|
||||||
if level in ('DEBUG', 'ALL'):
|
if level in ('DEBUG', 'ALL'):
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(DEBUG)
|
||||||
elif level == 'INFO':
|
elif level == 'INFO':
|
||||||
logger.setLevel(logging.INFO)
|
logger.setLevel(INFO)
|
||||||
elif level == 'ERROR':
|
elif level == 'ERROR':
|
||||||
logger.setLevel(logging.ERROR)
|
logger.setLevel(ERROR)
|
||||||
elif level == 'CRITICAL':
|
elif level == 'CRITICAL':
|
||||||
logger.setLevel(logging.CRITICAL)
|
logger.setLevel(CRITICAL)
|
||||||
else:
|
else:
|
||||||
logger.setLevel(logging.WARNING)
|
logger.setLevel(WARNING)
|
||||||
formatter = logging.Formatter(format, datefmt)
|
formatter = Formatter(format, datefmt)
|
||||||
hdlr.setFormatter(formatter)
|
hdlr.setFormatter(formatter)
|
||||||
logger.addHandler(hdlr)
|
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
|
return logger
|
||||||
|
|
||||||
|
|
||||||
def iptoint(ipstr):
|
def iptoint(ipstr):
|
||||||
return struct.unpack('!I', socket.inet_aton(ipstr))[0]
|
return sunpack('!I', inet_aton(ipstr))[0]
|
||||||
|
|
||||||
|
|
||||||
def inttoip(ipval):
|
def inttoip(ipval):
|
||||||
return socket.inet_ntoa(struct.pack('!I', ipval))
|
return inet_ntoa(spack('!I', ipval))
|
||||||
|
|
||||||
|
|
||||||
def _netifaces_get_iface_config(address):
|
def _netifaces_get_iface_config(address):
|
||||||
pool = iptoint(address)
|
pool = iptoint(address)
|
||||||
for iface in netifaces.interfaces():
|
for iface in nif.interfaces():
|
||||||
ifinfo = netifaces.ifaddresses(iface)
|
ifinfo = nif.ifaddresses(iface)
|
||||||
if netifaces.AF_INET not in ifinfo:
|
if nif.AF_INET not in ifinfo:
|
||||||
continue
|
continue
|
||||||
for inetinfo in netifaces.ifaddresses(iface)[netifaces.AF_INET]:
|
for inetinfo in nif.ifaddresses(iface)[nif.AF_INET]:
|
||||||
addr_s = inetinfo.get('addr')
|
addr_s = inetinfo.get('addr')
|
||||||
netmask_s = inetinfo.get('netmask')
|
netmask_s = inetinfo.get('netmask')
|
||||||
if addr_s is None or netmask_s is None:
|
if addr_s is None or netmask_s is None:
|
||||||
@ -213,7 +202,7 @@ def _netifaces_get_iface_config(address):
|
|||||||
def _iproute_get_iface_config(address):
|
def _iproute_get_iface_config(address):
|
||||||
pool = iptoint(address)
|
pool = iptoint(address)
|
||||||
iplines = (line.strip()
|
iplines = (line.strip()
|
||||||
for line in commands.getoutput("ip address show").split('\n'))
|
for line in run("ip address show").stdout.split('\n'))
|
||||||
iface = None
|
iface = None
|
||||||
for l in iplines:
|
for l in iplines:
|
||||||
items = l.split()
|
items = l.split()
|
||||||
@ -240,17 +229,55 @@ def _iproute_get_iface_config(address):
|
|||||||
def get_iface_config(address):
|
def get_iface_config(address):
|
||||||
if not address:
|
if not address:
|
||||||
return None
|
return None
|
||||||
if not netifaces:
|
nifcfg = _netifaces_get_iface_config if nif else _iproute_get_iface_config
|
||||||
return _iproute_get_iface_config(address)
|
return nifcfg(address)
|
||||||
return _netifaces_get_iface_config(address)
|
|
||||||
|
|
||||||
|
|
||||||
class EasyConfigParser(SafeConfigParser):
|
def is_quoted(str_):
|
||||||
"ConfigParser extension to support default config values"
|
"""Tells whether a string is enclosed in simple- or double- quoted
|
||||||
|
markers"""
|
||||||
|
str_ = str_.strip()
|
||||||
|
return (str_.startswith('"') and str_.endswith('"')) or \
|
||||||
|
(str_.startswith("'") and str_.endswith("'"))
|
||||||
|
|
||||||
def get(self, section, option, default=None):
|
|
||||||
|
class EasyConfigParser(ConfigParser):
|
||||||
|
"""ConfigParser extension to support default config values and do not
|
||||||
|
mess with multi-line option strings"""
|
||||||
|
|
||||||
|
INDENT_SIZE = 8
|
||||||
|
|
||||||
|
InterpolationSyntaxError = InterpolationSyntaxError
|
||||||
|
|
||||||
|
def get(self, section, option, default=None, raw=True, vars=None,
|
||||||
|
fallback=None):
|
||||||
|
"""Return the section:option value if it exists, or the default value
|
||||||
|
if either the section or the option is missing"""
|
||||||
if not self.has_section(section):
|
if not self.has_section(section):
|
||||||
return default
|
return default
|
||||||
if not self.has_option(section, option):
|
if not self.has_option(section, option):
|
||||||
return default
|
return default
|
||||||
return SafeConfigParser.get(self, section, option)
|
return ConfigParser.get(self, section, option, raw=raw, vars=vars,
|
||||||
|
fallback=fallback)
|
||||||
|
|
||||||
|
def write(self, filep):
|
||||||
|
"""Write an .ini-format representation of the configuration state,
|
||||||
|
with automatic line wrapping, using improved multi-line
|
||||||
|
representation.
|
||||||
|
"""
|
||||||
|
for section in self._sections:
|
||||||
|
filep.write("[%s]\n" % section)
|
||||||
|
for (key, value) in self._sections[section].items():
|
||||||
|
if key != "__name__":
|
||||||
|
filep.write("%s = %s\n" %
|
||||||
|
(key, str(value).replace('\n', '\n' +
|
||||||
|
' ' * self.INDENT_SIZE)))
|
||||||
|
filep.write("\n")
|
||||||
|
|
||||||
|
def _interpolate(self, section, option, rawval, vars):
|
||||||
|
# special overloading of SafeConfigParser._interpolate:
|
||||||
|
# do not attempt to interpolate if the string is (double-)quoted
|
||||||
|
if is_quoted(rawval):
|
||||||
|
return rawval
|
||||||
|
# cannot use 'super' here as ConfigParser is outdated
|
||||||
|
return ConfigParser._interpolate(self, section, option, rawval, vars)
|
||||||
|
20
setup.py
20
setup.py
@ -1,7 +1,6 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (c) 2010-2016 Emmanuel Blot <emmanuel.blot@free.fr>
|
# Copyright (c) 2010-2019 Emmanuel Blot <emmanuel.blot@free.fr>
|
||||||
# Copyright (c) 2010-2011 Neotion
|
# Copyright (c) 2010-2011 Neotion
|
||||||
#
|
#
|
||||||
# This library is free software; you can redistribute it and/or
|
# This library is free software; you can redistribute it and/or
|
||||||
@ -18,22 +17,23 @@
|
|||||||
# License along with this library; if not, write to the Free Software
|
# License along with this library; if not, write to the Free Software
|
||||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
|
||||||
import os
|
|
||||||
from distutils.core import setup
|
from distutils.core import setup
|
||||||
|
from os.path import dirname, join as joinpath
|
||||||
|
from sys import platform
|
||||||
|
from pybootd import __version__
|
||||||
|
|
||||||
def _read(fname):
|
def _read(fname):
|
||||||
return open(os.path.join(os.path.dirname(__file__), fname)).read()
|
return open(joinpath(dirname(__file__), fname)).read()
|
||||||
|
|
||||||
|
|
||||||
requirements = ['six']
|
requirements = []
|
||||||
if os.uname()[0].lower() == 'darwin':
|
if platform == 'darwin':
|
||||||
requirements.append('netifaces (>= 0.5)')
|
requirements.append('netifaces (>= 0.5)')
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='pybootd',
|
name='pybootd',
|
||||||
version='1.6.0',
|
version=__version__,
|
||||||
description='Simplified BOOTP/DHCP/PXE and TFTP server',
|
description='Simplified BOOTP/DHCP/PXE and TFTP server',
|
||||||
author='Emmanuel Blot',
|
author='Emmanuel Blot',
|
||||||
author_email='emmanuel.blot@free.fr',
|
author_email='emmanuel.blot@free.fr',
|
||||||
@ -52,7 +52,7 @@ setup(
|
|||||||
'Lesser General Public License (LGPL)',
|
'Lesser General Public License (LGPL)',
|
||||||
'Operating System :: MacOS :: MacOS X',
|
'Operating System :: MacOS :: MacOS X',
|
||||||
'Operating System :: POSIX',
|
'Operating System :: POSIX',
|
||||||
'Programming Language :: Python :: 2.7',
|
'Programming Language :: Python :: 3.5',
|
||||||
'Topic :: Internet',
|
'Topic :: Internet',
|
||||||
'Topic :: System :: Installation/Setup',
|
'Topic :: System :: Installation/Setup',
|
||||||
'Topic :: System :: Networking',
|
'Topic :: System :: Networking',
|
||||||
|
Loading…
Reference in New Issue
Block a user