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