From 95745e00a2e1a47c6dec2be3d15d2a1b36e4f40c Mon Sep 17 00:00:00 2001 From: Sam Gleske Date: Fri, 1 Aug 2014 20:34:46 -0400 Subject: [PATCH 1/4] Allow --port range for random port selection This way the tracker listens on a new random port each time herd.py is called. This allows Herd to be integrated into a continuous delivery/continuous deployment environment. With port range socket is tested before port is used. pep8 cleanup on get_random_open_port function compared to Herd patch. --- README.md | 1 + horde/horde.py | 67 +++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c4f6eee..cf2cbdb 100644 --- a/README.md +++ b/README.md @@ -72,3 +72,4 @@ much more bueno. One would simply need to: * [Nate House](https://github.com/naterh) * [Russ Garrett](http://github.com/russss) * [Laurie Denness](http://github.com/lozzd) +* [Sam Gleske](http://github.com/samrocketman) diff --git a/horde/horde.py b/horde/horde.py index 55b1cef..a548663 100755 --- a/horde/horde.py +++ b/horde/horde.py @@ -1,6 +1,8 @@ #!/usr/bin/env python +import argparse import logging import os +import random import re import socket import subprocess @@ -8,8 +10,6 @@ import tempfile import threading import time - -import argparse import BitTornado.BT1.track as bttrack import BitTornado.BT1.makemetafile as makemetafile import murder_client as murder_client @@ -31,6 +31,63 @@ horde_py = os.path.join(horde_root, 'horde.py') +def get_random_open_port(port, interface=""): + """ + Arguments: + port - can be a single number or a number range with a hyphen as a string + interface - a string representing either a hostname in Internet domain notation or IP + address to test binding. By default a blank value means test a binding for all + interfaces. + Returns: + returns an int between 1 and 65535 based on input port + + If a single port is given as input then it simply returns the port + + If a port range is input then a random port will be returned if there is no + Listening socket for that port. + """ + regex=re.compile(r'[0-9]+-{0,1}[0-9]*') + #if port is an integer then convert to string + if isinstance(port, int): + port = str(port) + #Do some error checking on the input. + if not isinstance(port, str): + raise TypeError("port not of type str.") + if not regex.match(port): + raise ValueError("port str must be a number or a range of numbers between 1 and 65535.") + ports = sorted(map(lambda x: int(x), port.split('-'))) + #Do range checking on the input. + if len(ports) == 2 and ( ports[1] > 65535 ): + raise ValueError("port range is between 1 and 65535.") + elif ports[0] < 1 or ports[0] > 65535: + raise ValueError("port range is between 1 and 65535.") + #Return a port or a random range. + if len(ports) == 1: + #Since we're given a single port don't even try to test it. Just return the value. + return ports[0] + else: + port = random.randrange(ports[0],ports[1]+1) + count = 0 + found = False + while not found: + try: + #test the socket for a listening service + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.bind((interface, port)) + found = True + except socket.error,e: + #Generate another port to be tested because the last one had a listening socket. + port = random.randrange(ports[0], ports[1]+1) + s.close() + count += 1 + #avoid an infinite loop + if count > 10000: + break + if not found: + raise socket.error("could not obtain socket from port or port range.") + return port + + def run(local_file, remote_file, hosts): start = time.time() log.info("Spawning tracker...") @@ -144,7 +201,8 @@ def mktorrent(file_name, tracker): def track(): - bttrack.track(["--dfile", opts['data_file'], "--port", opts['port']]) + bttrack.track(["--dfile", opts['data_file'], "--port", + get_random_open_port(opts['port'])]) def seed(torrent, local_file): @@ -217,7 +275,8 @@ def entry_point(): parser.add_argument('--port', default=8998, - help="Port number to run the tracker on") + help="Port number to run the tracker on. Port range " + + "for random port selection also allowed (e.g. 8000-9000).") parser.add_argument('--remote-path', default='/tmp/horde', From 8a43b86634a4f688b4792b07cc170380afd8f924 Mon Sep 17 00:00:00 2001 From: Sam Gleske Date: Fri, 1 Aug 2014 21:39:28 -0400 Subject: [PATCH 2/4] Adding Sam Gleske as an author updating gitignore Updated gitignore so that paths created by setup.py commands are ignored. --- .gitignore | 3 +++ README.md | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c9b568f..dd620c2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ *.pyc *.swp +build +dist +horde.egg-info diff --git a/README.md b/README.md index cf2cbdb..7c2d1a2 100644 --- a/README.md +++ b/README.md @@ -72,4 +72,4 @@ much more bueno. One would simply need to: * [Nate House](https://github.com/naterh) * [Russ Garrett](http://github.com/russss) * [Laurie Denness](http://github.com/lozzd) -* [Sam Gleske](http://github.com/samrocketman) +* [Sam Gleske](https://github.com/samrocketman) From 9668620b9ddb7ff1448e213b1ce49c52dbcff1da Mon Sep 17 00:00:00 2001 From: Sam Gleske Date: Sat, 2 Aug 2014 20:11:36 -0400 Subject: [PATCH 3/4] bugfix random port range failing in murder client A random port selection from a port range has moved to being selected at the beginning of runtime. --- horde/horde.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/horde/horde.py b/horde/horde.py index a548663..6e0afbb 100755 --- a/horde/horde.py +++ b/horde/horde.py @@ -202,7 +202,7 @@ def mktorrent(file_name, tracker): def track(): bttrack.track(["--dfile", opts['data_file'], "--port", - get_random_open_port(opts['port'])]) + opts['port']]) def seed(torrent, local_file): @@ -245,7 +245,7 @@ def run_with_opts(local_file, remote_file, hosts='', retry=0, port=8998, opts['remote-file'] = remote_file opts['hosts'] = hosts opts['retry'] = retry - opts['port'] = port + opts['port'] = get_random_open_port(port) opts['remote_path'] = remote_path opts['data_file'] = data_file opts['log_dir'] = log_dir @@ -300,6 +300,9 @@ def entry_point(): opts = vars(parser.parse_args()) + #potentially select a random port + opts['port'] = get_random_open_port(opts['port']) + if opts['seed']: seed(opts['local-file'], opts['remote-file']) else: From f633ecd9fe645f5a1ab6f1b229794452b7f19ba6 Mon Sep 17 00:00:00 2001 From: Sam Gleske Date: Sun, 3 Aug 2014 13:56:48 -0400 Subject: [PATCH 4/4] bugfix disable port test socket release timeout Within function get_random_open_port(). The socket testing interfered with the actual tracker starting a listener. Since no communication happens over the opened socket, and we're just testing if it is free to bind, there should be no timeout for the socket to expire. --- horde/horde.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/horde/horde.py b/horde/horde.py index 6e0afbb..38a4d33 100755 --- a/horde/horde.py +++ b/horde/horde.py @@ -73,6 +73,11 @@ def get_random_open_port(port, interface=""): try: #test the socket for a listening service s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + #The SO_REUSEADDR flag tells the kernel to reuse a local socket in TIME_WAIT + #state, without waiting for its natural timeout to expire. This avoids + #an exception "OSError: [Errno 98] Address already in use" because we are + #only testing the binding of a socket and not using it for communication. + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind((interface, port)) found = True except socket.error,e: