Repository: Parsl/parsl (language: python)

Issue:
Remove channels
# Overview

Parsl has a facility called Channels intended to support use cases such as running a workflow on your laptop but with task execution on a supercomputer. This facility never evolved beyond prototype stage, and effort from University of Chicago and University of Illinois was rapidly distracted into the funcX project which aims (with substantially more code and developer time) to provide roughly the same facility - now named Globus Compute and developed by professional programmers rather than an academic research team.

The presence of channels as quasi-abandonware inside the modern Parsl codebase is a painful drain on both user and developer time: Users continue to be fooled into believing that this abandoned prototype facility, rather than the results of the funcX project, should be used for remote execution use cases. Developer time continues to be taken up dealing with the intricate lacing of channel handling through the core codebase.

Channels should be removed from Parsl, with the default behaviour of `LocalChannel` becoming the only behaviour.

# Proposed timeline

I propose this timeline as a default, unless a consensus forms for some other timeline. In the absence of any other consensus forming in the comments of this issue, I'll make this timeline happen.


now .. 7th August 2024: people paying attention to issues get to comment on this issue, including offering entirely different alternative paths forward
7th August 2024: channel code is marked as going away in the codebase, in some way that is hard to avoid for users (for example, renaming all the user-facing channel classes). Users should be encouraged to visit this issue and comment, and to seek alternatives such as Globus Compute.
7th November 2024: channel code is removed from Parsl

# Why can't this become an "abandoned component sitting in a directory"

The channel facility is not componentised - it contains prototype quality code inside the core of Parsl

# What about the AdHocProvider which attempts to use multiple channels to run on a cluster which has no resource manager?

This should go away too - without channels it is of little use. Users should be encouraged to find other ways to run a cluster without any resource manager. It should not be parsl's job to manage this aspect of a user's cluster.

# What if someone wants to take on Channel tech sponsorship?

This could happen, but I think it's unlikely.

The tech sponsor would need to be responsible for tidying up the architecture and implementing those fixes, with noise such as "why are channels initialised or not based on the presence of `script_dir` attribute on a provider?", test implementations and fixups such as testing parsl without shared file systems in a channel-like environment, and substantially more fundamental questions such as "how is htex supposed to work when channels do not provide a channel for htex network connections?".

For the more fundamental problems, this starts to sound like making a re-run of Globus Compute, which the funcX project has shown can occupy several full time developers for several years. I think probably that tech sponsors time would be better places working to get Parsl playing nicely with other remote execution technology - for example Globus Compute.

I just tagged a load of issues with I just tagged a bunch of issues with https://github.com/Parsl/parsl/labels/channels to give an overview of the sort of stuff a Channel tech sponsor would need to address.

An indicator for a successful channel tech sponsor would be substantial progress on these issues by the removal date in the timetable above.
Remove channels
# Overview

Parsl has a facility called Channels intended to support use cases such as running a workflow on your laptop but with task execution on a supercomputer. This facility never evolved beyond prototype stage, and effort from University of Chicago and University of Illinois was rapidly distracted into the funcX project which aims (with substantially more code and developer time) to provide roughly the same facility - now named Globus Compute and developed by professional programmers rather than an academic research team.

The presence of channels as quasi-abandonware inside the modern Parsl codebase is a painful drain on both user and developer time: Users continue to be fooled into believing that this abandoned prototype facility, rather than the results of the funcX project, should be used for remote execution use cases. Developer time continues to be taken up dealing with the intricate lacing of channel handling through the core codebase.

Channels should be removed from Parsl, with the default behaviour of `LocalChannel` becoming the only behaviour.

# Proposed timeline

I propose this timeline as a default, unless a consensus forms for some other timeline. In the absence of any other consensus forming in the comments of this issue, I'll make this timeline happen.


now .. 7th August 2024: people paying attention to issues get to comment on this issue, including offering entirely different alternative paths forward
7th August 2024: channel code is marked as going away in the codebase, in some way that is hard to avoid for users (for example, renaming all the user-facing channel classes). Users should be encouraged to visit this issue and comment, and to seek alternatives such as Globus Compute.
7th November 2024: channel code is removed from Parsl

# Why can't this become an "abandoned component sitting in a directory"

The channel facility is not componentised - it contains prototype quality code inside the core of Parsl

# What about the AdHocProvider which attempts to use multiple channels to run on a cluster which has no resource manager?

This should go away too - without channels it is of little use. Users should be encouraged to find other ways to run a cluster without any resource manager. It should not be parsl's job to manage this aspect of a user's cluster.

# What if someone wants to take on Channel tech sponsorship?

This could happen, but I think it's unlikely.

The tech sponsor would need to be responsible for tidying up the architecture and implementing those fixes, with noise such as "why are channels initialised or not based on the presence of `script_dir` attribute on a provider?", test implementations and fixups such as testing parsl without shared file systems in a channel-like environment, and substantially more fundamental questions such as "how is htex supposed to work when channels do not provide a channel for htex network connections?".

For the more fundamental problems, this starts to sound like making a re-run of Globus Compute, which the funcX project has shown can occupy several full time developers for several years. I think probably that tech sponsors time would be better places working to get Parsl playing nicely with other remote execution technology - for example Globus Compute.

I just tagged a load of issues with I just tagged a bunch of issues with https://github.com/Parsl/parsl/labels/channels to give an overview of the sort of stuff a Channel tech sponsor would need to address.

An indicator for a successful channel tech sponsor would be substantial progress on these issues by the removal date in the timetable above.


Discussion / hints:
@benclifford I just wanted to put my vote here to remove channels entirely. 

From the user perspective the only folks I think we need to make sure know of the change would be:
1) folks using `SSHChannels` ought to gently pushed to make a switch over to `globus-compute`. 
2) `AdHocChannels` will have no alternative really beyond your recommendation to use a cluster resource manager (slurm..)

@benclifford I agree that channels are no longer needed and can be very finnicky to use.
@benclifford I just wanted to put my vote here to remove channels entirely. 

From the user perspective the only folks I think we need to make sure know of the change would be:
1) folks using `SSHChannels` ought to gently pushed to make a switch over to `globus-compute`. 
2) `AdHocChannels` will have no alternative really beyond your recommendation to use a cluster resource manager (slurm..)

@benclifford I agree that channels are no longer needed and can be very finnicky to use.

Fix patch:
diff --git a/parsl/channels/base.py b/parsl/channels/base.py
index 754e89c385..5bb38ae074 100644
--- a/parsl/channels/base.py
+++ b/parsl/channels/base.py
@@ -8,12 +8,9 @@ class Channel(metaclass=ABCMeta):
 
     For certain resources such as campus clusters or supercomputers at
     research laboratories, resource requirements may require authentication.
-    For instance some resources may allow access to their job schedulers from
-    only their login-nodes which require you to authenticate through SSH, or
-    require two factor authentication.
 
-    The simplest Channel, *LocalChannel*, executes commands locally in a
-    shell, while the *SSHChannel* authenticates you to remote systems.
+    The only remaining Channel, *LocalChannel*, executes commands locally in a
+    shell.
 
     Channels provide the ability to execute commands remotely, using the
     execute_wait method, and manipulate the remote file system using methods
diff --git a/parsl/channels/oauth_ssh/__init__.py b/parsl/channels/oauth_ssh/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/parsl/channels/oauth_ssh/oauth_ssh.py b/parsl/channels/oauth_ssh/oauth_ssh.py
deleted file mode 100644
index 1b690a4e3c..0000000000
--- a/parsl/channels/oauth_ssh/oauth_ssh.py
+++ /dev/null
@@ -1,119 +0,0 @@
-import logging
-import socket
-
-from parsl.channels.ssh.ssh import DeprecatedSSHChannel
-from parsl.errors import OptionalModuleMissing
-
-try:
-    import paramiko
-    _ssh_enabled = True
-except (ImportError, NameError, FileNotFoundError):
-    _ssh_enabled = False
-
-try:
-    from oauth_ssh.oauth_ssh_token import find_access_token
-    from oauth_ssh.ssh_service import SSHService
-    _oauth_ssh_enabled = True
-except (ImportError, NameError):
-    _oauth_ssh_enabled = False
-
-
-logger = logging.getLogger(__name__)
-
-
-class DeprecatedOAuthSSHChannel(DeprecatedSSHChannel):
-    """SSH persistent channel. This enables remote execution on sites
-    accessible via ssh. This channel uses Globus based OAuth tokens for authentication.
-    """
-
-    def __init__(self, hostname, username=None, script_dir=None, envs=None, port=22):
-        ''' Initialize a persistent connection to the remote system.
-        We should know at this point whether ssh connectivity is possible
-
-        Args:
-            - hostname (String) : Hostname
-
-        KWargs:
-            - username (string) : Username on remote system
-            - script_dir (string) : Full path to a script dir where
-              generated scripts could be sent to.
-            - envs (dict) : A dictionary of env variables to be set when executing commands
-            - port (int) : Port at which the SSHService is running
-
-        Raises:
-        '''
-        if not _ssh_enabled:
-            raise OptionalModuleMissing(['ssh'],
-                                        "OauthSSHChannel requires the ssh module and config.")
-
-        if not _oauth_ssh_enabled:
-            raise OptionalModuleMissing(['oauth_ssh'],
-                                        "OauthSSHChannel requires oauth_ssh module and config.")
-
-        self.hostname = hostname
-        self.username = username
-        self.script_dir = script_dir
-        self.port = port
-        self.envs = {}
-        if envs is not None:
-            self.envs = envs
-
-        try:
-            access_token = find_access_token(hostname)
-        except Exception:
-            logger.exception("Failed to find the access token for {}".format(hostname))
-            raise
-
-        try:
-            self.service = SSHService(hostname, port)
-            self.transport = self.service.login(access_token, username)
-
-        except Exception:
-            logger.exception("Caught an exception in the OAuth authentication step with {}".format(hostname))
-            raise
-
-        self.sftp_client = paramiko.SFTPClient.from_transport(self.transport)
-
-    def execute_wait(self, cmd, walltime=60, envs={}):
-        ''' Synchronously execute a commandline string on the shell.
-
-        This command does *NOT* honor walltime currently.
-
-        Args:
-            - cmd (string) : Commandline string to execute
-
-        Kwargs:
-            - walltime (int) : walltime in seconds
-            - envs (dict) : Dictionary of env variables
-
-        Returns:
-            - retcode : Return code from the execution, -1 on fail
-            - stdout  : stdout string
-            - stderr  : stderr string
-
-        Raises:
-        None.
-        '''
-
-        session = self.transport.open_session()
-        session.setblocking(0)
-
-        nbytes = 1024
-        session.exec_command(self.prepend_envs(cmd, envs))
-        session.settimeout(walltime)
-
-        try:
-            # Wait until command is executed
-            exit_status = session.recv_exit_status()
-
-            stdout = session.recv(nbytes).decode('utf-8')
-            stderr = session.recv_stderr(nbytes).decode('utf-8')
-
-        except socket.timeout:
-            logger.exception("Command failed to execute without timeout limit on {}".format(self))
-            raise
-
-        return exit_status, stdout, stderr
-
-    def close(self) -> None:
-        self.transport.close()
diff --git a/parsl/channels/ssh/__init__.py b/parsl/channels/ssh/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/parsl/channels/ssh/ssh.py b/parsl/channels/ssh/ssh.py
deleted file mode 100644
index 1f06748bae..0000000000
--- a/parsl/channels/ssh/ssh.py
+++ /dev/null
@@ -1,295 +0,0 @@
-import errno
-import logging
-import os
-
-from parsl.channels.base import Channel
-from parsl.channels.errors import (
-    AuthException,
-    BadHostKeyException,
-    BadPermsScriptPath,
-    BadScriptPath,
-    FileCopyException,
-    SSHException,
-)
-from parsl.errors import OptionalModuleMissing
-from parsl.utils import RepresentationMixin
-
-try:
-    import paramiko
-    _ssh_enabled = True
-except (ImportError, NameError, FileNotFoundError):
-    _ssh_enabled = False
-
-
-logger = logging.getLogger(__name__)
-
-
-if _ssh_enabled:
-    class NoAuthSSHClient(paramiko.SSHClient):
-        def _auth(self, username, *args):
-            self._transport.auth_none(username)
-            return
-
-
-class DeprecatedSSHChannel(Channel, RepresentationMixin):
-    ''' SSH persistent channel. This enables remote execution on sites
-    accessible via ssh. It is assumed that the user has setup host keys
-    so as to ssh to the remote host. Which goes to say that the following
-    test on the commandline should work:
-
-    >>> ssh <username>@<hostname>
-
-    '''
-
-    def __init__(self, hostname, username=None, password=None, script_dir=None, envs=None,
-                 gssapi_auth=False, skip_auth=False, port=22, key_filename=None, host_keys_filename=None):
-        ''' Initialize a persistent connection to the remote system.
-        We should know at this point whether ssh connectivity is possible
-
-        Args:
-            - hostname (String) : Hostname
-
-        KWargs:
-            - username (string) : Username on remote system
-            - password (string) : Password for remote system
-            - port : The port designated for the ssh connection. Default is 22.
-            - script_dir (string) : Full path to a script dir where
-              generated scripts could be sent to.
-            - envs (dict) : A dictionary of environment variables to be set when executing commands
-            - key_filename (string or list): the filename, or list of filenames, of optional private key(s)
-
-        Raises:
-        '''
-        if not _ssh_enabled:
-            raise OptionalModuleMissing(['ssh'],
-                                        "SSHChannel requires the ssh module and config.")
-
-        self.hostname = hostname
-        self.username = username
-        self.password = password
-        self.port = port
-        self.script_dir = script_dir
-        self.skip_auth = skip_auth
-        self.gssapi_auth = gssapi_auth
-        self.key_filename = key_filename
-        self.host_keys_filename = host_keys_filename
-
-        if self.skip_auth:
-            self.ssh_client = NoAuthSSHClient()
-        else:
-            self.ssh_client = paramiko.SSHClient()
-        self.ssh_client.load_system_host_keys(filename=host_keys_filename)
-        self.ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-        self.sftp_client = None
-
-        self.envs = {}
-        if envs is not None:
-            self.envs = envs
-
-    def _is_connected(self):
-        transport = self.ssh_client.get_transport() if self.ssh_client else None
-        return transport and transport.is_active()
-
-    def _connect(self):
-        if not self._is_connected():
-            logger.debug(f"connecting to {self.hostname}:{self.port}")
-            try:
-                self.ssh_client.connect(
-                    self.hostname,
-                    username=self.username,
-                    password=self.password,
-                    port=self.port,
-                    allow_agent=True,
-                    gss_auth=self.gssapi_auth,
-                    gss_kex=self.gssapi_auth,
-                    key_filename=self.key_filename
-                )
-                transport = self.ssh_client.get_transport()
-                self.sftp_client = paramiko.SFTPClient.from_transport(transport)
-
-            except paramiko.BadHostKeyException as e:
-                raise BadHostKeyException(e, self.hostname)
-
-            except paramiko.AuthenticationException as e:
-                raise AuthException(e, self.hostname)
-
-            except paramiko.SSHException as e:
-                raise SSHException(e, self.hostname)
-
-            except Exception as e:
-                raise SSHException(e, self.hostname)
-
-    def _valid_sftp_client(self):
-        self._connect()
-        return self.sftp_client
-
-    def _valid_ssh_client(self):
-        self._connect()
-        return self.ssh_client
-
-    def prepend_envs(self, cmd, env={}):
-        env.update(self.envs)
-
-        if len(env.keys()) > 0:
-            env_vars = ' '.join(['{}={}'.format(key, value) for key, value in env.items()])
-            return 'env {0} {1}'.format(env_vars, cmd)
-        return cmd
-
-    def execute_wait(self, cmd, walltime=2, envs={}):
-        ''' Synchronously execute a commandline string on the shell.
-
-        Args:
-            - cmd (string) : Commandline string to execute
-            - walltime (int) : walltime in seconds
-
-        Kwargs:
-            - envs (dict) : Dictionary of env variables
-
-        Returns:
-            - retcode : Return code from the execution, -1 on fail
-            - stdout  : stdout string
-            - stderr  : stderr string
-
-        Raises:
-        None.
-        '''
-
-        # Execute the command
-        stdin, stdout, stderr = self._valid_ssh_client().exec_command(
-            self.prepend_envs(cmd, envs), bufsize=-1, timeout=walltime
-        )
-        # Block on exit status from the command
-        exit_status = stdout.channel.recv_exit_status()
-        return exit_status, stdout.read().decode("utf-8"), stderr.read().decode("utf-8")
-
-    def push_file(self, local_source, remote_dir):
-        ''' Transport a local file to a directory on a remote machine
-
-        Args:
-            - local_source (string): Path
-            - remote_dir (string): Remote path
-
-        Returns:
-            - str: Path to copied file on remote machine
-
-        Raises:
-            - BadScriptPath : if script path on the remote side is bad
-            - BadPermsScriptPath : You do not have perms to make the channel script dir
-            - FileCopyException : FileCopy failed.
-
-        '''
-        remote_dest = os.path.join(remote_dir, os.path.basename(local_source))
-
-        try:
-            self.makedirs(remote_dir, exist_ok=True)
-        except IOError as e:
-            logger.exception("Pushing {0} to {1} failed".format(local_source, remote_dir))
-            if e.errno == 2:
-                raise BadScriptPath(e, self.hostname)
-            elif e.errno == 13:
-                raise BadPermsScriptPath(e, self.hostname)
-            else:
-                logger.exception("File push failed due to SFTP client failure")
-                raise FileCopyException(e, self.hostname)
-        try:
-            self._valid_sftp_client().put(local_source, remote_dest, confirm=True)
-            # Set perm because some systems require the script to be executable
-            self._valid_sftp_client().chmod(remote_dest, 0o700)
-        except Exception as e:
-            logger.exception("File push from local source {} to remote destination {} failed".format(
-                local_source, remote_dest))
-            raise FileCopyException(e, self.hostname)
-
-        return remote_dest
-
-    def pull_file(self, remote_source, local_dir):
-        ''' Transport file on the remote side to a local directory
-
-        Args:
-            - remote_source (string): remote_source
-            - local_dir (string): Local directory to copy to
-
-
-        Returns:
-            - str: Local path to file
-
-        Raises:
-            - FileCopyException : FileCopy failed.
-        '''
-
-        local_dest = os.path.join(local_dir, os.path.basename(remote_source))
-
-        try:
-            os.makedirs(local_dir)
-        except OSError as e:
-            if e.errno != errno.EEXIST:
-                logger.exception("Failed to create local_dir: {0}".format(local_dir))
-                raise BadScriptPath(e, self.hostname)
-
-        try:
-            self._valid_sftp_client().get(remote_source, local_dest)
-        except Exception as e:
-            logger.exception("File pull failed")
-            raise FileCopyException(e, self.hostname)
-
-        return local_dest
-
-    def close(self) -> None:
-        if self._is_connected():
-            transport = self.ssh_client.get_transport()
-            self.ssh_client.close()
-
-            # ssh_client.close calls transport.close, but transport.close does
-            # not always wait for the transport thread to be stopped. See impl
-            # of Transport.close in paramiko and issue
-            # https://github.com/paramiko/paramiko/issues/520
-            logger.debug("Waiting for transport thread to stop")
-            transport.join(30)
-            if transport.is_alive():
-                logger.warning("SSH transport thread did not shut down")
-            else:
-                logger.debug("SSH transport thread stopped")
-
-    def isdir(self, path):
-        """Return true if the path refers to an existing directory.
-
-        Parameters
-        ----------
-        path : str
-            Path of directory on the remote side to check.
-        """
-        result = True
-        try:
-            self._valid_sftp_client().lstat(path)
-        except FileNotFoundError:
-            result = False
-
-        return result
-
-    def makedirs(self, path, mode=0o700, exist_ok=False):
-        """Create a directory on the remote side.
-
-        If intermediate directories do not exist, they will be created.
-
-        Parameters
-        ----------
-        path : str
-            Path of directory on the remote side to create.
-        mode : int
-            Permissions (posix-style) for the newly-created directory.
-        exist_ok : bool
-            If False, raise an OSError if the target directory already exists.
-        """
-        if exist_ok is False and self.isdir(path):
-            raise OSError('Target directory {} already exists'.format(path))
-
-        self.execute_wait('mkdir -p {}'.format(path))
-        self._valid_sftp_client().chmod(path, mode)
-
-    @property
-    def script_dir(self):
-        return self._script_dir
-
-    @script_dir.setter
-    def script_dir(self, value):
-        self._script_dir = value
diff --git a/parsl/channels/ssh_il/__init__.py b/parsl/channels/ssh_il/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/parsl/channels/ssh_il/ssh_il.py b/parsl/channels/ssh_il/ssh_il.py
deleted file mode 100644
index 67e5501a43..0000000000
--- a/parsl/channels/ssh_il/ssh_il.py
+++ /dev/null
@@ -1,85 +0,0 @@
-import getpass
-import logging
-
-from parsl.channels.ssh.ssh import DeprecatedSSHChannel
-from parsl.errors import OptionalModuleMissing
-
-try:
-    import paramiko
-    _ssh_enabled = True
-except (ImportError, NameError, FileNotFoundError):
-    _ssh_enabled = False
-
-
-logger = logging.getLogger(__name__)
-
-
-class DeprecatedSSHInteractiveLoginChannel(DeprecatedSSHChannel):
-    """SSH persistent channel. This enables remote execution on sites
-    accessible via ssh. This channel supports interactive login and is appropriate when
-    keys are not set up.
-    """
-
-    def __init__(self, hostname, username=None, password=None, script_dir=None, envs=None):
-        ''' Initialize a persistent connection to the remote system.
-        We should know at this point whether ssh connectivity is possible
-
-        Args:
-            - hostname (String) : Hostname
-
-        KWargs:
-            - username (string) : Username on remote system
-            - password (string) : Password for remote system
-            - script_dir (string) : Full path to a script dir where
-              generated scripts could be sent to.
-            - envs (dict) : A dictionary of env variables to be set when executing commands
-
-        Raises:
-        '''
-        if not _ssh_enabled:
-            raise OptionalModuleMissing(['ssh'],
-                                        "SSHInteractiveLoginChannel requires the ssh module and config.")
-
-        self.hostname = hostname
-        self.username = username
-        self.password = password
-
-        self.ssh_client = paramiko.SSHClient()
-        self.ssh_client.load_system_host_keys()
-        self.ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-
-        self.script_dir = script_dir
-
-        self.envs = {}
-        if envs is not None:
-            self.envs = envs
-
-        try:
-            self.ssh_client.connect(
-                hostname, username=username, password=password, allow_agent=True
-            )
-
-        except Exception:
-            logger.debug("Caught the SSHException in SSHInteractive")
-            pass
-        '''
-        except paramiko.BadHostKeyException as e:
-            raise BadHostKeyException(e, self.hostname)
-
-        except paramiko.AuthenticationException as e:
-            raise AuthException(e, self.hostname)
-
-        except paramiko.SSHException as e:
-            logger.debug("Caught the SSHException in SSHInteractive")
-            pass
-
-        except Exception as e:
-            raise SSHException(e, self.hostname)
-        '''
-
-        transport = self.ssh_client.get_transport()
-
-        il_password = getpass.getpass('Enter {0} Logon password :'.format(hostname))
-        transport.auth_password(username, il_password)
-
-        self.sftp_client = paramiko.SFTPClient.from_transport(transport)
diff --git a/parsl/providers/cluster_provider.py b/parsl/providers/cluster_provider.py
index 50f38a6136..6bc76bdf22 100644
--- a/parsl/providers/cluster_provider.py
+++ b/parsl/providers/cluster_provider.py
@@ -18,10 +18,7 @@ class ClusterProvider(ExecutionProvider):
       label : str
         Label for this provider.
       channel : Channel
