##// END OF EJS Templates
split QtConsole.init_ssh into generic tunnel_to_kernel...
MinRK -
Show More
@@ -1,604 +1,606 b''
1 1 """ A minimal application using the Qt console-style IPython frontend.
2 2
3 3 This is not a complete console app, as subprocess will not be able to receive
4 4 input, there is no real readline support, among other limitations.
5 5
6 6 Authors:
7 7
8 8 * Evan Patterson
9 9 * Min RK
10 10 * Erik Tollerud
11 11 * Fernando Perez
12 12
13 13 """
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 # stdlib imports
20 20 import glob
21 21 import os
22 22 import signal
23 23 import sys
24 from getpass import getpass
25 24
26 25 # System library imports
27 26 from IPython.external.qt import QtGui
28 27 from pygments.styles import get_all_styles
29 28 from zmq.utils import jsonapi as json
30 29
31 # external imports
32 from IPython.external.ssh import tunnel
33
34 30 # Local imports
35 31 from IPython.config.application import boolean_flag
36 32 from IPython.core.application import BaseIPythonApplication
37 33 from IPython.core.profiledir import ProfileDir
34 from IPython.lib.kernel import tunnel_to_kernel
38 35 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
39 36 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
40 37 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
41 38 from IPython.frontend.qt.console import styles
42 39 from IPython.frontend.qt.kernelmanager import QtKernelManager
43 from IPython.parallel.util import select_random_ports
44 40 from IPython.utils.path import filefind
45 41 from IPython.utils.py3compat import str_to_bytes
46 42 from IPython.utils.traitlets import (
47 43 Dict, List, Unicode, Int, CaselessStrEnum, CBool, Any
48 44 )
49 45 from IPython.zmq.ipkernel import (
50 46 flags as ipkernel_flags,
51 47 aliases as ipkernel_aliases,
52 48 IPKernelApp
53 49 )
54 50 from IPython.zmq.session import Session, default_secure
55 51 from IPython.zmq.zmqshell import ZMQInteractiveShell
56 52
57 53
58 54 #-----------------------------------------------------------------------------
59 55 # Network Constants
60 56 #-----------------------------------------------------------------------------
61 57
62 58 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
63 59
64 60 #-----------------------------------------------------------------------------
65 61 # Globals
66 62 #-----------------------------------------------------------------------------
67 63
68 64 _examples = """
69 65 ipython qtconsole # start the qtconsole
70 66 ipython qtconsole --pylab=inline # start with pylab in inline plotting mode
71 67 """
72 68
73 69 #-----------------------------------------------------------------------------
74 70 # Classes
75 71 #-----------------------------------------------------------------------------
76 72
77 73 class MainWindow(QtGui.QMainWindow):
78 74
79 75 #---------------------------------------------------------------------------
80 76 # 'object' interface
81 77 #---------------------------------------------------------------------------
82 78
83 79 def __init__(self, app, frontend, existing=False, may_close=True,
84 80 confirm_exit=True):
85 81 """ Create a MainWindow for the specified FrontendWidget.
86 82
87 83 The app is passed as an argument to allow for different
88 84 closing behavior depending on whether we are the Kernel's parent.
89 85
90 86 If existing is True, then this Console does not own the Kernel.
91 87
92 88 If may_close is True, then this Console is permitted to close the kernel
93 89 """
94 90 super(MainWindow, self).__init__()
95 91 self._app = app
96 92 self._frontend = frontend
97 93 self._existing = existing
98 94 if existing:
99 95 self._may_close = may_close
100 96 else:
101 97 self._may_close = True
102 98 self._frontend.exit_requested.connect(self.close)
103 99 self._confirm_exit = confirm_exit
104 100 self.setCentralWidget(frontend)
105 101
106 102 #---------------------------------------------------------------------------
107 103 # QWidget interface
108 104 #---------------------------------------------------------------------------
109 105
110 106 def closeEvent(self, event):
111 107 """ Close the window and the kernel (if necessary).
112 108
113 109 This will prompt the user if they are finished with the kernel, and if
114 110 so, closes the kernel cleanly. Alternatively, if the exit magic is used,
115 111 it closes without prompt.
116 112 """
117 113 keepkernel = None #Use the prompt by default
118 114 if hasattr(self._frontend,'_keep_kernel_on_exit'): #set by exit magic
119 115 keepkernel = self._frontend._keep_kernel_on_exit
120 116
121 117 kernel_manager = self._frontend.kernel_manager
122 118
123 119 if keepkernel is None and not self._confirm_exit:
124 120 # don't prompt, just terminate the kernel if we own it
125 121 # or leave it alone if we don't
126 122 keepkernel = not self._existing
127 123
128 124 if keepkernel is None: #show prompt
129 125 if kernel_manager and kernel_manager.channels_running:
130 126 title = self.window().windowTitle()
131 127 cancel = QtGui.QMessageBox.Cancel
132 128 okay = QtGui.QMessageBox.Ok
133 129 if self._may_close:
134 130 msg = "You are closing this Console window."
135 131 info = "Would you like to quit the Kernel and all attached Consoles as well?"
136 132 justthis = QtGui.QPushButton("&No, just this Console", self)
137 133 justthis.setShortcut('N')
138 134 closeall = QtGui.QPushButton("&Yes, quit everything", self)
139 135 closeall.setShortcut('Y')
140 136 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
141 137 title, msg)
142 138 box.setInformativeText(info)
143 139 box.addButton(cancel)
144 140 box.addButton(justthis, QtGui.QMessageBox.NoRole)
145 141 box.addButton(closeall, QtGui.QMessageBox.YesRole)
146 142 box.setDefaultButton(closeall)
147 143 box.setEscapeButton(cancel)
148 144 reply = box.exec_()
149 145 if reply == 1: # close All
150 146 kernel_manager.shutdown_kernel()
151 147 #kernel_manager.stop_channels()
152 148 event.accept()
153 149 elif reply == 0: # close Console
154 150 if not self._existing:
155 151 # Have kernel: don't quit, just close the window
156 152 self._app.setQuitOnLastWindowClosed(False)
157 153 self.deleteLater()
158 154 event.accept()
159 155 else:
160 156 event.ignore()
161 157 else:
162 158 reply = QtGui.QMessageBox.question(self, title,
163 159 "Are you sure you want to close this Console?"+
164 160 "\nThe Kernel and other Consoles will remain active.",
165 161 okay|cancel,
166 162 defaultButton=okay
167 163 )
168 164 if reply == okay:
169 165 event.accept()
170 166 else:
171 167 event.ignore()
172 168 elif keepkernel: #close console but leave kernel running (no prompt)
173 169 if kernel_manager and kernel_manager.channels_running:
174 170 if not self._existing:
175 171 # I have the kernel: don't quit, just close the window
176 172 self._app.setQuitOnLastWindowClosed(False)
177 173 event.accept()
178 174 else: #close console and kernel (no prompt)
179 175 if kernel_manager and kernel_manager.channels_running:
180 176 kernel_manager.shutdown_kernel()
181 177 event.accept()
182 178
183 179 #-----------------------------------------------------------------------------
184 180 # Aliases and Flags
185 181 #-----------------------------------------------------------------------------
186 182
187 183 flags = dict(ipkernel_flags)
188 184 qt_flags = {
189 185 'existing' : ({'IPythonQtConsoleApp' : {'existing' : 'kernel*.json'}},
190 186 "Connect to an existing kernel. If no argument specified, guess most recent"),
191 187 'pure' : ({'IPythonQtConsoleApp' : {'pure' : True}},
192 188 "Use a pure Python kernel instead of an IPython kernel."),
193 189 'plain' : ({'ConsoleWidget' : {'kind' : 'plain'}},
194 190 "Disable rich text support."),
195 191 }
196 192 qt_flags.update(boolean_flag(
197 193 'gui-completion', 'ConsoleWidget.gui_completion',
198 194 "use a GUI widget for tab completion",
199 195 "use plaintext output for completion"
200 196 ))
201 197 qt_flags.update(boolean_flag(
202 198 'confirm-exit', 'IPythonQtConsoleApp.confirm_exit',
203 199 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
204 200 to force a direct exit without any confirmation.
205 201 """,
206 202 """Don't prompt the user when exiting. This will terminate the kernel
207 203 if it is owned by the frontend, and leave it alive if it is external.
208 204 """
209 205 ))
210 206 flags.update(qt_flags)
211 207
212 208 aliases = dict(ipkernel_aliases)
213 209
214 210 qt_aliases = dict(
215 211 hb = 'IPythonQtConsoleApp.hb_port',
216 212 shell = 'IPythonQtConsoleApp.shell_port',
217 213 iopub = 'IPythonQtConsoleApp.iopub_port',
218 214 stdin = 'IPythonQtConsoleApp.stdin_port',
219 215 ip = 'IPythonQtConsoleApp.ip',
220 216 existing = 'IPythonQtConsoleApp.existing',
221 217 f = 'IPythonQtConsoleApp.connection_file',
222 218
223 219 style = 'IPythonWidget.syntax_style',
224 220 stylesheet = 'IPythonQtConsoleApp.stylesheet',
225 221 colors = 'ZMQInteractiveShell.colors',
226 222
227 223 editor = 'IPythonWidget.editor',
228 224 paging = 'ConsoleWidget.paging',
229 225 ssh = 'IPythonQtConsoleApp.sshserver',
230 226 )
231 227 aliases.update(qt_aliases)
232 228
233 229
234 230 #-----------------------------------------------------------------------------
235 231 # IPythonQtConsole
236 232 #-----------------------------------------------------------------------------
237 233
238 234
239 235 class IPythonQtConsoleApp(BaseIPythonApplication):
240 236 name = 'ipython-qtconsole'
241 237 default_config_file_name='ipython_config.py'
242 238
243 239 description = """
244 240 The IPython QtConsole.
245 241
246 242 This launches a Console-style application using Qt. It is not a full
247 243 console, in that launched terminal subprocesses will not be able to accept
248 244 input.
249 245
250 246 The QtConsole supports various extra features beyond the Terminal IPython
251 247 shell, such as inline plotting with matplotlib, via:
252 248
253 249 ipython qtconsole --pylab=inline
254 250
255 251 as well as saving your session as HTML, and printing the output.
256 252
257 253 """
258 254 examples = _examples
259 255
260 256 classes = [IPKernelApp, IPythonWidget, ZMQInteractiveShell, ProfileDir, Session]
261 257 flags = Dict(flags)
262 258 aliases = Dict(aliases)
263 259
264 260 kernel_argv = List(Unicode)
265 261
266 262 # create requested profiles by default, if they don't exist:
267 263 auto_create = CBool(True)
268 264 # connection info:
269 265 ip = Unicode(LOCALHOST, config=True,
270 266 help="""Set the kernel\'s IP address [default localhost].
271 267 If the IP address is something other than localhost, then
272 268 Consoles on other machines will be able to connect
273 269 to the Kernel, so be careful!"""
274 270 )
275 271
276 272 sshserver = Unicode('', config=True,
277 273 help="""The SSH server to use to connect to the kernel.""")
278 274 sshkey = Unicode('', config=True,
279 275 help="""Path to the ssh key to use for logging in to the ssh server.""")
280 276
281 277 hb_port = Int(0, config=True,
282 278 help="set the heartbeat port [default: random]")
283 279 shell_port = Int(0, config=True,
284 280 help="set the shell (XREP) port [default: random]")
285 281 iopub_port = Int(0, config=True,
286 282 help="set the iopub (PUB) port [default: random]")
287 283 stdin_port = Int(0, config=True,
288 284 help="set the stdin (XREQ) port [default: random]")
289 285 connection_file = Unicode('', config=True,
290 286 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
291 287
292 288 This file will contain the IP, ports, and authentication key needed to connect
293 289 clients to this kernel. By default, this file will be created in the security-dir
294 290 of the current profile, but can be specified by absolute path.
295 291 """)
296 292 def _connection_file_default(self):
297 293 return 'kernel-%i.json' % os.getpid()
298 294
299 295 existing = Unicode('', config=True,
300 296 help="""Connect to an already running kernel""")
301 297
302 298 stylesheet = Unicode('', config=True,
303 299 help="path to a custom CSS stylesheet")
304 300
305 301 pure = CBool(False, config=True,
306 302 help="Use a pure Python kernel instead of an IPython kernel.")
307 303 plain = CBool(False, config=True,
308 304 help="Use a plaintext widget instead of rich text (plain can't print/save).")
309 305
310 306 def _pure_changed(self, name, old, new):
311 307 kind = 'plain' if self.plain else 'rich'
312 308 self.config.ConsoleWidget.kind = kind
313 309 if self.pure:
314 310 self.widget_factory = FrontendWidget
315 311 elif self.plain:
316 312 self.widget_factory = IPythonWidget
317 313 else:
318 314 self.widget_factory = RichIPythonWidget
319 315
320 316 _plain_changed = _pure_changed
321 317
322 318 confirm_exit = CBool(True, config=True,
323 319 help="""
324 320 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
325 321 to force a direct exit without any confirmation.""",
326 322 )
327 323
328 324 # the factory for creating a widget
329 325 widget_factory = Any(RichIPythonWidget)
330 326
331 327 def parse_command_line(self, argv=None):
332 328 super(IPythonQtConsoleApp, self).parse_command_line(argv)
333 329 if argv is None:
334 330 argv = sys.argv[1:]
335 331
336 332 self.kernel_argv = list(argv) # copy
337 333 # kernel should inherit default config file from frontend
338 334 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
339 335 # Scrub frontend-specific flags
340 336 for a in argv:
341 337 if a.startswith('-') and a.lstrip('-') in qt_flags:
342 338 self.kernel_argv.remove(a)
343 339 swallow_next = False
344 340 for a in argv:
345 341 if swallow_next:
346 342 self.kernel_argv.remove(a)
347 343 swallow_next = False
348 344 continue
349 345 if a.startswith('-'):
350 346 split = a.lstrip('-').split('=')[0]
351 347 alias = split[0]
352 348 if alias in qt_aliases:
353 349 self.kernel_argv.remove(a)
354 350 if len(split) == 1:
355 351 # alias passed with arg via space
356 352 swallow_next = True
357 353
358 354 def init_connection_file(self):
359 355 """find the connection file, and load the info if found.
360 356
361 357 The current working directory and the current profile's security
362 358 directory will be searched for the file if it is not given by
363 359 absolute path.
364 360
365 361 When attempting to connect to an existing kernel and the `--existing`
366 362 argument does not match an existing file, it will be interpreted as a
367 363 fileglob, and the matching file in the current profile's security dir
368 364 with the latest access time will be used.
369 365 """
370 366 sec = self.profile_dir.security_dir
371 367 if self.existing:
372 368 try:
373 369 # first, try explicit name
374 370 cf = filefind(self.existing, ['.', sec])
375 371 except IOError:
376 372 # not found by full name
377 373 if '*' in self.existing:
378 374 # given as a glob already
379 375 pat = self.existing
380 376 else:
381 377 # accept any substring match
382 378 pat = '*%s*' % self.existing
383 379 matches = glob.glob( os.path.join(sec, pat) )
384 380 if not matches:
385 381 self.log.critical("Could not find existing kernel connection file %s", self.existing)
386 382 self.exit(1)
387 383 else:
388 384 # get most recent match:
389 385 cf = sorted(matches, key=lambda f: os.stat(f).st_atime)[-1]
390 386 self.log.info("Connecting to existing kernel: %s" % cf)
391 387 self.connection_file = cf
392 388 # should load_connection_file only be used for existing?
393 389 # as it is now, this allows reusing ports if an existing
394 390 # file is requested
395 391 self.load_connection_file()
396 392
397 393 def load_connection_file(self):
398 394 """load ip/port/hmac config from JSON connection file"""
399 395 # this is identical to KernelApp.load_connection_file
400 396 # perhaps it can be centralized somewhere?
401 397 try:
402 398 fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
403 399 except IOError:
404 400 self.log.debug("Connection File not found: %s", self.connection_file)
405 401 return
406 402 self.log.debug(u"Loading connection file %s", fname)
407 403 with open(fname) as f:
408 404 s = f.read()
409 405 cfg = json.loads(s)
410 406 if self.ip == LOCALHOST and 'ip' in cfg:
411 407 # not overridden by config or cl_args
412 408 self.ip = cfg['ip']
413 409 for channel in ('hb', 'shell', 'iopub', 'stdin'):
414 410 name = channel + '_port'
415 411 if getattr(self, name) == 0 and name in cfg:
416 412 # not overridden by config or cl_args
417 413 setattr(self, name, cfg[name])
418 414 if 'key' in cfg:
419 415 self.config.Session.key = str_to_bytes(cfg['key'])
420 416
421 417 def init_ssh(self):
422 418 """set up ssh tunnels, if needed."""
423 419 if not self.sshserver and not self.sshkey:
424 420 return
425 421
426 422 if self.sshkey and not self.sshserver:
423 # specifying just the key implies that we are connecting directly
427 424 self.sshserver = self.ip
428 425 self.ip=LOCALHOST
429 426
430 lports = select_random_ports(4)
431 rports = self.shell_port, self.iopub_port, self.stdin_port, self.hb_port
432 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = lports
427 # build connection dict for tunnels:
428 info = dict(ip=self.ip,
429 shell_port=self.shell_port,
430 iopub_port=self.iopub_port,
431 stdin_port=self.stdin_port,
432 hb_port=self.hb_port
433 )
434
435 self.log.info("Forwarding connections to %s via %s"%(self.ip, self.sshserver))
433 436
434 remote_ip = self.ip
437 # tunnels return a new set of ports, which will be on localhost:
435 438 self.ip = LOCALHOST
436 self.log.info("Forwarding connections to %s via %s"%(remote_ip, self.sshserver))
437
438 if tunnel.try_passwordless_ssh(self.sshserver, self.sshkey):
439 password=False
440 else:
441 password = getpass("SSH Password for %s: "%self.sshserver)
439 try:
440 newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
441 except:
442 # even catch KeyboardInterrupt
443 self.log.error("Could not setup tunnels", exc_info=True)
444 self.exit(1)
442 445
443 for lp,rp in zip(lports, rports):
444 tunnel.ssh_tunnel(lp, rp, self.sshserver, remote_ip, self.sshkey, password)
446 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
445 447
446 448 cf = self.connection_file
447 449 base,ext = os.path.splitext(cf)
448 450 base = os.path.basename(base)
449 451 self.connection_file = os.path.basename(base)+'-ssh'+ext
450 452 self.log.critical("To connect another client via this tunnel, use:")
451 453 self.log.critical("--existing %s" % self.connection_file)
452 454
453 455 def init_kernel_manager(self):
454 456 # Don't let Qt or ZMQ swallow KeyboardInterupts.
455 457 signal.signal(signal.SIGINT, signal.SIG_DFL)
456 458 sec = self.profile_dir.security_dir
457 459 try:
458 460 cf = filefind(self.connection_file, ['.', sec])
459 461 except IOError:
460 462 # file might not exist
461 463 if self.connection_file == os.path.basename(self.connection_file):
462 464 # just shortname, put it in security dir
463 465 cf = os.path.join(sec, self.connection_file)
464 466 else:
465 467 cf = self.connection_file
466 468
467 469 # Create a KernelManager and start a kernel.
468 470 self.kernel_manager = QtKernelManager(
469 471 ip=self.ip,
470 472 shell_port=self.shell_port,
471 473 iopub_port=self.iopub_port,
472 474 stdin_port=self.stdin_port,
473 475 hb_port=self.hb_port,
474 476 connection_file=cf,
475 477 config=self.config,
476 478 )
477 479 # start the kernel
478 480 if not self.existing:
479 481 kwargs = dict(ipython=not self.pure)
480 482 kwargs['extra_arguments'] = self.kernel_argv
481 483 self.kernel_manager.start_kernel(**kwargs)
482 484 elif self.sshserver:
483 485 # ssh, write new connection file
484 486 self.kernel_manager.write_connection_file()
485 487 self.kernel_manager.start_channels()
486 488
487 489
488 490 def init_qt_elements(self):
489 491 # Create the widget.
490 492 self.app = QtGui.QApplication([])
491 493 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
492 494 self.widget = self.widget_factory(config=self.config,
493 495 local_kernel=local_kernel)
494 496 self.widget.kernel_manager = self.kernel_manager
495 497 self.window = MainWindow(self.app, self.widget, self.existing,
496 498 may_close=local_kernel,
497 499 confirm_exit=self.confirm_exit)
498 500 self.window.setWindowTitle('Python' if self.pure else 'IPython')
499 501
500 502 def init_colors(self):
501 503 """Configure the coloring of the widget"""
502 504 # Note: This will be dramatically simplified when colors
503 505 # are removed from the backend.
504 506
505 507 if self.pure:
506 508 # only IPythonWidget supports styling
507 509 return
508 510
509 511 # parse the colors arg down to current known labels
510 512 try:
511 513 colors = self.config.ZMQInteractiveShell.colors
512 514 except AttributeError:
513 515 colors = None
514 516 try:
515 517 style = self.config.IPythonWidget.colors
516 518 except AttributeError:
517 519 style = None
518 520
519 521 # find the value for colors:
520 522 if colors:
521 523 colors=colors.lower()
522 524 if colors in ('lightbg', 'light'):
523 525 colors='lightbg'
524 526 elif colors in ('dark', 'linux'):
525 527 colors='linux'
526 528 else:
527 529 colors='nocolor'
528 530 elif style:
529 531 if style=='bw':
530 532 colors='nocolor'
531 533 elif styles.dark_style(style):
532 534 colors='linux'
533 535 else:
534 536 colors='lightbg'
535 537 else:
536 538 colors=None
537 539
538 540 # Configure the style.
539 541 widget = self.widget
540 542 if style:
541 543 widget.style_sheet = styles.sheet_from_template(style, colors)
542 544 widget.syntax_style = style
543 545 widget._syntax_style_changed()
544 546 widget._style_sheet_changed()
545 547 elif colors:
546 548 # use a default style
547 549 widget.set_default_style(colors=colors)
548 550 else:
549 551 # this is redundant for now, but allows the widget's
550 552 # defaults to change
551 553 widget.set_default_style()
552 554
553 555 if self.stylesheet:
554 556 # we got an expicit stylesheet
555 557 if os.path.isfile(self.stylesheet):
556 558 with open(self.stylesheet) as f:
557 559 sheet = f.read()
558 560 widget.style_sheet = sheet
559 561 widget._style_sheet_changed()
560 562 else:
561 563 raise IOError("Stylesheet %r not found."%self.stylesheet)
562 564
563 565 def initialize(self, argv=None):
564 566 super(IPythonQtConsoleApp, self).initialize(argv)
565 567 self.init_connection_file()
566 568 default_secure(self.config)
567 569 self.init_ssh()
568 570 self.init_kernel_manager()
569 571 self.init_qt_elements()
570 572 self.init_colors()
571 573 self.init_window_shortcut()
572 574
573 575 def init_window_shortcut(self):
574 576 fullScreenAction = QtGui.QAction('Toggle Full Screen', self.window)
575 577 fullScreenAction.setShortcut('Ctrl+Meta+Space')
576 578 fullScreenAction.triggered.connect(self.toggleFullScreen)
577 579 self.window.addAction(fullScreenAction)
578 580
579 581 def toggleFullScreen(self):
580 582 if not self.window.isFullScreen():
581 583 self.window.showFullScreen()
582 584 else:
583 585 self.window.showNormal()
584 586
585 587 def start(self):
586 588
587 589 # draw the window
588 590 self.window.show()
589 591
590 592 # Start the application main loop.
591 593 self.app.exec_()
592 594
593 595 #-----------------------------------------------------------------------------
594 596 # Main entry point
595 597 #-----------------------------------------------------------------------------
596 598
597 599 def main():
598 600 app = IPythonQtConsoleApp()
599 601 app.initialize()
600 602 app.start()
601 603
602 604
603 605 if __name__ == '__main__':
604 606 main()
@@ -1,99 +1,152 b''
1 1 """Utilities for connecting to kernels
2 2
3 3 Authors:
4 4
5 5 * Min Ragan-Kelley
6 6
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2011 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 import json
21 21 import sys
22 from getpass import getpass
22 23 from subprocess import Popen, PIPE
23 24
25 # external imports
26 from IPython.external.ssh import tunnel
27
28 # IPython imports
24 29 from IPython.utils.path import filefind
25 30 from IPython.utils.py3compat import str_to_bytes
26 31
27 32
28 33 #-----------------------------------------------------------------------------
29 34 # Functions
30 35 #-----------------------------------------------------------------------------
31 36
32 37 def get_connection_file(app=None):
33 38 """Return the path to the connection file of an app
34 39
35 40 Parameters
36 41 ----------
37 42 app : KernelApp instance [optional]
38 43 If unspecified, the currently running app will be used
39 44 """
40 45 if app is None:
41 46 from IPython.zmq.kernelapp import KernelApp
42 47 if not KernelApp.initialized():
43 48 raise RuntimeError("app not specified, and not in a running Kernel")
44 49
45 50 app = KernelApp.instance()
46 51 return filefind(app.connection_file, ['.', app.profile_dir.security_dir])
47 52
48 53 def get_connection_info(unpack=False):
49 54 """Return the connection information for the current Kernel.
50 55
51 56 Parameters
52 57 ----------
53 58 unpack : bool [default: False]
54 59 if True, return the unpacked dict, otherwise just the string contents
55 60 of the file.
56 61
57 62 Returns
58 63 -------
59 64 The connection dictionary of the current kernel, as string or dict,
60 65 depending on `unpack`.
61 66 """
62 67 cf = get_connection_file()
63 68 with open(cf) as f:
64 69 info = f.read()
65 70
66 71 if unpack:
67 72 info = json.loads(info)
68 73 # ensure key is bytes:
69 74 info['key'] = str_to_bytes(info.get('key', ''))
70 75 return info
71 76
72 77 def connect_qtconsole(argv=None):
73 78 """Connect a qtconsole to the current kernel.
74 79
75 80 This is useful for connecting a second qtconsole to a kernel, or to a
76 81 local notebook.
77 82
78 83 Parameters
79 84 ----------
80 85 argv : list [optional]
81 86 Any extra args to be passed to the console.
82 87
83 88 Returns
84 89 -------
85 90 subprocess.Popen instance running the qtconsole frontend
86 91 """
87 92 argv = [] if argv is None else argv
88 93
89 94 # get connection file from current kernel
90 95 cf = get_connection_file()
91 96
92 97 cmd = ';'.join([
93 98 "from IPython.frontend.qt.console import qtconsoleapp",
94 99 "qtconsoleapp.main()"
95 100 ])
96 101
97 102 return Popen([sys.executable, '-c', cmd, '--existing', cf] + argv, stdout=PIPE, stderr=PIPE)
98 103
104 def tunnel_to_kernel(connection_info, sshserver, sshkey=None):
105 """tunnel connections to a kernel via ssh
106
107 This will open four SSH tunnels from localhost on this machine to the
108 ports associated with the kernel. They can be either direct
109 localhost-localhost tunnels, or if an intermediate server is necessary,
110 the kernel must be listening on a public IP.
111
112 Parameters
113 ----------
114 connection_info : dict or str (path)
115 Either a connection dict, or the path to a JSON connection file
116 sshserver : str
117 The ssh sever to use to tunnel to the kernel. Can be a full
118 `user@server:port` string. ssh config aliases are respected.
119 sshkey : str [optional]
120 Path to file containing ssh key to use for authentication.
121 Only necessary if your ssh config does not already associate
122 a keyfile with the host.
123
124 Returns
125 -------
126
127 (shell, iopub, stdin, hb) : ints
128 The four ports on localhost that have been forwarded to the kernel.
129 """
130 if isinstance(connection_info, basestring):
131 # it's a path, unpack it
132 with open(connection_info) as f:
133 connection_info = json.loads(f.read())
134
135 cf = connection_info
136
137 lports = tunnel.select_random_ports(4)
138 rports = cf['shell_port'], cf['iopub_port'], cf['stdin_port'], cf['hb_port']
139
140 remote_ip = cf['ip']
141
142 if tunnel.try_passwordless_ssh(sshserver, sshkey):
143 password=False
144 else:
145 password = getpass("SSH Password for %s: "%sshserver)
146
147 for lp,rp in zip(lports, rports):
148 tunnel.ssh_tunnel(lp, rp, sshserver, remote_ip, sshkey, password)
149
150 return tuple(lports)
151
99 152
General Comments 0
You need to be logged in to leave comments. Login now