OutpostSpawner

Contents

OutpostSpawner#

class outpostspawner.OutpostSpawner(*args: t.Any, **kwargs: t.Any)#

A JupyterHub spawner that spawns services on remote locations in combination with a JupyterHub Outpost service.

additional_cafile c.OutpostSpawner.additional_cafile = Any(None)#

Additional certificate authorities can be added. Required if JUPYTERHUB_API_URL is an external URL and c.JupyterHub.internal_ssl is True.

apply_user_options c.OutpostSpawner.apply_user_options = Union(None)#

Hook to apply inputs from user_options to the Spawner.

Typically takes values in user_options, validates them, and updates Spawner attributes:

def apply_user_options(spawner, user_options):
    if "image" in user_options and isinstance(user_options["image"], str):
        spawner.image = user_options["image"]

c.Spawner.apply_user_options = apply_user_options

apply_user_options may be async.

Default: do nothing.

Typically a callable which takes (spawner: Spawner, user_options: dict), but for simple cases this can be a dict mapping user option fields to Spawner attribute names, e.g.:

c.Spawner.apply_user_options = {"image_input": "image"}
c.Spawner.options_from_form = "simple"

allows users to specify the image attribute, but not any others. Because user_options generally comes in as strings in form data, the dictionary mode uses traitlets from_string to coerce strings to values, which allows setting simple values from strings (e.g. numbers) without needing to implement callable hooks.

Note

Because user_options is user input and may be set directly via the REST API, no assumptions should be made on its structure or contents. An empty dict should always be supported. Make sure to validate any inputs before applying them, either in this callable, or in whatever is consuming the value if this is a dict.

New in version 5.3: Prior to 5.3, applying user options must be done in Spawner.start() or Spawner.pre_spawn_hook().

cancelling_event c.OutpostSpawner.cancelling_event = Union({'failed': False, 'ready': False, 'progress': 99, 'message': '', 'html_message': 'JupyterLab is cancelling the start.'})#

Event shown when a singleuser server was cancelled. Can be a function or a dict.

This may be a coroutine.

Example:

from datetime import datetime
async def cancel_click_event(spawner):
    now = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
    return {
        "failed": False,
        "ready": False,
        "progress": 99,
        "message": "",
        "html_message": f"<details><summary>{now}: Cancelling start ...</summary>We're stopping the start process.</details>",
    }

c.ForwardBaseSpawner.cancelling_event = cancel_click_event
check_allowed c.OutpostSpawner.check_allowed = Any(None)#

An optional hook function you can implement to double check if the given user_options allow a start. If the start is not allowed, it should raise an exception.

This may be a coroutine.

Example:

def custom_check_allowed(spawner, user_options):
    if not user_options.get("allowed", True):
        raise Exception("This is not allowed")

c.OutpostSpawner.check_allowed = custom_check_allowed
collect_logs c.OutpostSpawner.collect_logs = Bool(False)#

Whether to collect logs when stopping the service.

collect_logs_polling c.OutpostSpawner.collect_logs_polling = Bool(False)#

Whether to collect logs when polling the service.

custom_env c.OutpostSpawner.custom_env = Union()#

An optional hook function, or dict, you can implement to add extra environment variables to send to the JupyterHub Outpost service.

This may be a coroutine.

Example:

async def custom_env(spawner, user_options, jupyterhub_api_url):
    system = user_options.get("system", "")
    env = {
        "JUPYTERHUB_STAGE": os.environ.get("JUPYTERHUB_STAGE", ""),
        "JUPYTERHUB_DOMAIN": os.environ.get("JUPYTERHUB_DOMAIN", ""),
        "JUPYTERHUB_OPTION1": user_options.get("option1", "")
    }
    if system:
        env["JUPYTERHUB_FLAVORS_UPDATE_URL"] = f"{jupyterhub_api_url.rstrip('/')}/outpostflavors/{system}"
    return env

c.OutpostSpawner.custom_env = custom_env
custom_misc c.OutpostSpawner.custom_misc = Union()#

An optional hook function, or dict, you can implement to add extra configurations to send to the JupyterHub Outpost service. This will override the Spawner configuration set at the Outpost. key can be anything you would normally use in your Spawner configuration: c.OutpostSpawner.<key> = <value>

This may be a coroutine.

Example:

async def custom_misc(spawner, user_options):
    return {
        "image": "jupyter/base-notebook:latest"
    }

c.OutpostSpawner.custom_misc = custom_misc

will override the image configured at the Outpost:

c.JupyterHubOutpost.spawner_class = KubeSpawner
c.KubeSpawner.image = "default_image:1.0"

and spawn a JupyterLab using the jupyter/base-notebook:latest image.

custom_misc_disable_default c.OutpostSpawner.custom_misc_disable_default = Bool(False)#

By default, these misc options will be send to the Outpost service to override the corresponding values of the Spawner configured at the Outpost. You can disable this behaviour by setting this value to true.

Default custom_misc options:

extra_labels = await self.get_extra_labels()
custom_misc.update({
  "dns_name_template": self.dns_name_template,
  "pod_name_template": self.svc_name_template,
  "internal_ssl": self.internal_ssl,
  "ip": "0.0.0.0",
  "port": self.port,
  "services_enabled": True,
  "extra_labels": extra_labels
}
custom_poll_interval c.OutpostSpawner.custom_poll_interval = Union(0)#

An optional hook function, or dict, you can implement to define the poll interval (in milliseconds). This allows you to have to different intervals for different Outpost services. You can use this to randomize the poll interval for each spawner object.

Example:

import random
def custom_poll_interval(spawner, user_options):
    system = user_options.get("system", "None")
    if system == "A":
        base_poll_interval = 30
        poll_interval_randomizer = 10
        poll_interval = 1e3 * base_poll_interval + random.randint(
            0, 1e3 * poll_interval_randomizer
        )
    else:
        poll_interval = 0
    return poll_interval

c.OutpostSpawner.custom_poll_interval = custom_poll_interval
custom_port c.OutpostSpawner.custom_port = Union(0)#

An optional hook function, or dict, you can implement to define a port depending on the spawner object.

Example:

from jupyterhub.utils import random_potr
def custom_port(spawner, user_options):
    if user_options.get("system", "") == "A":
        return 8080
    return random_port()

c.OutpostSpawner.custom_port = custom_port
custom_user_options c.OutpostSpawner.custom_user_options = Union()#

An optional hook function, or dict, you can implement to add extra user_options to send to the JupyterHub Outpost service.

This may be a coroutine.

Example:

async def custom_user_options(spawner, user_options):
    user_options["image"] = "jupyter/minimal-notebook:latest"
    return user_options

c.OutpostSpawner.custom_user_options = custom_user_options
dns_name_template c.OutpostSpawner.dns_name_template = Unicode('{name}.{namespace}.svc.cluster.local')#

Template to use to form the dns name for the pod.

extra_labels c.OutpostSpawner.extra_labels = Union()#

An optional hook function, or dict, you can implement to add extra labels to the service created when using port-forwarding. Will also be forwarded to the Outpost service (see self.custom_misc_disable_default)

This may be a coroutine.

Example:

def extra_labels(spawner):
    labels = {
        "hub.jupyter.org/username": spawner.user.name,
        "hub.jupyter.org/servername": spawner.name,
        "sidecar.istio.io/inject": "false"
    }
    return labels

c.ForwardBaseSpawner.extra_labels = extra_labels
failed_spawn_request_hook c.OutpostSpawner.failed_spawn_request_hook = Any(None)#

An optional hook function you can implement to handle a failed start attempt properly. This will be called if the POST request to the Outpost service was not successful.

This may be a coroutine.

Example:

def custom_failed_spawn_request_hook(Spawner, exception_thrown):
    ...
    return

c.OutpostSpawner.failed_spawn_request_hook = custom_failed_spawn_request_hook
filter_events c.OutpostSpawner.filter_events = Callable(None)#

Different JupyterHub single-user servers may send different events. This filter allows you to unify all events. Should always return a dict. If the dict should not be shown, return an empty dict.

Example:

def custom_filter_events(spawner, event):
    event["html_message"] = event.get("message", "No message available")
    return event

c.ForwardBaseSpawner.filter_events = custom_filter_events
group_overrides c.OutpostSpawner.group_overrides = Union()#

Override specific traitlets based on group membership of the user.

This can be a dict, or a callable that returns a dict. The keys of the dict are only used for lexicographical sorting, to guarantee consistent ordering of the overrides. If it is a callable, it may be async, and will be passed one parameter - the spawner instance. It should return a dictionary.

The values of the dict are dicts with the following keys:

  • "groups" - If the user belongs to any of these groups, these overrides are applied to their server before spawning.

  • "spawner_override" - a dictionary with overrides to apply to the Spawner settings. Each value can be either the final value to change or a callable that take the Spawner instance as parameter and returns the final value. If the traitlet being overriden is a dictionary, the dictionary will be recursively updated, rather than overriden. If you want to remove a key, set its value to None.

Example

The following example config will:

  1. Add the environment variable “AM_I_GROUP_ALPHA” to everyone in the “group-alpha” group

  2. Add the environment variable “AM_I_GROUP_BETA” to everyone in the “group-beta” group. If a user is part of both “group-beta” and “group-alpha”, they will get both these env vars, due to the dictionary merging functionality.

  3. Add a higher memory limit for everyone in the “group-beta” group.

c.Spawner.group_overrides = {
    "01-group-alpha-env-add": {
        "groups": ["group-alpha"],
        "spawner_override": {"environment": {"AM_I_GROUP_ALPHA": "yes"}},
    },
    "02-group-beta-env-add": {
        "groups": ["group-beta"],
        "spawner_override": {"environment": {"AM_I_GROUP_BETA": "yes"}},
    },
    "03-group-beta-mem-limit": {
        "groups": ["group-beta"],
        "spawner_override": {"mem_limit": "2G"}
    }
}
http_client_defaults c.OutpostSpawner.http_client_defaults = Dict()#

Default keyword arguments for the shared HTTP client used to communicate with the Outposts

namespace c.OutpostSpawner.namespace = Unicode('')#

Kubernetes namespace to create services in.

Default:

ns_path = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"
if os.path.exists(ns_path):
    with open(ns_path) as f:
        return f.read().strip()
return "default"
poll_jitter c.OutpostSpawner.poll_jitter = Float(0.1)#

Jitter fraction for poll_interval.

Avoids alignment of poll calls for many Spawners, e.g. when restarting JupyterHub, which restarts all polls for running Spawners.

poll_jitter=0 means no jitter, 0.1 means 10%, etc.

post_spawn_request_hook c.OutpostSpawner.post_spawn_request_hook = Any(None)#

An optional hook function you can implement to handle a successful start attempt properly. This will be called if the POST request to the Outpost service was successful.

This may be a coroutine.

Example:

def post_spawn_request_hook(Spawner, resp_json):
    ...
    return

c.OutpostSpawner.post_spawn_request_hook = post_spawn_request_hook
pre_poll_hook c.OutpostSpawner.pre_poll_hook = Any(False)#

Hook which allows to run a function before calling poll. Useful when you already know the answer of the upcoming poll call (e.g. information in auth_state is missing).

Used return values of the pre_poll_hook: Return True: Unknown status. Call self._poll() Return False: Unknown status. Do not call self._poll(). Server continues as running. Return Integer: That’s the exit code. Do not call self._poll() Return None: Server still running. Do not call self._poll()

Callable function, may be a coroutine.

pre_stop_hook c.OutpostSpawner.pre_stop_hook = Any(False)#

Hook which allows to run a function before calling stop.

Callable function, may be a coroutine.

progress_ready_hook c.OutpostSpawner.progress_ready_hook = Any(None)#

An optional hook function that you can implement to modify the ready event, which will be shown to the user on the spawn progress page when their server is ready.

This can be set independent of any concrete spawner implementation.

This maybe a coroutine.

Example:

async def my_ready_hook(spawner, ready_event):
    ready_event["html_message"] = f"Server {spawner.name} is ready for {spawner.user.name}"
    return ready_event

c.Spawner.progress_ready_hook = my_ready_hook
public_api_url c.OutpostSpawner.public_api_url = Any(None)#

Singleuser servers started remotely may have to use a different api_url than the default internal one. This will overwrite JUPYTERHUB_API_URL in env. Default value is the default internal JUPYTERHUB_API_URL

request_404_poll_keep_running c.OutpostSpawner.request_404_poll_keep_running = Bool(False)#

How to handle a 404 response from Outpost API during a singleuser poll request.

request_failed_poll_keep_running c.OutpostSpawner.request_failed_poll_keep_running = Bool(True)#

How to handle a failed request to Outpost API during a singleuser poll request.

request_headers c.OutpostSpawner.request_headers = Union()#

An optional hook function, or dict, you can implement to define the header used for all requests sent to the JupyterHub Outpost service. They are forwarded directly to the tornado.httpclient.HTTPRequest object.

Example:

def request_headers(spawner, user_options):
    if user_options.get("system", "") == "A":
        auth = os.environ.get("SYSTEM_A_AUTHENTICATION")
    else:
        auth = os.environ.get("SYSTEM_B_AUTHENTICATION")
    return {
        "Content-Type": "application/json",
        "Accept": "application/json",
        "Authorization": f"Basic {auth}"
    }

c.OutpostSpawner.request_headers = request_headers
request_kwargs c.OutpostSpawner.request_kwargs = Union({})#

An optional hook function, or dict, you can implement to define keyword arguments for all requests sent to the JupyterHub Outpost service. They are directly forwarded to the tornado.httpclient.HTTPRequest object.

Example:

def request_kwargs(spawner, user_options):
    return {
        "request_timeout": 30,
        "connect_timeout": 10,
        "ca_certs": ...,
        "validate_cert": ...,
    }

c.OutpostSpawner.request_kwargs = request_kwargs
request_kwargs_start c.OutpostSpawner.request_kwargs_start = Union(None)#

An optional hook function, or dict, you can implement to define keyword arguments for the start request sent to the JupyterHub Outpost service. They are directly forwarded to the tornado.httpclient.HTTPRequest object. If not defined, request_kwargs will be used instead. Example:

def request_kwargs(spawner, user_options):
    return {
        "request_timeout": 30,
        "connect_timeout": 10,
        "ca_certs": ...,
        "validate_cert": ...,
    }

c.OutpostSpawner.request_kwargs = request_kwargs
request_url c.OutpostSpawner.request_url = Union()#

The URL used to communicate with the JupyterHub Outpost service.

This may be a coroutine.

Example:

def request_url(spawner, user_options):
    if user_options.get("system", "") == "A":
        return "http://outpost.namespace.svc:8080/services/"
    else:
        return "https://remote-outpost.com/services/"

c.OutpostSpawner.request_url = request_url
show_first_default_event c.OutpostSpawner.show_first_default_event = Any(True)#

Hook to define if the default event at 0% should be shown.

Can be a boolean or a callable function. This may be a coroutine.

ssh_create_remote_forward c.OutpostSpawner.ssh_create_remote_forward = Any(False)#

Whether a port forwarding process from a remote system to the hub is required or not. The remote system must be prepared properly to support this feature.

Must be a boolean or a callable function

ssh_custom_forward c.OutpostSpawner.ssh_custom_forward = Any(None)#

An optional hook function you can implement to create your own ssh port forwarding called in the start function. This can be used to use an external pod for the port forwarding instead of having JupyterHub handle it.

Example:

from tornado.httpclient import HTTPRequest
def ssh_custom_forward(spawner, port_forward_info):
    url = "..."
    headers = {
        ...
    }
    req = HTTPRequest(
        url=url,
        method="POST",
        headers=headers,
        body=json.dumps(port_forward_info),
    )
    await spawner.send_request(
        req, action="setuptunnel"
    )

c.ForwardBaseSpawner.ssh_custom_forward = ssh_custom_forward
ssh_custom_forward_remote c.OutpostSpawner.ssh_custom_forward_remote = Any(None)#

An optional hook function you can implement to create your own ssh port forwarding from remote system to hub.

ssh_custom_forward_remote_remove c.OutpostSpawner.ssh_custom_forward_remote_remove = Any(None)#

An optional hook function you can implement to remove your own ssh port forwarding from remote system to hub.

ssh_custom_forward_remove c.OutpostSpawner.ssh_custom_forward_remove = Any(None)#

An optional hook function you can implement to remove your own ssh port forwarding called in the stop function. This can be used to use an external pod for the port forwarding instead of having JupyterHub handle it.

Example:

from tornado.httpclient import HTTPRequest
def ssh_custom_forward_remove(spawner, port_forward_info):
    url = "..."
    headers = {
        ...
    }
    req = HTTPRequest(
        url=url,
        method="DELETE",
        headers=headers,
        body=json.dumps(port_forward_info),
    )
    await spawner.send_request(
        req, action="removetunnel"
    )

c.ForwardBaseSpawner.ssh_custom_forward_remove = ssh_custom_forward_remove
ssh_custom_svc c.OutpostSpawner.ssh_custom_svc = Any(None)#

An optional hook function you can implement to create a customized kubernetes svc called in the start function.

Example:

def ssh_custom_svc(spawner, port_forward_info):
    ...
    return spawner.pod_name, spawner.port

c.ForwardBaseSpawner.ssh_custom_svc = ssh_custom_svc
ssh_custom_svc_remove c.OutpostSpawner.ssh_custom_svc_remove = Any(None)#

An optional hook function you can implement to remove a customized kubernetes svc called in the stop function.

Example:

def ssh_custom_svc_remove(spawner, port_forward_info):
    ...
    return spawner.pod_name, spawner.port

c.ForwardBaseSpawner.ssh_custom_svc_remove = ssh_custom_svc_remove
ssh_during_startup c.OutpostSpawner.ssh_during_startup = Union(False)#

An optional hook function, or boolean, you can implement to decide whether a ssh port forwarding process should be run after the POST request to the JupyterHub Outpost service.

Common Use Case: singleuser service was started remotely and is not accessible by JupyterHub (e.g. it’s running on a different K8s Cluster), but you know exactly where it is (e.g. the service address).

Example:

def ssh_during_startup(spawner):
    if spawner.user_options.get("system", "") == "A":
        return True
    return False

c.ForwardBaseSpawner.ssh_during_startup = ssh_during_startup
ssh_forward_options c.OutpostSpawner.ssh_forward_options = Union()#

An optional hook, or dict, to configure the ssh commands used in the spawner.ssh_default_forward function. The default configuration parameters (see below) can be overridden.

Default:

ssh_forward_options_all = {
    "ServerAliveInterval": "15",
    "StrictHostKeyChecking": "accept-new",
    "ControlMaster": "auto",
    "ControlPersist": "yes",
    "Port": str(ssh_port),
    "ControlPath": f"/tmp/control_{ssh_address_or_host}",
    "IdentityFile": ssh_pkey,
}
ssh_forward_remote_options c.OutpostSpawner.ssh_forward_remote_options = Union()#

An optional hook, or dict, to configure the ssh commands used in the spawner.ssh_default_forward function. The default configuration parameters (see below) can be overriden.

Default:

ssh_forward_remote_options_all = {
    "StrictHostKeyChecking": "accept-new",
    "Port": str(ssh_port),
    "ControlPath": f"/tmp/control_{ssh_address_or_host}",
}
ssh_key c.OutpostSpawner.ssh_key = Union('/home/jovyan/.ssh/id_rsa')#

An optional hook function, or string, you can implement to set the ssh privatekey used for ssh port forwarding.

This may be a coroutine.

Example:

def ssh_key(spawner):
    if spawner.user_options.get("system", "") == "A":
        return "/mnt/private_keys/a"
    return "/mnt/private_keys/b"

c.ForwardBaseSpawner.ssh_key = ssh_key
ssh_node c.OutpostSpawner.ssh_node = Union(None)#

An optional hook function, or string, you can implement to set the ssh node used for ssh port forwarding.

This may be a coroutine.

Example:

def ssh_node(spawner):
    if spawner.user_options.get("system", "") == "A":
        return "outpost.namespace.svc"
    else:
        return "<public_ip>"

c.ForwardBaseSpawner.ssh_node = ssh_node
ssh_node_mapping c.OutpostSpawner.ssh_node_mapping = Callable(None)#

An optional hook function, you can implement to set the map the given ssh node to a different avlue.

This may be a coroutine.

Example:

def ssh_node_mapping(spawner, ssh_node):
    if ssh_node == "<internal_hostname>":
        return "<external_dns_name>"
    return ssh_node

c.ForwardBaseSpawner.ssh_node_mapping = ssh_node_mapping
ssh_port c.OutpostSpawner.ssh_port = Union(22)#

An optional hook function, or string, you can implement to set the ssh port used for ssh port forwarding.

This may be a coroutine.

Example:

def ssh_port(spawner):
    if spawner.user_options.get("system", "") == "A":
        return 22
    else:
        return 2222

c.ForwardBaseSpawner.ssh_port = ssh_port
ssh_recreate_at_start c.OutpostSpawner.ssh_recreate_at_start = Union(False)#

Whether ssh tunnels should be recreated when JupyterHub starts or not. If you have outsourced the port forwarding to an extra pod, you can set this to false. Outsourcing also means, that connections to running JupyterLabs are not affected by JupyterHub restarts.

This may be a coroutine.

ssh_remote_key c.OutpostSpawner.ssh_remote_key = Union('/home/jovyan/.ssh/id_rsa_remote')#

An optional hook function, or string, you can implement to set the ssh privatekey used for ssh port forwarding remote.

This may be a coroutine.

Example:

def ssh_remote_key(spawner):
    if spawner.user_options.get("system", "") == "A":
        return "/mnt/private_keys/a"
    return "/mnt/private_keys/b"

c.ForwardBaseSpawner.ssh_remote_key = ssh_remote_key
ssh_remote_node c.OutpostSpawner.ssh_remote_node = Union(None)#

An optional hook function, or string, you can implement to set the ssh node used for ssh port forwarding remote.

This may be a coroutine.

Example:

def ssh_node(spawner):
    if spawner.user_options.get("system", "") == "A":
        return "outpost.namespace.svc"
    else:
        return "<public_ip>"

c.ForwardBaseSpawner.ssh_remote_node = ssh_node
ssh_remote_port c.OutpostSpawner.ssh_remote_port = Union(22)#

An optional hook function, or string, you can implement to set the ssh port used for ssh port forwarding remote.

This may be a coroutine.

Example:

def ssh_port(spawner):
    if spawner.user_options.get("system", "") == "A":
        return 22
    else:
        return 2222

c.ForwardBaseSpawner.ssh_remote_port = ssh_port
ssh_remote_username c.OutpostSpawner.ssh_remote_username = Union('jhuboutpost')#

An optional hook function, or string, you can implement to set the ssh username used for ssh port forwarding remote.

This may be a coroutine.

Example:

def ssh_username(spawner):
    if spawner.user_options.get("system", "") == "A":
        return "jhuboutpost"
    return "ubuntu"

c.ForwardBaseSpawner.ssh_remote_username = ssh_username
ssh_username c.OutpostSpawner.ssh_username = Union('jhuboutpost')#

An optional hook function, or string, you can implement to set the ssh username used for ssh port forwarding.

This may be a coroutine.

Example:

def ssh_username(spawner):
    if spawner.user_options.get("system", "") == "A":
        return "jhuboutpost"
    return "ubuntu"

c.ForwardBaseSpawner.ssh_username = ssh_username
start_async c.OutpostSpawner.start_async = Union(False)#

Whether the start at the Outpost service should run in the background or not. Can be a boolean or a function.

May be a coroutine.

stop_async c.OutpostSpawner.stop_async = Union(False)#

Whether the stop at the Outpost service should run in the background or not. Can be a boolean or a function.

May be a coroutine.

svc_create c.OutpostSpawner.svc_create = Union(True)#

An optional hook function, or boolean, you can implement to disable the svc creation.

This may be a coroutine.

Example:

async def svc_create(spawner):
    if spawner.user_options.get("system", "") == "A":
        return False
    else:
        return True

c.ForwardBaseSpawner.svc_create = svc_create
svc_name_template c.OutpostSpawner.svc_name_template = Unicode('jupyter-{username}--{servername}')#

Template to use to form the name of user’s pods.

{username}, {userid}, {servername}, {hubnamespace}, {unescaped_username}, and {unescaped_servername} will be expanded if found within strings of this configuration. The username and servername come escaped to follow the DNS label standard.

Trailing - characters are stripped for safe handling of empty server names (user default servers).

This must be unique within the namespace the pods are being spawned in, so if you are running multiple jupyterhubs spawning in the same namespace, consider setting this to be something more unique.

update_expected_path c.OutpostSpawner.update_expected_path = Any(False)#

Hook which allows to update the return value of Spawner.start().after starting ssh forwards. Result used by JupyterHub to look for Jupyter Server

Callable function, may be a coroutine.

update_start_response c.OutpostSpawner.update_start_response = Any(False)#

Hook which allows to update the return value of Spawner.start() before starting ssh forwards..

Callable function, may be a coroutine.