##// END OF EJS Templates
Linkify some API docs
Thomas Kluyver -
Show More
@@ -1,571 +1,570 b''
1 1 """Utilities for connecting to kernels
2 2
3 Notable contents:
4 - ConnectionFileMixin class
5 encapsulates the logic related to writing and reading connections files.
3 The :class:`ConnectionFileMixin` class in this module encapsulates the logic
4 related to writing and reading connections files.
6 5 """
7 6 # Copyright (c) IPython Development Team.
8 7 # Distributed under the terms of the Modified BSD License.
9 8
10 9 #-----------------------------------------------------------------------------
11 10 # Imports
12 11 #-----------------------------------------------------------------------------
13 12
14 13 from __future__ import absolute_import
15 14
16 15 import glob
17 16 import json
18 17 import os
19 18 import socket
20 19 import sys
21 20 from getpass import getpass
22 21 from subprocess import Popen, PIPE
23 22 import tempfile
24 23
25 24 import zmq
26 25
27 26 # external imports
28 27 from IPython.external.ssh import tunnel
29 28
30 29 # IPython imports
31 30 from IPython.config import Configurable
32 31 from IPython.core.profiledir import ProfileDir
33 32 from IPython.utils.localinterfaces import localhost
34 33 from IPython.utils.path import filefind, get_ipython_dir
35 34 from IPython.utils.py3compat import (str_to_bytes, bytes_to_str, cast_bytes_py2,
36 35 string_types)
37 36 from IPython.utils.traitlets import (
38 37 Bool, Integer, Unicode, CaselessStrEnum,
39 38 )
40 39
41 40
42 41 #-----------------------------------------------------------------------------
43 42 # Working with Connection Files
44 43 #-----------------------------------------------------------------------------
45 44
46 45 def write_connection_file(fname=None, shell_port=0, iopub_port=0, stdin_port=0, hb_port=0,
47 46 control_port=0, ip='', key=b'', transport='tcp',
48 47 signature_scheme='hmac-sha256',
49 48 ):
50 49 """Generates a JSON config file, including the selection of random ports.
51 50
52 51 Parameters
53 52 ----------
54 53
55 54 fname : unicode
56 55 The path to the file to write
57 56
58 57 shell_port : int, optional
59 58 The port to use for ROUTER (shell) channel.
60 59
61 60 iopub_port : int, optional
62 61 The port to use for the SUB channel.
63 62
64 63 stdin_port : int, optional
65 64 The port to use for the ROUTER (raw input) channel.
66 65
67 66 control_port : int, optional
68 67 The port to use for the ROUTER (control) channel.
69 68
70 69 hb_port : int, optional
71 70 The port to use for the heartbeat REP channel.
72 71
73 72 ip : str, optional
74 73 The ip address the kernel will bind to.
75 74
76 75 key : str, optional
77 76 The Session key used for message authentication.
78 77
79 78 signature_scheme : str, optional
80 79 The scheme used for message authentication.
81 80 This has the form 'digest-hash', where 'digest'
82 81 is the scheme used for digests, and 'hash' is the name of the hash function
83 82 used by the digest scheme.
84 83 Currently, 'hmac' is the only supported digest scheme,
85 84 and 'sha256' is the default hash function.
86 85
87 86 """
88 87 if not ip:
89 88 ip = localhost()
90 89 # default to temporary connector file
91 90 if not fname:
92 91 fd, fname = tempfile.mkstemp('.json')
93 92 os.close(fd)
94 93
95 94 # Find open ports as necessary.
96 95
97 96 ports = []
98 97 ports_needed = int(shell_port <= 0) + \
99 98 int(iopub_port <= 0) + \
100 99 int(stdin_port <= 0) + \
101 100 int(control_port <= 0) + \
102 101 int(hb_port <= 0)
103 102 if transport == 'tcp':
104 103 for i in range(ports_needed):
105 104 sock = socket.socket()
106 105 # struct.pack('ii', (0,0)) is 8 null bytes
107 106 sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, b'\0' * 8)
108 107 sock.bind(('', 0))
109 108 ports.append(sock)
110 109 for i, sock in enumerate(ports):
111 110 port = sock.getsockname()[1]
112 111 sock.close()
113 112 ports[i] = port
114 113 else:
115 114 N = 1
116 115 for i in range(ports_needed):
117 116 while os.path.exists("%s-%s" % (ip, str(N))):
118 117 N += 1
119 118 ports.append(N)
120 119 N += 1
121 120 if shell_port <= 0:
122 121 shell_port = ports.pop(0)
123 122 if iopub_port <= 0:
124 123 iopub_port = ports.pop(0)
125 124 if stdin_port <= 0:
126 125 stdin_port = ports.pop(0)
127 126 if control_port <= 0:
128 127 control_port = ports.pop(0)
129 128 if hb_port <= 0:
130 129 hb_port = ports.pop(0)
131 130
132 131 cfg = dict( shell_port=shell_port,
133 132 iopub_port=iopub_port,
134 133 stdin_port=stdin_port,
135 134 control_port=control_port,
136 135 hb_port=hb_port,
137 136 )
138 137 cfg['ip'] = ip
139 138 cfg['key'] = bytes_to_str(key)
140 139 cfg['transport'] = transport
141 140 cfg['signature_scheme'] = signature_scheme
142 141
143 142 with open(fname, 'w') as f:
144 143 f.write(json.dumps(cfg, indent=2))
145 144
146 145 return fname, cfg
147 146
148 147
149 148 def get_connection_file(app=None):
150 149 """Return the path to the connection file of an app
151 150
152 151 Parameters
153 152 ----------
154 153 app : IPKernelApp instance [optional]
155 154 If unspecified, the currently running app will be used
156 155 """
157 156 if app is None:
158 157 from IPython.kernel.zmq.kernelapp import IPKernelApp
159 158 if not IPKernelApp.initialized():
160 159 raise RuntimeError("app not specified, and not in a running Kernel")
161 160
162 161 app = IPKernelApp.instance()
163 162 return filefind(app.connection_file, ['.', app.profile_dir.security_dir])
164 163
165 164
166 165 def find_connection_file(filename, profile=None):
167 166 """find a connection file, and return its absolute path.
168 167
169 168 The current working directory and the profile's security
170 169 directory will be searched for the file if it is not given by
171 170 absolute path.
172 171
173 172 If profile is unspecified, then the current running application's
174 173 profile will be used, or 'default', if not run from IPython.
175 174
176 175 If the argument does not match an existing file, it will be interpreted as a
177 176 fileglob, and the matching file in the profile's security dir with
178 177 the latest access time will be used.
179 178
180 179 Parameters
181 180 ----------
182 181 filename : str
183 182 The connection file or fileglob to search for.
184 183 profile : str [optional]
185 184 The name of the profile to use when searching for the connection file,
186 185 if different from the current IPython session or 'default'.
187 186
188 187 Returns
189 188 -------
190 189 str : The absolute path of the connection file.
191 190 """
192 191 from IPython.core.application import BaseIPythonApplication as IPApp
193 192 try:
194 193 # quick check for absolute path, before going through logic
195 194 return filefind(filename)
196 195 except IOError:
197 196 pass
198 197
199 198 if profile is None:
200 199 # profile unspecified, check if running from an IPython app
201 200 if IPApp.initialized():
202 201 app = IPApp.instance()
203 202 profile_dir = app.profile_dir
204 203 else:
205 204 # not running in IPython, use default profile
206 205 profile_dir = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), 'default')
207 206 else:
208 207 # find profiledir by profile name:
209 208 profile_dir = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), profile)
210 209 security_dir = profile_dir.security_dir
211 210
212 211 try:
213 212 # first, try explicit name
214 213 return filefind(filename, ['.', security_dir])
215 214 except IOError:
216 215 pass
217 216
218 217 # not found by full name
219 218
220 219 if '*' in filename:
221 220 # given as a glob already
222 221 pat = filename
223 222 else:
224 223 # accept any substring match
225 224 pat = '*%s*' % filename
226 225 matches = glob.glob( os.path.join(security_dir, pat) )
227 226 if not matches:
228 227 raise IOError("Could not find %r in %r" % (filename, security_dir))
229 228 elif len(matches) == 1:
230 229 return matches[0]
231 230 else:
232 231 # get most recent match, by access time:
233 232 return sorted(matches, key=lambda f: os.stat(f).st_atime)[-1]
234 233
235 234
236 235 def get_connection_info(connection_file=None, unpack=False, profile=None):
237 236 """Return the connection information for the current Kernel.
238 237
239 238 Parameters
240 239 ----------
241 240 connection_file : str [optional]
242 241 The connection file to be used. Can be given by absolute path, or
243 242 IPython will search in the security directory of a given profile.
244 243 If run from IPython,
245 244
246 245 If unspecified, the connection file for the currently running
247 246 IPython Kernel will be used, which is only allowed from inside a kernel.
248 247 unpack : bool [default: False]
249 248 if True, return the unpacked dict, otherwise just the string contents
250 249 of the file.
251 250 profile : str [optional]
252 251 The name of the profile to use when searching for the connection file,
253 252 if different from the current IPython session or 'default'.
254 253
255 254
256 255 Returns
257 256 -------
258 257 The connection dictionary of the current kernel, as string or dict,
259 258 depending on `unpack`.
260 259 """
261 260 if connection_file is None:
262 261 # get connection file from current kernel
263 262 cf = get_connection_file()
264 263 else:
265 264 # connection file specified, allow shortnames:
266 265 cf = find_connection_file(connection_file, profile=profile)
267 266
268 267 with open(cf) as f:
269 268 info = f.read()
270 269
271 270 if unpack:
272 271 info = json.loads(info)
273 272 # ensure key is bytes:
274 273 info['key'] = str_to_bytes(info.get('key', ''))
275 274 return info
276 275
277 276
278 277 def connect_qtconsole(connection_file=None, argv=None, profile=None):
279 278 """Connect a qtconsole to the current kernel.
280 279
281 280 This is useful for connecting a second qtconsole to a kernel, or to a
282 281 local notebook.
283 282
284 283 Parameters
285 284 ----------
286 285 connection_file : str [optional]
287 286 The connection file to be used. Can be given by absolute path, or
288 287 IPython will search in the security directory of a given profile.
289 288 If run from IPython,
290 289
291 290 If unspecified, the connection file for the currently running
292 291 IPython Kernel will be used, which is only allowed from inside a kernel.
293 292 argv : list [optional]
294 293 Any extra args to be passed to the console.
295 294 profile : str [optional]
296 295 The name of the profile to use when searching for the connection file,
297 296 if different from the current IPython session or 'default'.
298 297
299 298
300 299 Returns
301 300 -------
302 subprocess.Popen instance running the qtconsole frontend
301 :class:`subprocess.Popen` instance running the qtconsole frontend
303 302 """
304 303 argv = [] if argv is None else argv
305 304
306 305 if connection_file is None:
307 306 # get connection file from current kernel
308 307 cf = get_connection_file()
309 308 else:
310 309 cf = find_connection_file(connection_file, profile=profile)
311 310
312 311 cmd = ';'.join([
313 312 "from IPython.qt.console import qtconsoleapp",
314 313 "qtconsoleapp.main()"
315 314 ])
316 315
317 316 return Popen([sys.executable, '-c', cmd, '--existing', cf] + argv,
318 317 stdout=PIPE, stderr=PIPE, close_fds=(sys.platform != 'win32'),
319 318 )
320 319
321 320
322 321 def tunnel_to_kernel(connection_info, sshserver, sshkey=None):
323 322 """tunnel connections to a kernel via ssh
324 323
325 324 This will open four SSH tunnels from localhost on this machine to the
326 325 ports associated with the kernel. They can be either direct
327 326 localhost-localhost tunnels, or if an intermediate server is necessary,
328 327 the kernel must be listening on a public IP.
329 328
330 329 Parameters
331 330 ----------
332 331 connection_info : dict or str (path)
333 332 Either a connection dict, or the path to a JSON connection file
334 333 sshserver : str
335 334 The ssh sever to use to tunnel to the kernel. Can be a full
336 335 `user@server:port` string. ssh config aliases are respected.
337 336 sshkey : str [optional]
338 337 Path to file containing ssh key to use for authentication.
339 338 Only necessary if your ssh config does not already associate
340 339 a keyfile with the host.
341 340
342 341 Returns
343 342 -------
344 343
345 344 (shell, iopub, stdin, hb) : ints
346 345 The four ports on localhost that have been forwarded to the kernel.
347 346 """
348 347 if isinstance(connection_info, string_types):
349 348 # it's a path, unpack it
350 349 with open(connection_info) as f:
351 350 connection_info = json.loads(f.read())
352 351
353 352 cf = connection_info
354 353
355 354 lports = tunnel.select_random_ports(4)
356 355 rports = cf['shell_port'], cf['iopub_port'], cf['stdin_port'], cf['hb_port']
357 356
358 357 remote_ip = cf['ip']
359 358
360 359 if tunnel.try_passwordless_ssh(sshserver, sshkey):
361 360 password=False
362 361 else:
363 362 password = getpass("SSH Password for %s: " % cast_bytes_py2(sshserver))
364 363
365 364 for lp,rp in zip(lports, rports):
366 365 tunnel.ssh_tunnel(lp, rp, sshserver, remote_ip, sshkey, password)
367 366
368 367 return tuple(lports)
369 368
370 369
371 370 #-----------------------------------------------------------------------------
372 371 # Mixin for classes that work with connection files
373 372 #-----------------------------------------------------------------------------
374 373
375 374 channel_socket_types = {
376 375 'hb' : zmq.REQ,
377 376 'shell' : zmq.DEALER,
378 377 'iopub' : zmq.SUB,
379 378 'stdin' : zmq.DEALER,
380 379 'control': zmq.DEALER,
381 380 }
382 381
383 382 port_names = [ "%s_port" % channel for channel in ('shell', 'stdin', 'iopub', 'hb', 'control')]
384 383
385 384 class ConnectionFileMixin(Configurable):
386 385 """Mixin for configurable classes that work with connection files"""
387 386
388 387 # The addresses for the communication channels
389 388 connection_file = Unicode('', config=True,
390 389 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
391 390
392 391 This file will contain the IP, ports, and authentication key needed to connect
393 392 clients to this kernel. By default, this file will be created in the security dir
394 393 of the current profile, but can be specified by absolute path.
395 394 """)
396 395 _connection_file_written = Bool(False)
397 396
398 397 transport = CaselessStrEnum(['tcp', 'ipc'], default_value='tcp', config=True)
399 398
400 399 ip = Unicode(config=True,
401 400 help="""Set the kernel\'s IP address [default localhost].
402 401 If the IP address is something other than localhost, then
403 402 Consoles on other machines will be able to connect
404 403 to the Kernel, so be careful!"""
405 404 )
406 405
407 406 def _ip_default(self):
408 407 if self.transport == 'ipc':
409 408 if self.connection_file:
410 409 return os.path.splitext(self.connection_file)[0] + '-ipc'
411 410 else:
412 411 return 'kernel-ipc'
413 412 else:
414 413 return localhost()
415 414
416 415 def _ip_changed(self, name, old, new):
417 416 if new == '*':
418 417 self.ip = '0.0.0.0'
419 418
420 419 # protected traits
421 420
422 421 hb_port = Integer(0, config=True,
423 422 help="set the heartbeat port [default: random]")
424 423 shell_port = Integer(0, config=True,
425 424 help="set the shell (ROUTER) port [default: random]")
426 425 iopub_port = Integer(0, config=True,
427 426 help="set the iopub (PUB) port [default: random]")
428 427 stdin_port = Integer(0, config=True,
429 428 help="set the stdin (ROUTER) port [default: random]")
430 429 control_port = Integer(0, config=True,
431 430 help="set the control (ROUTER) port [default: random]")
432 431
433 432 @property
434 433 def ports(self):
435 434 return [ getattr(self, name) for name in port_names ]
436 435
437 436 #--------------------------------------------------------------------------
438 437 # Connection and ipc file management
439 438 #--------------------------------------------------------------------------
440 439
441 440 def get_connection_info(self):
442 441 """return the connection info as a dict"""
443 442 return dict(
444 443 transport=self.transport,
445 444 ip=self.ip,
446 445 shell_port=self.shell_port,
447 446 iopub_port=self.iopub_port,
448 447 stdin_port=self.stdin_port,
449 448 hb_port=self.hb_port,
450 449 control_port=self.control_port,
451 450 signature_scheme=self.session.signature_scheme,
452 451 key=self.session.key,
453 452 )
454 453
455 454 def cleanup_connection_file(self):
456 455 """Cleanup connection file *if we wrote it*
457 456
458 457 Will not raise if the connection file was already removed somehow.
459 458 """
460 459 if self._connection_file_written:
461 460 # cleanup connection files on full shutdown of kernel we started
462 461 self._connection_file_written = False
463 462 try:
464 463 os.remove(self.connection_file)
465 464 except (IOError, OSError, AttributeError):
466 465 pass
467 466
468 467 def cleanup_ipc_files(self):
469 468 """Cleanup ipc files if we wrote them."""
470 469 if self.transport != 'ipc':
471 470 return
472 471 for port in self.ports:
473 472 ipcfile = "%s-%i" % (self.ip, port)
474 473 try:
475 474 os.remove(ipcfile)
476 475 except (IOError, OSError):
477 476 pass
478 477
479 478 def write_connection_file(self):
480 479 """Write connection info to JSON dict in self.connection_file."""
481 480 if self._connection_file_written and os.path.exists(self.connection_file):
482 481 return
483 482
484 483 self.connection_file, cfg = write_connection_file(self.connection_file,
485 484 transport=self.transport, ip=self.ip, key=self.session.key,
486 485 stdin_port=self.stdin_port, iopub_port=self.iopub_port,
487 486 shell_port=self.shell_port, hb_port=self.hb_port,
488 487 control_port=self.control_port,
489 488 signature_scheme=self.session.signature_scheme,
490 489 )
491 490 # write_connection_file also sets default ports:
492 491 for name in port_names:
493 492 setattr(self, name, cfg[name])
494 493
495 494 self._connection_file_written = True
496 495
497 496 def load_connection_file(self):
498 497 """Load connection info from JSON dict in self.connection_file."""
499 498 self.log.debug(u"Loading connection file %s", self.connection_file)
500 499 with open(self.connection_file) as f:
501 500 cfg = json.load(f)
502 501 self.transport = cfg.get('transport', self.transport)
503 502 self.ip = cfg.get('ip', self._ip_default())
504 503
505 504 for name in port_names:
506 505 if getattr(self, name) == 0 and name in cfg:
507 506 # not overridden by config or cl_args
508 507 setattr(self, name, cfg[name])
509 508 if 'key' in cfg:
510 509 self.config.Session.key = str_to_bytes(cfg['key'])
511 510 if 'signature_scheme' in cfg:
512 511 self.config.Session.signature_scheme = cfg['signature_scheme']
513 512 #--------------------------------------------------------------------------
514 513 # Creating connected sockets
515 514 #--------------------------------------------------------------------------
516 515
517 516 def _make_url(self, channel):
518 517 """Make a ZeroMQ URL for a given channel."""
519 518 transport = self.transport
520 519 ip = self.ip
521 520 port = getattr(self, '%s_port' % channel)
522 521
523 522 if transport == 'tcp':
524 523 return "tcp://%s:%i" % (ip, port)
525 524 else:
526 525 return "%s://%s-%s" % (transport, ip, port)
527 526
528 527 def _create_connected_socket(self, channel, identity=None):
529 528 """Create a zmq Socket and connect it to the kernel."""
530 529 url = self._make_url(channel)
531 530 socket_type = channel_socket_types[channel]
532 531 self.log.debug("Connecting to: %s" % url)
533 532 sock = self.context.socket(socket_type)
534 533 # set linger to 1s to prevent hangs at exit
535 534 sock.linger = 1000
536 535 if identity:
537 536 sock.identity = identity
538 537 sock.connect(url)
539 538 return sock
540 539
541 540 def connect_iopub(self, identity=None):
542 541 """return zmq Socket connected to the IOPub channel"""
543 542 sock = self._create_connected_socket('iopub', identity=identity)
544 543 sock.setsockopt(zmq.SUBSCRIBE, b'')
545 544 return sock
546 545
547 546 def connect_shell(self, identity=None):
548 547 """return zmq Socket connected to the Shell channel"""
549 548 return self._create_connected_socket('shell', identity=identity)
550 549
551 550 def connect_stdin(self, identity=None):
552 551 """return zmq Socket connected to the StdIn channel"""
553 552 return self._create_connected_socket('stdin', identity=identity)
554 553
555 554 def connect_hb(self, identity=None):
556 555 """return zmq Socket connected to the Heartbeat channel"""
557 556 return self._create_connected_socket('hb', identity=identity)
558 557
559 558 def connect_control(self, identity=None):
560 559 """return zmq Socket connected to the Heartbeat channel"""
561 560 return self._create_connected_socket('control', identity=identity)
562 561
563 562
564 563 __all__ = [
565 564 'write_connection_file',
566 565 'get_connection_file',
567 566 'find_connection_file',
568 567 'get_connection_info',
569 568 'connect_qtconsole',
570 569 'tunnel_to_kernel',
571 570 ]
General Comments 0
You need to be logged in to leave comments. Login now