Skip to content

Commit 3661c67

Browse files
committed
implement remote kernel connection through SSH
This uses the --ssh option of the ipython console app to create the necessary tunnels and create a new connection file with "-ssh" added (because it may use different ports). Therefore, the REPL is created before the driver. The connection file naming convention of "emacs-*.ssh" is abandoned in order to support arbitrary remote kernel connection files. Fixes #11.
1 parent 15011a8 commit 3661c67

3 files changed

Lines changed: 102 additions & 17 deletions

File tree

README.org

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,62 @@
154154
: (2, [['a', 1, 2], ['b', 2, 3], ['c', 3, 4]])
155155
#+END_SRC
156156

157+
*** SSH connection to remote kernels
158+
159+
It is possible to connect to a remote kernel running on some server to which SSH
160+
tunnels can be created. Usually the kernel process is started in a terminal
161+
multiplexer like =screen= or =tmux= to keep it running after disconnecting. When
162+
a kernel is started on the remote server using the following command
163+
164+
#+BEGIN_SRC sh
165+
ipython kernel
166+
#+END_SRC
167+
168+
The name of the connection file for later use with the =--existing= option of
169+
=ipython= is printed out. By default has the form =kernel-${PID}.json=, but a
170+
custom name can be specified with
171+
172+
#+BEGIN_SRC sh
173+
ipython kernel --IPKernelApp.connection_file=${custom_name}.json
174+
#+END_SRC
175+
176+
The connection file is written in the runtime directory on the server when using
177+
Jupyter (IPython >= 4.0) which is usually =/run/user/${UID}/jupyter/= or falls
178+
back to =${HOME}/.local/share/jupyter/runtime/=. The exact location can be found
179+
on the server with
180+
181+
#+BEGIN_SRC sh
182+
python -c "from jupyter_core.paths import jupyter_runtime_dir as p; print(p())"
183+
#+END_SRC
184+
185+
When using the older IPython < 4.0, the connection file is written in the
186+
=security/= subdirectory of the current profile, e.g.
187+
=$HOME/.ipython/profile_default/security/= with the default profile. The profile
188+
directory can also be found with =ipython locate=.
189+
190+
191+
The connection contains connection information (secret hash, ports) and has to
192+
be transferred from the server to the appropriate directory on the client
193+
machine, e.g. with the standard runtime directory with Jupyter it could be
194+
195+
#+BEGIN_SRC sh
196+
scp server:/run/user/\$UID/jupyter/${connnection_file} /run/user/$UID/jupyter
197+
#+END_SRC
198+
199+
The backslash before the first =$= makes sure the user id on the server is used
200+
in the source path.
201+
202+
Finally, the remote session can be connected through specifying both of the
203+
following arguments in the source blocks:
204+
- =:session ${connection_file_basename}= :: Will be used to find the
205+
connection file, specified without the =.json= extension.
206+
- =:ssh ${server_name_or_ip}= :: Will be passed to the =--ssh= option of
207+
=ipython console= which will establish needed SSH tunnels (nothing more, no
208+
shell session is created) and write a modified connection file (that is
209+
handled internally). It is best to have SSH configured in such a way that
210+
the user does not have to be specified (option =User= in ssh configuration)
211+
and possibly password-less login through SSH keys (option =IdentityFile= in
212+
ssh configuration).
157213
** What features are there outside of Org SRC block evaluation?
158214

159215
* You can ask the running IPython kernel for documentation. Open a
@@ -211,10 +267,28 @@
211267

212268
* Open a REPL using =C-c C-v C-z= so that you get completion in Python buffers.
213269

270+
* The source block header arguments can be set as a special property
271+
272+
#+BEGIN_SRC org
273+
#+PROPERTY: header-args:ipython :session kernel_name :ssh remote_server
274+
#+END_SRC
275+
and then they don't have to be specified for each cell, they are used during
276+
execution automatically. However, they are set only after (re)loading the Org
277+
file or by =C-c C-c= on this line.
278+
214279
** Help, it doesn't work
215280

216281
First thing to do is check that you have all of the required
217282
dependencies. Several common problems have been resolved in the
218283
project's issues, so take a look there to see if your problem has a
219284
quick fix. Otherwise feel free to cut an issue - I'll do my best to
220285
help.
286+
*** Errors during and/or long session start-up
287+
This is because it takes a few seconds for the driver server process to bind to
288+
a port and the REPL to connect to the kernel and possibly establishing SSH
289+
tunnels. Executing code before the session is fully established can result in
290+
errors. Because there is no simple way (to the best knowledge of the authors) to
291+
find out when the session is fully established, the code execution is postponed
292+
by several seconds. This waiting period can be customized by the
293+
`ob-ipython-connection-wait` variable in case it is too short or needlessly
294+
long.

driver.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
try: # Jupyter and IPython >= 4.0
22
import jupyter_client as client
3-
find_connection_file = client.find_connection_file
3+
client_utils = client
44
except ImportError: # IPython 3
5-
from IPython.lib.kernel import find_connection_file
5+
import IPython.lib.kernel as client_utils
66
import IPython.kernel.blocking.client as client
77

