Repository: dask/distributed

Issue:
Ability to pin down all ports?
Folks, 

I am using the pip installable version of distributed and the nanny defaults to a random port, even if the --port option is used. This interferes with locked down environments where I need to have a known set of ports open for a service.

Could we have a "nanny-port"option on dworker?

Nathan



Discussion:
Yup, that makes sense.  I'll do this tomorrow and issue a bugfix release (was planning to anyway.)

Excellent!


Resolving patch:
diff --git a/distributed/cli/dworker.py b/distributed/cli/dworker.py
--- a/distributed/cli/dworker.py
+++ b/distributed/cli/dworker.py
@@ -18,10 +18,14 @@
 
 @click.command()
 @click.argument('center', type=str)
-@click.option('--port', type=int, default=0,
-              help="Serving port, defaults to randomly assigned")
+@click.option('--worker-port', type=int, default=0,
+              help="Serving worker port, defaults to randomly assigned")
 @click.option('--http-port', type=int, default=0,
               help="Serving http port, defaults to randomly assigned")
+@click.option('--nanny-port', type=int, default=0,
+              help="Serving nanny port, defaults to randomly assigned")
+@click.option('--port', type=int, default=0,
+              help="Deprecated, see --nanny-port")
 @click.option('--host', type=str, default=None,
               help="Serving host. Defaults to an ip address that can hopefully"
                    " be visible from the center network.")
@@ -31,7 +35,12 @@
               help="Number of worker processes.  Defaults to one.")
 @click.option('--name', type=str, default='', help="Alias")
 @click.option('--no-nanny', is_flag=True)
-def main(center, host, port, http_port, nthreads, nprocs, no_nanny, name):
+def main(center, host, worker_port, http_port, nanny_port, nthreads, nprocs,
+        no_nanny, name, port):
+    if port:
+        logger.info("--port is deprecated, use --nanny-port instead")
+        assert not nanny_port
+        nanny_port = port
     try:
         center_host, center_port = center.split(':')
         center_ip = socket.gethostbyname(center_host)
@@ -39,7 +48,7 @@ def main(center, host, port, http_port, nthreads, nprocs, no_nanny, name):
     except IndexError:
         logger.info("Usage:  dworker center_host:center_port")
 
-    if nprocs > 1 and port != 0:
+    if nprocs > 1 and worker_port != 0:
         logger.error("Failed to launch worker.  You cannot use the --port argument when nprocs > 1.")
         exit(1)
 
@@ -61,11 +70,12 @@ def main(center, host, port, http_port, nthreads, nprocs, no_nanny, name):
         # reach the center
         ip = get_ip(center_ip, center_port)
     nannies = [t(center_ip, center_port, ncores=nthreads, ip=ip,
-                 services=services, name=name, loop=loop)
+                 services=services, name=name, loop=loop,
+                 worker_port=worker_port)
                for i in range(nprocs)]
 
     for nanny in nannies:
-        nanny.start(port)
+        nanny.start(nanny_port)
     try:
         loop.start()
     except KeyboardInterrupt:
diff --git a/distributed/nanny.py b/distributed/nanny.py
--- a/distributed/nanny.py
+++ b/distributed/nanny.py
@@ -22,11 +22,12 @@ class Nanny(Server):
     The nanny spins up Worker processes, watches then, and kills or restarts
     them as necessary.
     """
-    def __init__(self, center_ip, center_port, ip=None,
+    def __init__(self, center_ip, center_port, ip=None, worker_port=0,
                 ncores=None, loop=None, local_dir=None, services=None,
                 name=None, **kwargs):
         self.ip = ip or get_ip()
         self.worker_port = None
+        self._given_worker_port = worker_port
         self.ncores = ncores
         self.local_dir = local_dir
         self.worker_dir = ''
@@ -102,8 +103,8 @@ def instantiate(self, stream=None):
         self.process = Process(target=run_worker,
                                args=(q, self.ip, self.center.ip,
                                      self.center.port, self.ncores,
-                                     self.port, self.local_dir, self.services,
-                                     self.name))
+                                     self.port, self._given_worker_port,
+                                     self.local_dir, self.services, self.name))
         self.process.daemon = True
         self.process.start()
         while True:
@@ -191,7 +192,7 @@ def monitor_resources(self, stream, interval=1):
 
 
 def run_worker(q, ip, center_ip, center_port, ncores, nanny_port,
-        local_dir, services, name):
+        worker_port, local_dir, services, name):
     """ Function run by the Nanny when creating the worker """
     from distributed import Worker  # pragma: no cover
     from tornado.ioloop import IOLoop  # pragma: no cover
@@ -205,7 +206,7 @@ def run_worker(q, ip, center_ip, center_port, ncores, nanny_port,
     @gen.coroutine  # pragma: no cover
     def start():
         try:  # pragma: no cover
-            yield worker._start()  # pragma: no cover
+            yield worker._start(worker_port)  # pragma: no cover
         except Exception as e:  # pragma: no cover
             logger.exception(e)  # pragma: no cover
             q.put(e)  # pragma: no cover
diff --git a/distributed/scheduler.py b/distributed/scheduler.py
--- a/distributed/scheduler.py
+++ b/distributed/scheduler.py
@@ -241,7 +241,8 @@ def identity(self, stream):
         d = {'type': type(self).__name__,
              'id': str(self.id),
              'workers': list(self.ncores),
-             'services': {key: v.port for (key, v) in self.services.items()}}
+             'services': {key: v.port for (key, v) in self.services.items()},
+             'workers': dict(self.worker_info)}
         return d
 
     def put(self, msg):


Test patch:
diff --git a/distributed/cli/tests/test_dworker.py b/distributed/cli/tests/test_dworker.py
new file mode 100644
--- /dev/null
+++ b/distributed/cli/tests/test_dworker.py
@@ -0,0 +1,38 @@
+from __future__ import print_function, division, absolute_import
+
+import requests
+from subprocess import Popen, PIPE
+from time import time, sleep
+
+from distributed import Scheduler, Executor
+from distributed.core import rpc
+from distributed.utils import sync, ignoring
+from distributed.utils_test import loop
+
+
+def test_nanny_worker_ports(loop):
+    try:
+        worker = Popen(['dworker', '127.0.0.1:8989', '--host', '127.0.0.1',
+                        '--worker-port', '8788', '--nanny-port', '8789'],
+                        stdout=PIPE, stderr=PIPE)
+        sched = Popen(['dscheduler', '--port', '8989'], stdout=PIPE, stderr=PIPE)
+        with Executor('127.0.0.1:8989', loop=loop) as e:
+            start = time()
+            while True:
+                d = sync(e.loop, e.scheduler.identity)
+                if d['workers']:
+                    break
+                else:
+                    assert time() - start < 5
+                    sleep(0.1)
+            assert d['workers']['127.0.0.1:8788']['services']['nanny'] == 8789
+    finally:
+        with ignoring(Exception):
+            w = rpc('127.0.0.1:8789')
+            sync(loop, w.terminate)
+
+        with ignoring(Exception):
+            sched.kill()
+
+        with ignoring(Exception):
+            worker.kill()