tools: port nfd-status-http-server to python3
Also cleanup tools/wscript
Change-Id: Iedb9d0ef3c63ad5ef7350381f0b5dfe1681f82b0
diff --git a/tools/nfd-status-http-server.py b/tools/nfd-status-http-server.py
index 7d5f1b8..5f2c891 100755
--- a/tools/nfd-status-http-server.py
+++ b/tools/nfd-status-http-server.py
@@ -1,8 +1,8 @@
-#!/usr/bin/env python2.7
+#!/usr/bin/env python3
# -*- Mode: python; py-indent-offset: 4; indent-tabs-mode: nil; coding: utf-8; -*-
"""
-Copyright (c) 2014-2016, Regents of the University of California,
+Copyright (c) 2014-2018, Regents of the University of California,
Arizona Board of Regents,
Colorado State University,
University Pierre & Marie Curie, Sorbonne University,
@@ -25,165 +25,112 @@
NFD, e.g., in COPYING.md file. If not, see <http://www.gnu.org/licenses/>.
"""
-from BaseHTTPServer import HTTPServer
-from SimpleHTTPServer import SimpleHTTPRequestHandler
-from SocketServer import ThreadingMixIn
-import sys
-import subprocess
-import urlparse
-import logging
-import argparse
-import socket
-import os
+from http.server import HTTPServer, SimpleHTTPRequestHandler
+from socketserver import ThreadingMixIn
+import argparse, ipaddress, os, socket, subprocess
-class StatusHandler(SimpleHTTPRequestHandler):
- """ The handler class to handle requests."""
+class NfdStatusHandler(SimpleHTTPRequestHandler):
+ """ The handler class to handle requests """
def do_GET(self):
- # get the url info to decide how to respond
- parsedPath = urlparse.urlparse(self.path)
- if parsedPath.path == "/":
- # get current nfd status, and use it as result message
- (httpCode, contentType, body) = self.getNfdStatus()
- self.send_response(httpCode)
- self.send_header("Content-Type", contentType)
- self.end_headers()
- self.wfile.write(body)
- elif parsedPath.path == "/robots.txt" and self.server.robots == True:
- self.send_response(200)
- self.send_header("Content-Type", "text/plain")
- self.end_headers()
+ if self.path == "/":
+ self.__serveReport()
+ elif self.path == "/robots.txt" and self.server.allowRobots:
+ self.send_error(404)
else:
- SimpleHTTPRequestHandler.do_GET(self)
+ super().do_GET()
- def log_message(self, format, *args):
- if self.server.verbose:
- logging.info("%s - %s\n" % (self.address_string(), format % args))
-
- def makeErrorResponseHtml(self, text):
- return '<!DOCTYPE html><title>NFD status</title><p>%s</p>' % text
-
- def getNfdStatus(self):
- """ Obtain XML-formatted NFD status report """
+ def __serveReport(self):
+ """ Obtain XML-formatted NFD status report and send it back as response body """
try:
- sp = subprocess.Popen(['nfdc', 'status', 'report', 'xml'], stdout=subprocess.PIPE, close_fds=True)
- output = sp.communicate()[0]
- except OSError as e:
- self.log_message('error invoking nfdc: %s', e)
- html = self.makeErrorResponseHtml('Internal error')
- return (500, "text/html; charset=UTF-8", html)
-
- if sp.returncode == 0:
+ # enable universal_newlines to get output as string rather than byte sequence
+ output = subprocess.check_output(["nfdc", "status", "report", "xml"], universal_newlines=True)
+ except OSError as err:
+ super().log_message("error invoking nfdc: {}".format(err))
+ self.send_error(500)
+ except subprocess.CalledProcessError as err:
+ super().log_message("error invoking nfdc: command exited with status {}".format(err.returncode))
+ self.send_error(504, "Cannot connect to NFD (code {})".format(err.returncode))
+ else:
# add stylesheet processing instruction after the XML document type declaration
- pos = output.index('>') + 1
+ # (yes, this is a ugly hack)
+ pos = output.index(">") + 1
xml = output[:pos]\
+ '<?xml-stylesheet type="text/xsl" href="nfd-status.xsl"?>'\
+ output[pos:]
- return (200, 'text/xml; charset=UTF-8', xml)
- else:
- html = self.makeErrorResponseHtml('Cannot connect to NFD, Code = %d' % sp.returncode)
- return (504, "text/html; charset=UTF-8", html)
+ self.send_response(200)
+ self.send_header("Content-Type", "text/xml; charset=UTF-8")
+ self.end_headers()
+ self.wfile.write(xml.encode())
-class ThreadHttpServer(ThreadingMixIn, HTTPServer):
+ # override
+ def log_message(self, *args):
+ if self.server.verbose:
+ super().log_message(*args)
+
+
+class ThreadingHttpServer(ThreadingMixIn, HTTPServer):
""" Handle requests using threads """
- def __init__(self, server, handler, verbose=False, robots=False):
- serverAddr = server[0]
- # socket.AF_UNSPEC is not supported, check whether it is v6 or v4
- ipType = self.getIpType(serverAddr)
- if ipType == socket.AF_INET6:
+ def __init__(self, bindAddr, port, handler, allowRobots=False, verbose=False):
+ # socketserver.BaseServer defaults to AF_INET even if you provide an IPv6 address
+ # see https://bugs.python.org/issue20215 and https://bugs.python.org/issue24209
+ if bindAddr.version == 6:
self.address_family = socket.AF_INET6
- elif ipType == socket.AF_INET:
- self.address_family == socket.AF_INET
- else:
- logging.error("The input IP address is neither IPv6 nor IPv4")
- sys.exit(2)
-
- try:
- HTTPServer.__init__(self, server, handler)
- except Exception as e:
- logging.error(str(e))
- sys.exit(2)
+ self.allowRobots = allowRobots
self.verbose = verbose
- self.robots = robots
+ super().__init__((str(bindAddr), port), handler)
- def getIpType(self, ipAddr):
- """ Get ipAddr's address type """
- # if ipAddr is an IPv6 addr, return AF_INET6
+
+def main():
+ def ipAddress(arg):
+ """ Validate IP address """
try:
- socket.inet_pton(socket.AF_INET6, ipAddr)
- return socket.AF_INET6
- except socket.error:
- pass
- # if ipAddr is an IPv4 addr return AF_INET, if not, return None
+ value = ipaddress.ip_address(arg)
+ except ValueError:
+ raise argparse.ArgumentTypeError("{!r} is not a valid IP address".format(arg))
+ return value
+
+ def portNumber(arg):
+ """ Validate port number """
try:
- socket.inet_pton(socket.AF_INET, ipAddr)
- return socket.AF_INET
- except socket.error:
- return None
+ value = int(arg)
+ except ValueError:
+ value = -1
+ if value < 0 or value > 65535:
+ raise argparse.ArgumentTypeError("{!r} is not a valid port number".format(arg))
+ return value
+ parser = argparse.ArgumentParser(description="Serves NFD status page via HTTP")
+ parser.add_argument("-V", "--version", action="version", version="@VERSION@")
+ parser.add_argument("-a", "--address", default="127.0.0.1", type=ipAddress, metavar="ADDR",
+ help="bind to this IP address (default: %(default)s)")
+ parser.add_argument("-p", "--port", default=8080, type=portNumber,
+ help="bind to this port number (default: %(default)s)")
+ parser.add_argument("-f", "--workdir", default="@DATAROOTDIR@/ndn", metavar="DIR",
+ help="server's working directory (default: %(default)s)")
+ parser.add_argument("-r", "--robots", action="store_true",
+ help="allow crawlers and other HTTP bots")
+ parser.add_argument("-v", "--verbose", action="store_true",
+ help="turn on verbose logging")
+ args = parser.parse_args()
-# main function to start
-def httpServer():
- parser = argparse.ArgumentParser()
- parser.add_argument("-p", type=int, metavar="port number",
- help="Specify the HTTP server port number, default is 8080.",
- dest="port", default=8080)
- # if address is not specified, use 127.0.0.1
- parser.add_argument("-a", default="127.0.0.1", metavar="IP address", dest="addr",
- help="Specify the HTTP server IP address.")
- parser.add_argument("-r", default=False, dest="robots", action="store_true",
- help="Enable HTTP robots to crawl; disabled by default.")
- parser.add_argument("-f", default="@DATAROOTDIR@/ndn", metavar="Server Directory", dest="serverDir",
- help="Specify the working directory of nfd-status-http-server, default is @DATAROOTDIR@/ndn.")
- parser.add_argument("-v", default=False, dest="verbose", action="store_true",
- help="Verbose mode.")
- parser.add_argument("--version", default=False, dest="version", action="store_true",
- help="Show version and exit")
+ os.chdir(args.workdir)
- args = vars(parser.parse_args())
+ httpd = ThreadingHttpServer(args.address, args.port, NfdStatusHandler,
+ allowRobots=args.robots, verbose=args.verbose)
- if args['version']:
- print "@VERSION@"
- return
-
- localPort = args["port"]
- localAddr = args["addr"]
- verbose = args["verbose"]
- robots = args["robots"]
- serverDirectory = args["serverDir"]
-
- os.chdir(serverDirectory)
-
- # setting log message format
- logging.basicConfig(format='%(asctime)s [%(levelname)s] %(message)s',
- level=logging.INFO)
-
- # if port is invalid, exit
- if localPort <= 0 or localPort > 65535:
- logging.error("Specified port number is invalid")
- sys.exit(2)
-
- httpd = ThreadHttpServer((localAddr, localPort), StatusHandler,
- verbose, robots)
- httpServerAddr = ""
if httpd.address_family == socket.AF_INET6:
- httpServerAddr = "http://[%s]:%s" % (httpd.server_address[0],
- httpd.server_address[1])
+ url = "http://[{}]:{}"
else:
- httpServerAddr = "http://%s:%s" % (httpd.server_address[0],
- httpd.server_address[1])
-
- logging.info("Server started - at %s" % httpServerAddr)
+ url = "http://{}:{}"
+ print("Server started at", url.format(*httpd.server_address))
try:
httpd.serve_forever()
except KeyboardInterrupt:
pass
-
httpd.server_close()
- logging.info("Server stopped")
-
-if __name__ == '__main__':
- httpServer()
+if __name__ == "__main__":
+ main()
diff --git a/tools/wscript b/tools/wscript
index f3dd5c9..de4cb78 100644
--- a/tools/wscript
+++ b/tools/wscript
@@ -4,68 +4,56 @@
top = '..'
-TOOLS_DEPENDENCY = 'core-objects NDN_CXX BOOST LIBRESOLV'
-
def build(bld):
- # single object tools
- # tools/example-tool.cpp should a self-contained tool with a main function
- # and it's built into build/bin/example-tool.
+ TOOLS_DEPENDENCY = 'core-objects NDN_CXX BOOST LIBRESOLV'
+
+ # Single object tools:
+ # tools/example-tool.cpp is a self-contained tool with a main() function
+ # and is built as build/bin/example-tool.
# These tools cannot be unit-tested.
for i in bld.path.ant_glob(['*.cpp']):
- name = str(i)[:-len(".cpp")]
- bld(features='cxx cxxprogram',
- target="../bin/%s" % name,
- source=[i],
- use=TOOLS_DEPENDENCY
- )
+ name = str(i)[:-len('.cpp')]
+ bld.program(target='../bin/%s' % name,
+ source=[i],
+ use=TOOLS_DEPENDENCY)
-
- # sub-directory tools
- # tools/example-tool/**/*.cpp is compiled and linked into build/bin/example-tool
- # tools/example-tool/main.cpp must exist and it should contain the main function.
+ # Sub-directory tools:
+ # tools/example-tool/**/*.cpp is compiled and linked into build/bin/example-tool.
+ # tools/example-tool/main.cpp must exist and must contain the main() function.
# All other objects are collected into 'tools-objects' and can be unit-tested.
testableObjects = []
for name in bld.path.ant_glob(['*'], dir=True, src=False):
mainFile = bld.path.find_node(['%s/main.cpp' % name])
if mainFile is None:
- continue # not a C++ tool
+ continue # not a C++ tool
srcFiles = bld.path.ant_glob(['%s/**/*.cpp' % name], excl=['%s/main.cpp' % name])
- if len(srcFiles) > 0:
+ if srcFiles:
srcObjects = 'tools-%s-objects' % name
- bld(features='cxx',
- name=srcObjects,
- source=srcFiles,
- use=TOOLS_DEPENDENCY,
- includes='%s' % name,
- )
+ bld.objects(target=srcObjects,
+ source=srcFiles,
+ use=TOOLS_DEPENDENCY,
+ includes='%s' % name)
testableObjects.append(srcObjects)
-
- bld(features='cxx cxxprogram',
- target="../bin/%s" % name,
- source=[mainFile],
- use=TOOLS_DEPENDENCY + ' ' + srcObjects,
- includes='%s' % name,
- )
else:
- bld(features='cxx cxxprogram',
- target="../bin/%s" % name,
- source=[mainFile],
- use=TOOLS_DEPENDENCY,
- includes='%s' % name,
- )
+ srcObjects = ''
- bld(name='tools-objects',
+ bld.program(target='../bin/%s' % name,
+ source=[mainFile],
+ use=TOOLS_DEPENDENCY + ' ' + srcObjects,
+ includes='%s' % name)
+
+ bld(target='tools-objects',
export_includes='.',
use=testableObjects)
- bld(features="subst",
- source=bld.path.ant_glob(['*.sh', '*.py']),
- target=['../bin/%s' % node.change_ext('')
- for node in bld.path.ant_glob(['*.sh', '*.py'])],
- install_path="${BINDIR}",
+ scripts = bld.path.ant_glob(['*.sh', '*.py'])
+ bld(features='subst',
+ name='scripts',
+ target=['../bin/%s' % node.change_ext('') for node in scripts],
+ source=scripts,
+ install_path='${BINDIR}',
chmod=Utils.O755,
- VERSION=Context.g_module.VERSION
- )
+ VERSION=Context.g_module.VERSION)
- bld.install_files("${DATAROOTDIR}/ndn",
+ bld.install_files('${DATAROOTDIR}/ndn',
bld.path.ant_glob('nfd-status-http-server-files/*'))