-        Channel for accessing this provider. Possible channels include
-        :class:`~parsl.channels.LocalChannel` (the default),
-        :class:`~parsl.channels.SSHChannel`, or
-        :class:`~parsl.channels.SSHInteractiveLoginChannel`.
+        Channel for accessing this provider.
       walltime : str
         Walltime requested per block in HH:MM:SS.
       launcher : Launcher
diff --git a/parsl/providers/condor/condor.py b/parsl/providers/condor/condor.py
index 25266d3a2e..a736386d38 100644
--- a/parsl/providers/condor/condor.py
+++ b/parsl/providers/condor/condor.py
@@ -37,10 +37,7 @@ class CondorProvider(RepresentationMixin, ClusterProvider):
     Parameters
     ----------
     channel : Channel
-        Channel for accessing this provider. Possible channels include
-        :class:`~parsl.channels.LocalChannel` (the default),
-        :class:`~parsl.channels.SSHChannel`, or
-        :class:`~parsl.channels.SSHInteractiveLoginChannel`.
+        Channel for accessing this provider.
     nodes_per_block : int
         Nodes to provision per block.
     cores_per_slot : int
diff --git a/parsl/providers/grid_engine/grid_engine.py b/parsl/providers/grid_engine/grid_engine.py
index 8e231637a9..e7db987022 100644
--- a/parsl/providers/grid_engine/grid_engine.py
+++ b/parsl/providers/grid_engine/grid_engine.py
@@ -37,10 +37,7 @@ class GridEngineProvider(ClusterProvider, RepresentationMixin):
     Parameters
     ----------
     channel : Channel
-        Channel for accessing this provider. Possible channels include
-        :class:`~parsl.channels.LocalChannel` (the default),
-        :class:`~parsl.channels.SSHChannel`, or
-        :class:`~parsl.channels.SSHInteractiveLoginChannel`.
+        Channel for accessing this provider.
     nodes_per_block : int
         Nodes to provision per block.
     min_blocks : int
diff --git a/parsl/providers/lsf/lsf.py b/parsl/providers/lsf/lsf.py
index c993b58c7b..8f18f5c879 100644
--- a/parsl/providers/lsf/lsf.py
+++ b/parsl/providers/lsf/lsf.py
@@ -33,10 +33,7 @@ class LSFProvider(ClusterProvider, RepresentationMixin):
     Parameters
     ----------
     channel : Channel
-        Channel for accessing this provider. Possible channels include
-        :class:`~parsl.channels.LocalChannel` (the default),
-        :class:`~parsl.channels.SSHChannel`, or
-        :class:`~parsl.channels.SSHInteractiveLoginChannel`.
+        Channel for accessing this provider.
     nodes_per_block : int
         Nodes to provision per block.
         When request_by_nodes is False, it is computed by cores_per_block / cores_per_node.
diff --git a/parsl/providers/pbspro/pbspro.py b/parsl/providers/pbspro/pbspro.py
index c998602152..752f504334 100644
--- a/parsl/providers/pbspro/pbspro.py
+++ b/parsl/providers/pbspro/pbspro.py
@@ -18,10 +18,7 @@ class PBSProProvider(TorqueProvider):
     Parameters
     ----------
     channel : Channel
-        Channel for accessing this provider. Possible channels include
-        :class:`~parsl.channels.LocalChannel` (the default),
-        :class:`~parsl.channels.SSHChannel`, or
-        :class:`~parsl.channels.SSHInteractiveLoginChannel`.
+        Channel for accessing this provider.
     account : str
         Account the job will be charged against.
     queue : str
diff --git a/parsl/providers/slurm/slurm.py b/parsl/providers/slurm/slurm.py
index 54b4053fed..2aa855bb48 100644
--- a/parsl/providers/slurm/slurm.py
+++ b/parsl/providers/slurm/slurm.py
@@ -71,10 +71,7 @@ class SlurmProvider(ClusterProvider, RepresentationMixin):
     constraint : str
         Slurm job constraint, often used to choose cpu or gpu type. If unspecified or ``None``, no constraint slurm directive will be added.
     channel : Channel
-        Channel for accessing this provider. Possible channels include
-        :class:`~parsl.channels.LocalChannel` (the default),
-        :class:`~parsl.channels.SSHChannel`, or
-        :class:`~parsl.channels.SSHInteractiveLoginChannel`.
+        Channel for accessing this provider.
     nodes_per_block : int
         Nodes to provision per block.
     cores_per_node : int
diff --git a/parsl/providers/torque/torque.py b/parsl/providers/torque/torque.py
index dfe267dbd3..c15591706c 100644
--- a/parsl/providers/torque/torque.py
+++ b/parsl/providers/torque/torque.py
@@ -34,10 +34,7 @@ class TorqueProvider(ClusterProvider, RepresentationMixin):
     Parameters
     ----------
     channel : Channel
-        Channel for accessing this provider. Possible channels include
-        :class:`~parsl.channels.LocalChannel` (the default),
-        :class:`~parsl.channels.SSHChannel`, or
-        :class:`~parsl.channels.SSHInteractiveLoginChannel`.
+        Channel for accessing this provider.
     account : str
         Account the job will be charged against.
     queue : str
diff --git a/setup.py b/setup.py
index b381ecfc2b..cace8c0252 100755
--- a/setup.py
+++ b/setup.py
@@ -21,7 +21,6 @@
     ],
     'aws' : ['boto3'],
     'kubernetes' : ['kubernetes'],
-    'oauth_ssh' : ['oauth-ssh>=0.9'],
     'docs' : [
         'ipython<=8.6.0',
         'nbsphinx',
@@ -35,7 +34,6 @@
     'flux': ['pyyaml', 'cffi', 'jsonschema'],
     'proxystore': ['proxystore'],
     'radical-pilot': ['radical.pilot==1.60', 'radical.utils==1.60'],
-    'ssh': ['paramiko'],
     # Disabling psi-j since github direct links are not allowed by pypi
     # 'psij': ['psi-j-parsl@git+https://github.com/ExaWorks/psi-j-parsl']
 }


Test patch:
diff --git a/parsl/tests/test_providers/test_local_provider.py b/parsl/tests/test_providers/test_local_provider.py
index 497c13370d..8d56d89ddf 100644
--- a/parsl/tests/test_providers/test_local_provider.py
+++ b/parsl/tests/test_providers/test_local_provider.py
@@ -12,7 +12,6 @@
 import pytest
 
 from parsl.channels import LocalChannel
-from parsl.channels.ssh.ssh import DeprecatedSSHChannel
 from parsl.jobs.states import JobState
 from parsl.launchers import SingleNodeLauncher
 from parsl.providers import LocalProvider
@@ -69,140 +68,6 @@ def test_local_channel():
         _run_tests(p)
 
 
-SSHD_CONFIG = """
-Port {port}
-ListenAddress 127.0.0.1
-HostKey {hostkey}
-AuthorizedKeysFile {connpubkey}
-AuthenticationMethods publickey
-StrictModes no
-Subsystem sftp {sftp_path}
-"""
-
-
-# It would probably be better, when more formalized site testing comes into existence, to
-# use a site-testing provided server/configuration instead of the current scheme
-@pytest.mark.local
-@pytest.mark.sshd_required
-def test_ssh_channel():
-    with tempfile.TemporaryDirectory() as config_dir:
-        sshd_thread, priv_key, server_port = _start_sshd(config_dir)
-        try:
-            with tempfile.TemporaryDirectory() as remote_script_dir:
-                # The SSH library fails to add the new host key to the file if the file does not
-                # already exist, so create it here.
-                pathlib.Path('{}/known.hosts'.format(config_dir)).touch(mode=0o600)
-                script_dir = tempfile.mkdtemp()
-                channel = DeprecatedSSHChannel('127.0.0.1', port=server_port,
-                                               script_dir=remote_script_dir,
-                                               host_keys_filename='{}/known.hosts'.format(config_dir),
-                                               key_filename=priv_key)
-                try:
-                    p = LocalProvider(channel=channel,
-                                      launcher=SingleNodeLauncher(debug=False))
-                    p.script_dir = script_dir
-                    _run_tests(p)
-                finally:
-                    channel.close()
-        finally:
-            _stop_sshd(sshd_thread)
-
-
-def _stop_sshd(sshd_thread):
-    sshd_thread.stop()
-    sshd_thread.join()
-
-
-class SSHDThread(threading.Thread):
-    def __init__(self, config_file):
-        threading.Thread.__init__(self, daemon=True)
-        self.config_file = config_file
-        self.stop_flag = False
-        self.error = None
-
-    def run(self):
-        try:
-            # sshd needs to be run with an absolute path, hence the call to which()
-            sshpath = shutil.which('sshd')
-            assert sshpath is not None, "can find sshd executable"
-            p = subprocess.Popen([sshpath, '-D', '-f', self.config_file],
-                                 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
-            while True:
-                ec = p.poll()
-                if self.stop_flag:
-                    p.terminate()
-                    break
-                elif ec is None:
-                    time.sleep(0.1)
-                elif ec == 0:
-                    self.error = Exception('sshd exited prematurely: {}{}'.format(p.stdout.read(),
-                                                                                  p.stderr.read()))
-                    break
-                else:
-                    self.error = Exception('sshd failed: {}{}'.format(p.stdout.read(),
-                                                                      p.stderr.read()))
-                    break
-        except Exception as ex:
-            logger.exception("SSHDThread exception from run loop")
-            self.error = ex
-
-    def stop(self):
-        self.stop_flag = True
-
-
-def _start_sshd(config_dir: str):
-    server_config, priv_key, port = _init_sshd(config_dir)
-    sshd_thread = SSHDThread(server_config)
-    sshd_thread.start()
-    time.sleep(1.0)
-    if not sshd_thread.is_alive():
-        raise Exception('Failed to start sshd: {}'.format(sshd_thread.error))
-    return sshd_thread, priv_key, port
-
-
-def _init_sshd(config_dir):
-    hostkey = '{}/hostkey'.format(config_dir)
-    connkey = '{}/connkey'.format(config_dir)
-    os.system('ssh-keygen -b 2048 -t rsa -q -N "" -f {}'.format(hostkey))
-    os.system('ssh-keygen -b 2048 -t rsa -q -N "" -f {}'.format(connkey))
-    port = _find_free_port(22222)
-    server_config_str = SSHD_CONFIG.format(port=port, hostkey=hostkey,
-                                           connpubkey='{}.pub'.format(connkey),
-                                           sftp_path=_get_system_sftp_path())
-    server_config = '{}/sshd_config'.format(config_dir)
-    with open(server_config, 'w') as f:
-        f.write(server_config_str)
-    return server_config, connkey, port
-
-
-def _get_system_sftp_path():
-    try:
-        with open('/etc/ssh/sshd_config') as f:
-            line = f.readline()
-            while line:
-                tokens = line.split()
-                if tokens[0] == 'Subsystem' and tokens[1] == 'sftp':
-                    return tokens[2]
-                line = f.readline()
-    except Exception:
-        pass
-    return '/usr/lib/openssh/sftp-server'
-
-
-def _find_free_port(start: int):
-    port = start
-    while port < 65535:
-        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-        try:
-            s.bind(('127.0.0.1', port))
-            s.close()
-            return port
-        except Exception:
-            pass
-        port += random.randint(1, 20)
-    raise Exception('Could not find free port')
-
-
 def _run(p: LocalProvider, command: str, np: int = 1):
     id = p.submit(command, np)
     return _wait(p, id)