88
import sys
@@ -43,7 +43,7 @@ def msg_router(name, ch):
4343
clients = {}
4444

4545
def create_client(name):
46-
cf = find_connection_file('emacs-' + name)
46+
cf = client_utils.find_connection_file(name + '.json')
4747
c = client.BlockingKernelClient(connection_file=cf)
4848
c.load_connection_file()
4949
c.start_channels()
@@ -105,8 +105,8 @@ def get(self):
105105

106106
def make_app():
107107
return tornado.web.Application([
108-
tornado.web.url(r"/execute/(\w+)", ExecuteHandler),
109-
tornado.web.url(r"/inspect/(\w+)", InspectHandler),
108+
tornado.web.url(r"/execute/(\S+)", ExecuteHandler),
109+
tornado.web.url(r"/inspect/(\S+)", InspectHandler),
110110
tornado.web.url(r"/debug", DebugHandler),
111111
])
112112

ob-ipython.el

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@
6262
"Path to the driver script."
6363
:group 'ob-ipython)
6464

65+
(defcustom ob-ipython-connection-wait 2
66+
"Seconds to wait for connections to be established."
67+
:group 'ob-ipython)
68+
6569
;;; utils
6670

6771
(defun ob-ipython--write-base64-string (file b64-string)
@@ -120,11 +124,12 @@
120124
;;; process management
121125

122126
(defun ob-ipython--kernel-cmd (name)
123-
(-concat (list "ipython" "kernel" (format "--IPKernelApp.connection_file=emacs-%s.json" name))
127+
(-concat (list "ipython" "kernel" (format "--IPKernelApp.connection_file=%s.json" name))
124128
ob-ipython-kernel-extra-args))
125129

126-
(defun ob-ipython--kernel-repl-cmd (name)
127-
(list "ipython" "console" "--existing" (format "emacs-%s.json" name)))
130+
(defun ob-ipython--kernel-repl-cmd (name ssh)
131+
(-concat (list "ipython" "console" "--existing" (format "%s.json" name))
132+
(if ssh (list "--ssh" ssh))))
128133

129134
(defun ob-ipython--create-process (name cmd)
130135
(apply 'start-process name (format "*ob-ipython-%s*" name) (car cmd) (cdr cmd)))
@@ -153,14 +158,17 @@
153158
(number-to-string ob-ipython-driver-port)))
154159
;; give driver a chance to bind to a port and start serving
155160
;; requests. so horrible; so effective.
156-
(sleep-for 1)))
161+
(sleep-for ob-ipython-connection-wait)))
157162

158163
(defun ob-ipython--get-driver-process ()
159164
(get-process "ob-ipython-driver"))
160165

161-
(defun ob-ipython--create-repl (name)
162-
(run-python (s-join " " (ob-ipython--kernel-repl-cmd name)) nil nil)
163-
(format "*%s*" python-shell-buffer-name))
166+
(defun ob-ipython--create-repl (name ssh)
167+
(run-python (s-join " " (ob-ipython--kernel-repl-cmd name ssh)) nil nil)
168+
(format "*%s*" python-shell-buffer-name)
169+
;; SSH tunnels take some time to establish and we must wait for the modified
170+
;; connection file to be written for the driver
171+
(if ssh (sleep-for ob-ipython-connection-wait)))
164172

165173
;;; kernel management
166174

@@ -297,13 +305,14 @@ a new kernel will be started."
297305
This function is called by `org-babel-execute-src-block'."
298306
(let* ((file (cdr (assoc :file params)))
299307
(session (cdr (assoc :session params)))
308+
(ssh (cdr (assoc :ssh params)))
300309
(result-type (cdr (assoc :result-type params))))
301-
(org-babel-ipython-initiate-session session)
310+
(org-babel-ipython-initiate-session session params)
302311
(-when-let (ret (ob-ipython--eval
303312
(ob-ipython--execute-request
304313
(org-babel-expand-body:generic (encode-coding-string body 'utf-8)
305314
params (org-babel-variable-assignments:python params))
306-
(ob-ipython--normalize-session session))))
315+
(ob-ipython--normalize-session (if ssh (concat session "-ssh") session)))))
307316
(let ((result (cdr (assoc :result ret)))
308317
(output (cdr (assoc :output ret))))
309318
(if (eq result-type 'output)
@@ -330,9 +339,11 @@ VARS contains resolved variable references"
330339
(if (string= session "none")
331340
(error "ob-ipython currently only supports evaluation using a session.
332341
Make sure your src block has a :session param.")
333-
(ob-ipython--create-driver)
334-
(ob-ipython--create-kernel (ob-ipython--normalize-session session))
335-
(ob-ipython--create-repl (ob-ipython--normalize-session session))))
342+
(let ((ssh (cdr (assoc :ssh params)))
343+
(nsession (ob-ipython--normalize-session session)))
344+
(if (not ssh) (ob-ipython--create-kernel nsession))
345+
(ob-ipython--create-repl nsession ssh)
346+
(ob-ipython--create-driver))))
336347

337348
(provide 'ob-ipython)
338349

0 commit comments

Comments
 (0)