Repository: Parsl/parsl

Issue:
IPP controller port_range option doesn't constraint all TCP ports needed by IPP 
Hello,

I tried using Parsl (0.6.1) on a condor cluster with TCP ports e.g.: 9100-9800 open using the IPyParallel executor with the [port_range](https://github.com/Parsl/parsl/blob/master/parsl/executors/ipp_controller.py#L28-L30) option.

However, while the JSON created does choose a random port for registration, it doesn't do the same for other TCP ports like the hubFactory control, mux, task, etc. Example in [1] (where `port_range='9100,9800'`).

It turns out  `ipcontroller` [allows](https://stackoverflow.com/questions/23145650/how-to-setup-ssh-tunnel-for-ipython-cluster-ipcluster) to define these parameters via `--HubFactory.<task, mux, control, etc>` , so I just created a controller that randomly selects all these ports from `port_range`, which seems to be working fine for me. It would be nice to have this feature into the IPP controller itself though.

https://github.com/vc3-project/vc3-playbooks/blob/master/login/jupyter_files/parsl/vc3controller.py#L52-L58  

```
[1]
cat <<EOF > ipengine.a77ce9a9-f742-41ce-afb4-e2b6c7058e78.json
{
  "ssh": "",
  "interface": "tcp://*",
  "registration": 9560,
  "control": 33229,
  "mux": 44249,
  "hb_ping": 41099,
  "hb_pong": 25087,
  "task": 43484,
  "iopub": 54747,
  "key": "808521cb-95a5a73d5c388767b23c668a",
  "location": "128.135.158.176",
  "pack": "json",
  "unpack": "json",
  "signature_scheme": "hmac-sha256"
}
```
 


Discussion:
@khurtado-- awesome sleuthing, thanks for figuring this out!! Since you are in a position to test this with your use case, is there any chance you'd be up for popping [these lines](https://github.com/vc3-project/vc3-playbooks/blob/master/login/jupyter_files/parsl/vc3controller.py#L52-L58) into the `Controller` and submitting a PR?

Resolving patch:
diff --git a/parsl/executors/ipp_controller.py b/parsl/executors/ipp_controller.py
--- a/parsl/executors/ipp_controller.py
+++ b/parsl/executors/ipp_controller.py
@@ -26,8 +26,9 @@ class Controller(RepresentationMixin):
         Port on which the iPython hub listens for registration. If set to `None`, the IPython default will be used. Default
         is None.
     port_range : str, optional
-        The minimum and maximum port value, in the format '<min>,<max>' (for example: '50000,60000'. If this does not equal
-        None, a random port in `port_range` will be selected.
+        The minimum and maximum port values to use, in the format '<min>,<max>' (for example: '50000,60000').
+        If this does not equal None, random ports in `port_range` will be selected for all HubFactory listening services.
+        This option overrides the port setting value for registration.
     reuse : bool, optional
         Reuse an existing controller.
     ipython_dir : str, optional
@@ -45,9 +46,17 @@ def __init__(self, public_ip=None, interfaces=None, port=None, port_range=None,
         self.interfaces = interfaces
         self.port = port
         self.port_range = port_range
+
         if port_range is not None:
             port_min, port_max = [int(x) for x in port_range.split(',')]
-            self.port = random.randint(port_min, port_max)
+            (
+                self.port,
+                self.hb_ping, self.hb_pong,
+                self.control_client, self.control_engine,
+                self.mux_client, self.mux_engine,
+                self.task_client, self.task_engine
+            ) = random.sample(range(port_min, port_max), 9)
+
         self.reuse = reuse
         self.log = log
         self.ipython_dir = ipython_dir
@@ -77,9 +86,16 @@ def start(self):
                 self.interfaces if self.interfaces is not None else '--ip=*',
                 '' if self.profile is 'default' else '--profile={0}'.format(self.profile),
                 '--reuse' if self.reuse else '',
-                '--port={}'.format(self.port) if self.port is not None else '',
-                '--location={}'.format(self.public_ip) if self.public_ip else ''
+                '--location={}'.format(self.public_ip) if self.public_ip else '',
+                '--port={}'.format(self.port) if self.port is not None else ''
             ]
+            if self.port_range is not None:
+                opts += [
+                    '--HubFactory.hb={0},{1}'.format(self.hb_ping, self.hb_pong),
+                    '--HubFactory.control={0},{1}'.format(self.control_client, self.control_engine),
+                    '--HubFactory.mux={0},{1}'.format(self.mux_client, self.mux_engine),
+                    '--HubFactory.task={0},{1}'.format(self.task_client, self.task_engine)
+                ]
             logger.debug("Starting ipcontroller with '{}'".format(' '.join([str(x) for x in opts])))
             self.proc = subprocess.Popen(opts, stdout=stdout, stderr=stderr, preexec_fn=os.setsid)
         except FileNotFoundError:


Test patch: