Repository: spyder-ide/spyder

Issue:
Connect to kernel dialog needs to be clearer that either the SSH keyfile *or* the password is required
<!--- **PLEASE READ:** When submitting here, please ensure you've completed the following checklist and checked the boxes to confirm. Issue reports without it may be closed. Thanks! --->

## Problem Description

Several users have stated that the connect to kernel dialog is confusing when connecting to a remote kernel, because it isn't clear that either a SSH keyfile *or* a password is required, not both—and looking at it now, I can see why.

![image](https://user-images.githubusercontent.com/17051931/45266353-8ef1d780-b41f-11e8-9623-92a25007de3f.png)

### User Reports

In particular, on our Gitter, a user said:

> Hi, I am trying to connect Spyder on my PC (running within Anaconda) to a remote ipython kernel on an AWS EC2 AMI and I'm struggling. I followed the instructions here: http://docs.spyder-ide.org/ipythonconsole.html#connect-to-an-external-kernel and the kernel is running fine on the EC2 AMI and I have the JSON file on my PC but I'm not sure what to enter in the 'Connect to an existing kernel' box. I add the path to the JSON file in the 'Connection info' box. I check the 'This is a remote kernel' box. I enter my EC2 host: 'ec2-user@blah-blah:8888' and then get stuck on the SSH key and password boxes. When I connect to the EC2 AMI from MobaXterm I use an imported Putty key (.ppk) file (and don't use a password) whereas the Spyder dialogue box is asking for a .pem file. Do I need the original AWS .pem key?


And on spyder-ide/spyder-docs#78 , @jsh9 said:

> It might also be worth noting (either in Spyder's UI, or in the documentation) that the "ssh key" field is optional. I spent quite some time googling what ssh key file I should provide to no avail. Other users may run into the same confusion.


### Proposed Solutions

I'll clarify it in the docs as part of spyder-ide/spyder-dcos#78 , but it needs to be clearer in the UI too.

One possibility would be including the text "Or" between the two fields and grouping them in some way, e.g. 

Host name: ``____________________________``

SSH Key: ``_______________________________``
Or
Password: ``______________________________``

Alternatively, we could just include a line of explanatory text above the two fields saying only one of the two are required, e.g. 

Host name: ``____________________________``
Only one of the following is required:
SSH Key: ``_______________________________``
Password: ``______________________________``
Connect to kernel dialog needs to be clearer that either the SSH keyfile *or* the password is required
<!--- **PLEASE READ:** When submitting here, please ensure you've completed the following checklist and checked the boxes to confirm. Issue reports without it may be closed. Thanks! --->

## Problem Description

Several users have stated that the connect to kernel dialog is confusing when connecting to a remote kernel, because it isn't clear that either a SSH keyfile *or* a password is required, not both—and looking at it now, I can see why.

![image](https://user-images.githubusercontent.com/17051931/45266353-8ef1d780-b41f-11e8-9623-92a25007de3f.png)

### User Reports

In particular, on our Gitter, a user said:

> Hi, I am trying to connect Spyder on my PC (running within Anaconda) to a remote ipython kernel on an AWS EC2 AMI and I'm struggling. I followed the instructions here: http://docs.spyder-ide.org/ipythonconsole.html#connect-to-an-external-kernel and the kernel is running fine on the EC2 AMI and I have the JSON file on my PC but I'm not sure what to enter in the 'Connect to an existing kernel' box. I add the path to the JSON file in the 'Connection info' box. I check the 'This is a remote kernel' box. I enter my EC2 host: 'ec2-user@blah-blah:8888' and then get stuck on the SSH key and password boxes. When I connect to the EC2 AMI from MobaXterm I use an imported Putty key (.ppk) file (and don't use a password) whereas the Spyder dialogue box is asking for a .pem file. Do I need the original AWS .pem key?


And on spyder-ide/spyder-docs#78 , @jsh9 said:

> It might also be worth noting (either in Spyder's UI, or in the documentation) that the "ssh key" field is optional. I spent quite some time googling what ssh key file I should provide to no avail. Other users may run into the same confusion.


### Proposed Solutions

I'll clarify it in the docs as part of spyder-ide/spyder-dcos#78 , but it needs to be clearer in the UI too.

One possibility would be including the text "Or" between the two fields and grouping them in some way, e.g. 

Host name: ``____________________________``

SSH Key: ``_______________________________``
Or
Password: ``______________________________``

Alternatively, we could just include a line of explanatory text above the two fields saying only one of the two are required, e.g. 

Host name: ``____________________________``
Only one of the following is required:
SSH Key: ``_______________________________``
Password: ``______________________________``


Discussion:
> Alternatively, we could just include a line of explanatory text above the two fields saying only one of the two are required

I like this option.
You could also put radio buttons in front of the "SSH Key" and "Password" fields. But I think a line of explanatory text will probably look better.
And if the dialogue remembered some choices would be fantastic.
I can even think about if Spyder could itself start a remote kernel if given the ssh credentials, but now I'm just dreaming :-)
@jontis Thanks for your feedback! 

That's someone out of scope for this particular issue, but it wouldn't be too hard to have Spyder remember the hostname. As for the credentials, we obviously wouldn't want to store them insecurely, but now that we depend on the ``keyring`` package anyway we could use it to store them like we do user's Github passwords if they choose, and include a "Remember credentials" box, unchecked by default, to toggle this. However, that might have to go in master, since its a more substantial change. @ccordoba12 Thoughts?

As for your second request, that's pretty far out of scope for now, but its not impossible that it would be implemented some day (and perhaps sooner, if a sufficiently motivated user like yourself wanted to work on it :) ). 
As I see it, starting the remote kernel and copying it to local is just 5 lines of bash scripting, but then connecting it with Spyder, that's a whole different thing. If someone helps me connecting, I'd be happy to do the rest :-)
@jontis 

> starting the remote kernel and copying it to local is just 5 lines of bash scripting

Right; that's what the user currently does themselves, using their ssh client and credentials

> but then connecting it with Spyder, that's a whole different thing

If I understand you correctly, Spyder already does this; that's the purpose of the Connect to Kernel dialog after all. All it would need is the path to the connection file (which you mentioned copying in your bash script; you could just put it in a consistent place each time), and the hostname and ssg crendentials (which depends on the host the user wants to connect to; the user already inputs this but it could be remembered for future sessions as discussed above).

So all this would really take, as you allude to, is the user entering the hostname and credentials (or accepting the previous remembered ones) into the existing connect to kernel dialog or similar, and then either Spyder triggering that bash script (or just running commands via os.system, assuming a POSIX environment) and then getting the connection file path from that automatically (either as returned output or by just picking the post recent copied file in some specific temporary directory, etc). That would be fairly simple. The possibly complex part is ensuring a POSIX environment and ssh client is available, or otherwise falling back to something else on Windows systems—the Connect to kernel backend might handle this already; I'm not intimately familiar.

However, again, we'd generally rely on the community (or yourself) implementing this part since the user can already just create a 5-line bash script specific to their host, credentials etc. and run it; the only Spyder-related part it really automates is just the connection file name/kernel ID. Remembering the last hostname and credentials is a separate issue, which would be nice to do at some point if there's demand; both are a separate issue from this one but great suggestions! We'd be happy to review a PR especially to that latter effect against master, if you'd be willing to; if not, we can probably implement at least remembering hostname/credentials at some point since we already do it for the Github dialog.
> @ccordoba12 Thoughts?

Sure, we can do this for Spyder 4.

> I can even think about if Spyder could itself start a remote kernel if given the ssh credentials

The Jupyter team has done some experimental work to make this a reality:

https://github.com/takluyver/jupyter_ssh_kernels

However, I don't know when this could be available for general consumption.
> Alternatively, we could just include a line of explanatory text above the two fields saying only one of the two are required

I like this option.
You could also put radio buttons in front of the "SSH Key" and "Password" fields. But I think a line of explanatory text will probably look better.
And if the dialogue remembered some choices would be fantastic.
I can even think about if Spyder could itself start a remote kernel if given the ssh credentials, but now I'm just dreaming :-)
@jontis Thanks for your feedback! 

That's someone out of scope for this particular issue, but it wouldn't be too hard to have Spyder remember the hostname. As for the credentials, we obviously wouldn't want to store them insecurely, but now that we depend on the ``keyring`` package anyway we could use it to store them like we do user's Github passwords if they choose, and include a "Remember credentials" box, unchecked by default, to toggle this. However, that might have to go in master, since its a more substantial change. @ccordoba12 Thoughts?

As for your second request, that's pretty far out of scope for now, but its not impossible that it would be implemented some day (and perhaps sooner, if a sufficiently motivated user like yourself wanted to work on it :) ). 
As I see it, starting the remote kernel and copying it to local is just 5 lines of bash scripting, but then connecting it with Spyder, that's a whole different thing. If someone helps me connecting, I'd be happy to do the rest :-)
@jontis 

> starting the remote kernel and copying it to local is just 5 lines of bash scripting

Right; that's what the user currently does themselves, using their ssh client and credentials

> but then connecting it with Spyder, that's a whole different thing

If I understand you correctly, Spyder already does this; that's the purpose of the Connect to Kernel dialog after all. All it would need is the path to the connection file (which you mentioned copying in your bash script; you could just put it in a consistent place each time), and the hostname and ssg crendentials (which depends on the host the user wants to connect to; the user already inputs this but it could be remembered for future sessions as discussed above).

So all this would really take, as you allude to, is the user entering the hostname and credentials (or accepting the previous remembered ones) into the existing connect to kernel dialog or similar, and then either Spyder triggering that bash script (or just running commands via os.system, assuming a POSIX environment) and then getting the connection file path from that automatically (either as returned output or by just picking the post recent copied file in some specific temporary directory, etc). That would be fairly simple. The possibly complex part is ensuring a POSIX environment and ssh client is available, or otherwise falling back to something else on Windows systems—the Connect to kernel backend might handle this already; I'm not intimately familiar.

However, again, we'd generally rely on the community (or yourself) implementing this part since the user can already just create a 5-line bash script specific to their host, credentials etc. and run it; the only Spyder-related part it really automates is just the connection file name/kernel ID. Remembering the last hostname and credentials is a separate issue, which would be nice to do at some point if there's demand; both are a separate issue from this one but great suggestions! We'd be happy to review a PR especially to that latter effect against master, if you'd be willing to; if not, we can probably implement at least remembering hostname/credentials at some point since we already do it for the Github dialog.
> @ccordoba12 Thoughts?

Sure, we can do this for Spyder 4.

> I can even think about if Spyder could itself start a remote kernel if given the ssh credentials

The Jupyter team has done some experimental work to make this a reality:

https://github.com/takluyver/jupyter_ssh_kernels

However, I don't know when this could be available for general consumption.

Resolving patch:
diff --git a/spyder/plugins/ipythonconsole.py b/spyder/plugins/ipythonconsole.py
--- a/spyder/plugins/ipythonconsole.py
+++ b/spyder/plugins/ipythonconsole.py
@@ -166,26 +166,24 @@ def openssh_tunnel(self, lport, rport, server, remoteip='127.0.0.1',
 
 
 class KernelConnectionDialog(QDialog):
-    """Dialog to connect to existing kernels (either local or remote)"""
-    
+    """Dialog to connect to existing kernels (either local or remote)."""
+
     def __init__(self, parent=None):
         super(KernelConnectionDialog, self).__init__(parent)
         self.setWindowTitle(_('Connect to an existing kernel'))
-        
-        main_label = QLabel(_("Please enter the connection info of the kernel "
-                              "you want to connect to. For that you can "
-                              "either select its JSON connection file using "
-                              "the <tt>Browse</tt> button, or write directly "
-                              "its id, in case it's a local kernel (for "
-                              "example <tt>kernel-3764.json</tt> or just "
-                              "<tt>3764</tt>)."))
+
+        main_label = QLabel(_(
+            "Please select the JSON connection file (<i>e.g.</i> "
+            "<tt>kernel-3764.json</tt>) or enter the 4-digit ID (<i>e.g.</i> "
+            "<tt>3764</tt>) of the existing kernel to connect to, "
+            "and enter the SSH host name and credentials if a remote kernel."))
         main_label.setWordWrap(True)
         main_label.setAlignment(Qt.AlignJustify)
-        
-        # connection file
-        cf_label = QLabel(_('Connection info:'))
+
+        # Connection file
+        cf_label = QLabel(_('Kernel ID/Connection file:'))
         self.cf = QLineEdit()
-        self.cf.setPlaceholderText(_('Path to connection file or kernel id'))
+        self.cf.setPlaceholderText(_('ID number or path to connection file'))
         self.cf.setMinimumWidth(250)
         cf_open_btn = QPushButton(_('Browse'))
         cf_open_btn.clicked.connect(self.select_connection_file)
@@ -194,32 +192,40 @@ def __init__(self, parent=None):
         cf_layout.addWidget(cf_label)
         cf_layout.addWidget(self.cf)
         cf_layout.addWidget(cf_open_btn)
-        
-        # remote kernel checkbox 
-        self.rm_cb = QCheckBox(_('This is a remote kernel'))
-        
-        # ssh connection 
+
+        # Remote kernel checkbox
+        self.rm_cb = QCheckBox(_('This is a remote kernel (via SSH)'))
+
+        # Descriptive text explaining SSH credentials
+        credential_label = QLabel(_(
+            "<b>Note:</b> If connecting to a remote kernel, only the "
+            "SSH keyfile <i>or</i> the Password field need to be completed, "
+            "unless the keyfile is protected with a passphrase."))
+        credential_label.setWordWrap(True)
+
+        # SSH connection
         self.hn = QLineEdit()
         self.hn.setPlaceholderText(_('username@hostname:port'))
-        
+
+        self.pw = QLineEdit()
+        self.pw.setPlaceholderText(_('Remote user password or '
+                                     'SSH keyfile passphrase'))
+        self.pw.setEchoMode(QLineEdit.Password)
+
         self.kf = QLineEdit()
-        self.kf.setPlaceholderText(_('Path to ssh key file'))
+        self.kf.setPlaceholderText(_('Path to SSH keyfile (optional)'))
         kf_open_btn = QPushButton(_('Browse'))
         kf_open_btn.clicked.connect(self.select_ssh_key)
 
         kf_layout = QHBoxLayout()
         kf_layout.addWidget(self.kf)
         kf_layout.addWidget(kf_open_btn)
-        
-        self.pw = QLineEdit()
-        self.pw.setPlaceholderText(_('Password or ssh key passphrase'))
-        self.pw.setEchoMode(QLineEdit.Password)
 
         ssh_form = QFormLayout()
-        ssh_form.addRow(_('Host name'), self.hn)
-        ssh_form.addRow(_('Ssh key'), kf_layout)
-        ssh_form.addRow(_('Password'), self.pw)
-        
+        ssh_form.addRow(_('Host name:'), self.hn)
+        ssh_form.addRow(_('Password:'), self.pw)
+        ssh_form.addRow(_('SSH keyfile:'), kf_layout)
+
         # Ok and Cancel buttons
         self.accept_btns = QDialogButtonBox(
             QDialogButtonBox.Ok | QDialogButtonBox.Cancel,
@@ -233,26 +239,28 @@ def __init__(self, parent=None):
         layout.addWidget(main_label)
         layout.addLayout(cf_layout)
         layout.addWidget(self.rm_cb)
+        layout.addWidget(credential_label)
         layout.addLayout(ssh_form)
         layout.addWidget(self.accept_btns)
-                
-        # remote kernel checkbox enables the ssh_connection_form
+
+        # Remote kernel checkbox enables the ssh_connection_form
         def ssh_set_enabled(state):
-            for wid in [self.hn, self.kf, kf_open_btn, self.pw]:
+            for wid in [credential_label, self.hn, self.pw,
+                        self.kf, kf_open_btn]:
                 wid.setEnabled(state)
             for i in range(ssh_form.rowCount()):
                 ssh_form.itemAt(2 * i).widget().setEnabled(state)
-       
+
         ssh_set_enabled(self.rm_cb.checkState())
         self.rm_cb.stateChanged.connect(ssh_set_enabled)
 
     def select_connection_file(self):
-        cf = getopenfilename(self, _('Open connection file'),
+        cf = getopenfilename(self, _('Select kernel connection file'),
                              jupyter_runtime_dir(), '*.json;;*.*')[0]
         self.cf.setText(cf)
 
     def select_ssh_key(self):
-        kf = getopenfilename(self, _('Select ssh key'),
+        kf = getopenfilename(self, _('Select SSH keyfile'),
                              get_home_dir(), '*.pem;;*.*')[0]
         self.kf.setText(kf)
 


Test patch: