##// END OF EJS Templates
Notaries sign notebooks now
MinRK -
Show More
@@ -1,874 +1,874 b''
1 1 # coding: utf-8
2 2 """A tornado based IPython notebook server.
3 3
4 4 Authors:
5 5
6 6 * Brian Granger
7 7 """
8 8 from __future__ import print_function
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 # stdlib
21 21 import errno
22 22 import io
23 23 import json
24 24 import logging
25 25 import os
26 26 import random
27 27 import select
28 28 import signal
29 29 import socket
30 30 import sys
31 31 import threading
32 32 import time
33 33 import webbrowser
34 34
35 35
36 36 # Third party
37 37 # check for pyzmq 2.1.11
38 38 from IPython.utils.zmqrelated import check_for_zmq
39 39 check_for_zmq('2.1.11', 'IPython.html')
40 40
41 41 from jinja2 import Environment, FileSystemLoader
42 42
43 43 # Install the pyzmq ioloop. This has to be done before anything else from
44 44 # tornado is imported.
45 45 from zmq.eventloop import ioloop
46 46 ioloop.install()
47 47
48 48 # check for tornado 3.1.0
49 49 msg = "The IPython Notebook requires tornado >= 3.1.0"
50 50 try:
51 51 import tornado
52 52 except ImportError:
53 53 raise ImportError(msg)
54 54 try:
55 55 version_info = tornado.version_info
56 56 except AttributeError:
57 57 raise ImportError(msg + ", but you have < 1.1.0")
58 58 if version_info < (3,1,0):
59 59 raise ImportError(msg + ", but you have %s" % tornado.version)
60 60
61 61 from tornado import httpserver
62 62 from tornado import web
63 63
64 64 # Our own libraries
65 65 from IPython.html import DEFAULT_STATIC_FILES_PATH
66 66 from .base.handlers import Template404
67 67 from .log import log_request
68 68 from .services.kernels.kernelmanager import MappingKernelManager
69 69 from .services.notebooks.nbmanager import NotebookManager
70 70 from .services.notebooks.filenbmanager import FileNotebookManager
71 71 from .services.clusters.clustermanager import ClusterManager
72 72 from .services.sessions.sessionmanager import SessionManager
73 73
74 74 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
75 75
76 76 from IPython.config.application import catch_config_error, boolean_flag
77 77 from IPython.core.application import BaseIPythonApplication
78 78 from IPython.core.profiledir import ProfileDir
79 79 from IPython.consoleapp import IPythonConsoleApp
80 80 from IPython.kernel import swallow_argv
81 81 from IPython.kernel.zmq.session import default_secure
82 82 from IPython.kernel.zmq.kernelapp import (
83 83 kernel_flags,
84 84 kernel_aliases,
85 85 )
86 86 from IPython.nbformat import current, sign
87 87 from IPython.utils.importstring import import_item
88 88 from IPython.utils.localinterfaces import localhost
89 89 from IPython.utils import submodule
90 90 from IPython.utils.traitlets import (
91 91 Dict, Unicode, Integer, List, Bool, Bytes,
92 92 DottedObjectName, Instance,
93 93 )
94 94 from IPython.utils import py3compat
95 95 from IPython.utils.path import filefind, get_ipython_dir
96 96
97 97 from .utils import url_path_join
98 98
99 99 #-----------------------------------------------------------------------------
100 100 # Module globals
101 101 #-----------------------------------------------------------------------------
102 102
103 103 _examples = """
104 104 ipython notebook # start the notebook
105 105 ipython notebook --profile=sympy # use the sympy profile
106 106 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
107 107 """
108 108
109 109 #-----------------------------------------------------------------------------
110 110 # Helper functions
111 111 #-----------------------------------------------------------------------------
112 112
113 113 def random_ports(port, n):
114 114 """Generate a list of n random ports near the given port.
115 115
116 116 The first 5 ports will be sequential, and the remaining n-5 will be
117 117 randomly selected in the range [port-2*n, port+2*n].
118 118 """
119 119 for i in range(min(5, n)):
120 120 yield port + i
121 121 for i in range(n-5):
122 122 yield max(1, port + random.randint(-2*n, 2*n))
123 123
124 124 def load_handlers(name):
125 125 """Load the (URL pattern, handler) tuples for each component."""
126 126 name = 'IPython.html.' + name
127 127 mod = __import__(name, fromlist=['default_handlers'])
128 128 return mod.default_handlers
129 129
130 130 #-----------------------------------------------------------------------------
131 131 # The Tornado web application
132 132 #-----------------------------------------------------------------------------
133 133
134 134 class NotebookWebApplication(web.Application):
135 135
136 136 def __init__(self, ipython_app, kernel_manager, notebook_manager,
137 137 cluster_manager, session_manager, log, base_project_url,
138 138 settings_overrides):
139 139
140 140 settings = self.init_settings(
141 141 ipython_app, kernel_manager, notebook_manager, cluster_manager,
142 142 session_manager, log, base_project_url, settings_overrides)
143 143 handlers = self.init_handlers(settings)
144 144
145 145 super(NotebookWebApplication, self).__init__(handlers, **settings)
146 146
147 147 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
148 148 cluster_manager, session_manager, log, base_project_url,
149 149 settings_overrides):
150 150 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
151 151 # base_project_url will always be unicode, which will in turn
152 152 # make the patterns unicode, and ultimately result in unicode
153 153 # keys in kwargs to handler._execute(**kwargs) in tornado.
154 154 # This enforces that base_project_url be ascii in that situation.
155 155 #
156 156 # Note that the URLs these patterns check against are escaped,
157 157 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
158 158 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
159 159 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
160 160 settings = dict(
161 161 # basics
162 162 log_function=log_request,
163 163 base_project_url=base_project_url,
164 164 base_kernel_url=ipython_app.base_kernel_url,
165 165 template_path=template_path,
166 166 static_path=ipython_app.static_file_path,
167 167 static_handler_class = FileFindHandler,
168 168 static_url_prefix = url_path_join(base_project_url,'/static/'),
169 169
170 170 # authentication
171 171 cookie_secret=ipython_app.cookie_secret,
172 172 login_url=url_path_join(base_project_url,'/login'),
173 173 password=ipython_app.password,
174 174
175 175 # managers
176 176 kernel_manager=kernel_manager,
177 177 notebook_manager=notebook_manager,
178 178 cluster_manager=cluster_manager,
179 179 session_manager=session_manager,
180 180
181 181 # IPython stuff
182 182 nbextensions_path = ipython_app.nbextensions_path,
183 183 mathjax_url=ipython_app.mathjax_url,
184 184 config=ipython_app.config,
185 185 jinja2_env=Environment(loader=FileSystemLoader(template_path)),
186 186 )
187 187
188 188 # allow custom overrides for the tornado web app.
189 189 settings.update(settings_overrides)
190 190 return settings
191 191
192 192 def init_handlers(self, settings):
193 193 # Load the (URL pattern, handler) tuples for each component.
194 194 handlers = []
195 195 handlers.extend(load_handlers('base.handlers'))
196 196 handlers.extend(load_handlers('tree.handlers'))
197 197 handlers.extend(load_handlers('auth.login'))
198 198 handlers.extend(load_handlers('auth.logout'))
199 199 handlers.extend(load_handlers('notebook.handlers'))
200 200 handlers.extend(load_handlers('nbconvert.handlers'))
201 201 handlers.extend(load_handlers('services.kernels.handlers'))
202 202 handlers.extend(load_handlers('services.notebooks.handlers'))
203 203 handlers.extend(load_handlers('services.clusters.handlers'))
204 204 handlers.extend(load_handlers('services.sessions.handlers'))
205 205 handlers.extend(load_handlers('services.nbconvert.handlers'))
206 206 handlers.extend([
207 207 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : settings['notebook_manager'].notebook_dir}),
208 208 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
209 209 ])
210 210 # prepend base_project_url onto the patterns that we match
211 211 new_handlers = []
212 212 for handler in handlers:
213 213 pattern = url_path_join(settings['base_project_url'], handler[0])
214 214 new_handler = tuple([pattern] + list(handler[1:]))
215 215 new_handlers.append(new_handler)
216 216 # add 404 on the end, which will catch everything that falls through
217 217 new_handlers.append((r'(.*)', Template404))
218 218 return new_handlers
219 219
220 220
221 221 #-----------------------------------------------------------------------------
222 222 # Subcommands
223 223 #-----------------------------------------------------------------------------
224 224
225 225 class NbserverListApp(BaseIPythonApplication):
226 226
227 227 description="List currently running notebook servers in this profile."
228 228
229 229 flags = dict(
230 230 json=({'NbserverListApp': {'json': True}},
231 231 "Produce machine-readable JSON output."),
232 232 )
233 233
234 234 json = Bool(False, config=True,
235 235 help="If True, each line of output will be a JSON object with the "
236 236 "details from the server info file.")
237 237
238 238 def start(self):
239 239 if not self.json:
240 240 print("Currently running servers:")
241 241 for serverinfo in list_running_servers(self.profile):
242 242 if self.json:
243 243 print(json.dumps(serverinfo))
244 244 else:
245 245 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
246 246
247 247
248 248 class NotebookTrustApp(BaseIPythonApplication):
249 249
250 250 description="""Sign one or more IPython notebooks with your key,
251 251 to trust their dynamic (HTML, Javascript) output."""
252 252
253 253 examples="""ipython notebook trust mynotebook.ipynb"""
254 254
255 255 notary = Instance(sign.NotebookNotary)
256 256 def _notary_default(self):
257 257 return sign.NotebookNotary(parent=self, profile_dir=self.profile_dir)
258 258
259 259 def sign_notebook(self, notebook_path):
260 260 if not os.path.exists(notebook_path):
261 261 self.log.error("Notebook missing: %s" % notebook_path)
262 262 self.exit(1)
263 263 with io.open(notebook_path, encoding='utf8') as f:
264 264 nb = current.read(f, 'json')
265 sign.trust_notebook(nb, self.notary.secret, self.notary.signature_scheme)
265 self.notary.sign(nb)
266 266 with io.open(notebook_path, 'w', encoding='utf8') as f:
267 267 current.write(nb, f, 'json')
268 268
269 269 def start(self):
270 270 if not self.extra_args:
271 271 self.log.critical("Specify at least one notebook to sign.")
272 272 self.exit(1)
273 273
274 274 for notebook_path in self.extra_args:
275 275 print("Signing notebook: %s" % notebook_path)
276 276 self.sign_notebook(notebook_path)
277 277
278 278 #-----------------------------------------------------------------------------
279 279 # Aliases and Flags
280 280 #-----------------------------------------------------------------------------
281 281
282 282 flags = dict(kernel_flags)
283 283 flags['no-browser']=(
284 284 {'NotebookApp' : {'open_browser' : False}},
285 285 "Don't open the notebook in a browser after startup."
286 286 )
287 287 flags['no-mathjax']=(
288 288 {'NotebookApp' : {'enable_mathjax' : False}},
289 289 """Disable MathJax
290 290
291 291 MathJax is the javascript library IPython uses to render math/LaTeX. It is
292 292 very large, so you may want to disable it if you have a slow internet
293 293 connection, or for offline use of the notebook.
294 294
295 295 When disabled, equations etc. will appear as their untransformed TeX source.
296 296 """
297 297 )
298 298
299 299 # Add notebook manager flags
300 300 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
301 301 'Auto-save a .py script everytime the .ipynb notebook is saved',
302 302 'Do not auto-save .py scripts for every notebook'))
303 303
304 304 # the flags that are specific to the frontend
305 305 # these must be scrubbed before being passed to the kernel,
306 306 # or it will raise an error on unrecognized flags
307 307 notebook_flags = ['no-browser', 'no-mathjax', 'script', 'no-script']
308 308
309 309 aliases = dict(kernel_aliases)
310 310
311 311 aliases.update({
312 312 'ip': 'NotebookApp.ip',
313 313 'port': 'NotebookApp.port',
314 314 'port-retries': 'NotebookApp.port_retries',
315 315 'transport': 'KernelManager.transport',
316 316 'keyfile': 'NotebookApp.keyfile',
317 317 'certfile': 'NotebookApp.certfile',
318 318 'notebook-dir': 'NotebookManager.notebook_dir',
319 319 'browser': 'NotebookApp.browser',
320 320 })
321 321
322 322 # remove ipkernel flags that are singletons, and don't make sense in
323 323 # multi-kernel evironment:
324 324 aliases.pop('f', None)
325 325
326 326 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
327 327 u'notebook-dir', u'profile', u'profile-dir']
328 328
329 329 #-----------------------------------------------------------------------------
330 330 # NotebookApp
331 331 #-----------------------------------------------------------------------------
332 332
333 333 class NotebookApp(BaseIPythonApplication):
334 334
335 335 name = 'ipython-notebook'
336 336
337 337 description = """
338 338 The IPython HTML Notebook.
339 339
340 340 This launches a Tornado based HTML Notebook Server that serves up an
341 341 HTML5/Javascript Notebook client.
342 342 """
343 343 examples = _examples
344 344
345 345 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
346 346 FileNotebookManager]
347 347 flags = Dict(flags)
348 348 aliases = Dict(aliases)
349 349
350 350 subcommands = dict(
351 351 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
352 352 trust=(NotebookTrustApp, NotebookTrustApp.description),
353 353 )
354 354
355 355 kernel_argv = List(Unicode)
356 356
357 357 def _log_level_default(self):
358 358 return logging.INFO
359 359
360 360 def _log_format_default(self):
361 361 """override default log format to include time"""
362 362 return u"%(asctime)s.%(msecs).03d [%(name)s]%(highlevel)s %(message)s"
363 363
364 364 # create requested profiles by default, if they don't exist:
365 365 auto_create = Bool(True)
366 366
367 367 # file to be opened in the notebook server
368 368 file_to_run = Unicode('')
369 369
370 370 # Network related information.
371 371
372 372 ip = Unicode(config=True,
373 373 help="The IP address the notebook server will listen on."
374 374 )
375 375 def _ip_default(self):
376 376 return localhost()
377 377
378 378 def _ip_changed(self, name, old, new):
379 379 if new == u'*': self.ip = u''
380 380
381 381 port = Integer(8888, config=True,
382 382 help="The port the notebook server will listen on."
383 383 )
384 384 port_retries = Integer(50, config=True,
385 385 help="The number of additional ports to try if the specified port is not available."
386 386 )
387 387
388 388 certfile = Unicode(u'', config=True,
389 389 help="""The full path to an SSL/TLS certificate file."""
390 390 )
391 391
392 392 keyfile = Unicode(u'', config=True,
393 393 help="""The full path to a private key file for usage with SSL/TLS."""
394 394 )
395 395
396 396 cookie_secret = Bytes(b'', config=True,
397 397 help="""The random bytes used to secure cookies.
398 398 By default this is a new random number every time you start the Notebook.
399 399 Set it to a value in a config file to enable logins to persist across server sessions.
400 400
401 401 Note: Cookie secrets should be kept private, do not share config files with
402 402 cookie_secret stored in plaintext (you can read the value from a file).
403 403 """
404 404 )
405 405 def _cookie_secret_default(self):
406 406 return os.urandom(1024)
407 407
408 408 password = Unicode(u'', config=True,
409 409 help="""Hashed password to use for web authentication.
410 410
411 411 To generate, type in a python/IPython shell:
412 412
413 413 from IPython.lib import passwd; passwd()
414 414
415 415 The string should be of the form type:salt:hashed-password.
416 416 """
417 417 )
418 418
419 419 open_browser = Bool(True, config=True,
420 420 help="""Whether to open in a browser after starting.
421 421 The specific browser used is platform dependent and
422 422 determined by the python standard library `webbrowser`
423 423 module, unless it is overridden using the --browser
424 424 (NotebookApp.browser) configuration option.
425 425 """)
426 426
427 427 browser = Unicode(u'', config=True,
428 428 help="""Specify what command to use to invoke a web
429 429 browser when opening the notebook. If not specified, the
430 430 default browser will be determined by the `webbrowser`
431 431 standard library module, which allows setting of the
432 432 BROWSER environment variable to override it.
433 433 """)
434 434
435 435 webapp_settings = Dict(config=True,
436 436 help="Supply overrides for the tornado.web.Application that the "
437 437 "IPython notebook uses.")
438 438
439 439 enable_mathjax = Bool(True, config=True,
440 440 help="""Whether to enable MathJax for typesetting math/TeX
441 441
442 442 MathJax is the javascript library IPython uses to render math/LaTeX. It is
443 443 very large, so you may want to disable it if you have a slow internet
444 444 connection, or for offline use of the notebook.
445 445
446 446 When disabled, equations etc. will appear as their untransformed TeX source.
447 447 """
448 448 )
449 449 def _enable_mathjax_changed(self, name, old, new):
450 450 """set mathjax url to empty if mathjax is disabled"""
451 451 if not new:
452 452 self.mathjax_url = u''
453 453
454 454 base_project_url = Unicode('/', config=True,
455 455 help='''The base URL for the notebook server.
456 456
457 457 Leading and trailing slashes can be omitted,
458 458 and will automatically be added.
459 459 ''')
460 460 def _base_project_url_changed(self, name, old, new):
461 461 if not new.startswith('/'):
462 462 self.base_project_url = '/'+new
463 463 elif not new.endswith('/'):
464 464 self.base_project_url = new+'/'
465 465
466 466 base_kernel_url = Unicode('/', config=True,
467 467 help='''The base URL for the kernel server
468 468
469 469 Leading and trailing slashes can be omitted,
470 470 and will automatically be added.
471 471 ''')
472 472 def _base_kernel_url_changed(self, name, old, new):
473 473 if not new.startswith('/'):
474 474 self.base_kernel_url = '/'+new
475 475 elif not new.endswith('/'):
476 476 self.base_kernel_url = new+'/'
477 477
478 478 websocket_url = Unicode("", config=True,
479 479 help="""The base URL for the websocket server,
480 480 if it differs from the HTTP server (hint: it almost certainly doesn't).
481 481
482 482 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
483 483 """
484 484 )
485 485
486 486 extra_static_paths = List(Unicode, config=True,
487 487 help="""Extra paths to search for serving static files.
488 488
489 489 This allows adding javascript/css to be available from the notebook server machine,
490 490 or overriding individual files in the IPython"""
491 491 )
492 492 def _extra_static_paths_default(self):
493 493 return [os.path.join(self.profile_dir.location, 'static')]
494 494
495 495 @property
496 496 def static_file_path(self):
497 497 """return extra paths + the default location"""
498 498 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
499 499
500 500 nbextensions_path = List(Unicode, config=True,
501 501 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
502 502 )
503 503 def _nbextensions_path_default(self):
504 504 return [os.path.join(get_ipython_dir(), 'nbextensions')]
505 505
506 506 mathjax_url = Unicode("", config=True,
507 507 help="""The url for MathJax.js."""
508 508 )
509 509 def _mathjax_url_default(self):
510 510 if not self.enable_mathjax:
511 511 return u''
512 512 static_url_prefix = self.webapp_settings.get("static_url_prefix",
513 513 url_path_join(self.base_project_url, "static")
514 514 )
515 515
516 516 # try local mathjax, either in nbextensions/mathjax or static/mathjax
517 517 for (url_prefix, search_path) in [
518 518 (url_path_join(self.base_project_url, "nbextensions"), self.nbextensions_path),
519 519 (static_url_prefix, self.static_file_path),
520 520 ]:
521 521 self.log.debug("searching for local mathjax in %s", search_path)
522 522 try:
523 523 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
524 524 except IOError:
525 525 continue
526 526 else:
527 527 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
528 528 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
529 529 return url
530 530
531 531 # no local mathjax, serve from CDN
532 532 if self.certfile:
533 533 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
534 534 host = u"https://c328740.ssl.cf1.rackcdn.com"
535 535 else:
536 536 host = u"http://cdn.mathjax.org"
537 537
538 538 url = host + u"/mathjax/latest/MathJax.js"
539 539 self.log.info("Using MathJax from CDN: %s", url)
540 540 return url
541 541
542 542 def _mathjax_url_changed(self, name, old, new):
543 543 if new and not self.enable_mathjax:
544 544 # enable_mathjax=False overrides mathjax_url
545 545 self.mathjax_url = u''
546 546 else:
547 547 self.log.info("Using MathJax: %s", new)
548 548
549 549 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
550 550 config=True,
551 551 help='The notebook manager class to use.')
552 552
553 553 trust_xheaders = Bool(False, config=True,
554 554 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
555 555 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
556 556 )
557 557
558 558 info_file = Unicode()
559 559
560 560 def _info_file_default(self):
561 561 info_file = "nbserver-%s.json"%os.getpid()
562 562 return os.path.join(self.profile_dir.security_dir, info_file)
563 563
564 564 def parse_command_line(self, argv=None):
565 565 super(NotebookApp, self).parse_command_line(argv)
566 566
567 567 if self.extra_args:
568 568 arg0 = self.extra_args[0]
569 569 f = os.path.abspath(arg0)
570 570 self.argv.remove(arg0)
571 571 if not os.path.exists(f):
572 572 self.log.critical("No such file or directory: %s", f)
573 573 self.exit(1)
574 574 if os.path.isdir(f):
575 575 self.config.FileNotebookManager.notebook_dir = f
576 576 elif os.path.isfile(f):
577 577 self.file_to_run = f
578 578
579 579 def init_kernel_argv(self):
580 580 """construct the kernel arguments"""
581 581 # Scrub frontend-specific flags
582 582 self.kernel_argv = swallow_argv(self.argv, notebook_aliases, notebook_flags)
583 583 if any(arg.startswith(u'--pylab') for arg in self.kernel_argv):
584 584 self.log.warn('\n '.join([
585 585 "Starting all kernels in pylab mode is not recommended,",
586 586 "and will be disabled in a future release.",
587 587 "Please use the %matplotlib magic to enable matplotlib instead.",
588 588 "pylab implies many imports, which can have confusing side effects",
589 589 "and harm the reproducibility of your notebooks.",
590 590 ]))
591 591 # Kernel should inherit default config file from frontend
592 592 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
593 593 # Kernel should get *absolute* path to profile directory
594 594 self.kernel_argv.extend(["--profile-dir", self.profile_dir.location])
595 595
596 596 def init_configurables(self):
597 597 # force Session default to be secure
598 598 default_secure(self.config)
599 599 self.kernel_manager = MappingKernelManager(
600 600 parent=self, log=self.log, kernel_argv=self.kernel_argv,
601 601 connection_dir = self.profile_dir.security_dir,
602 602 )
603 603 kls = import_item(self.notebook_manager_class)
604 604 self.notebook_manager = kls(parent=self, log=self.log)
605 605 self.session_manager = SessionManager(parent=self, log=self.log)
606 606 self.cluster_manager = ClusterManager(parent=self, log=self.log)
607 607 self.cluster_manager.update_profiles()
608 608
609 609 def init_logging(self):
610 610 # This prevents double log messages because tornado use a root logger that
611 611 # self.log is a child of. The logging module dipatches log messages to a log
612 612 # and all of its ancenstors until propagate is set to False.
613 613 self.log.propagate = False
614 614
615 615 # hook up tornado 3's loggers to our app handlers
616 616 for name in ('access', 'application', 'general'):
617 617 logger = logging.getLogger('tornado.%s' % name)
618 618 logger.parent = self.log
619 619 logger.setLevel(self.log.level)
620 620
621 621 def init_webapp(self):
622 622 """initialize tornado webapp and httpserver"""
623 623 self.web_app = NotebookWebApplication(
624 624 self, self.kernel_manager, self.notebook_manager,
625 625 self.cluster_manager, self.session_manager,
626 626 self.log, self.base_project_url, self.webapp_settings
627 627 )
628 628 if self.certfile:
629 629 ssl_options = dict(certfile=self.certfile)
630 630 if self.keyfile:
631 631 ssl_options['keyfile'] = self.keyfile
632 632 else:
633 633 ssl_options = None
634 634 self.web_app.password = self.password
635 635 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
636 636 xheaders=self.trust_xheaders)
637 637 if not self.ip:
638 638 warning = "WARNING: The notebook server is listening on all IP addresses"
639 639 if ssl_options is None:
640 640 self.log.critical(warning + " and not using encryption. This "
641 641 "is not recommended.")
642 642 if not self.password:
643 643 self.log.critical(warning + " and not using authentication. "
644 644 "This is highly insecure and not recommended.")
645 645 success = None
646 646 for port in random_ports(self.port, self.port_retries+1):
647 647 try:
648 648 self.http_server.listen(port, self.ip)
649 649 except socket.error as e:
650 650 if e.errno == errno.EADDRINUSE:
651 651 self.log.info('The port %i is already in use, trying another random port.' % port)
652 652 continue
653 653 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
654 654 self.log.warn("Permission to listen on port %i denied" % port)
655 655 continue
656 656 else:
657 657 raise
658 658 else:
659 659 self.port = port
660 660 success = True
661 661 break
662 662 if not success:
663 663 self.log.critical('ERROR: the notebook server could not be started because '
664 664 'no available port could be found.')
665 665 self.exit(1)
666 666
667 667 @property
668 668 def display_url(self):
669 669 ip = self.ip if self.ip else '[all ip addresses on your system]'
670 670 return self._url(ip)
671 671
672 672 @property
673 673 def connection_url(self):
674 674 ip = self.ip if self.ip else localhost()
675 675 return self._url(ip)
676 676
677 677 def _url(self, ip):
678 678 proto = 'https' if self.certfile else 'http'
679 679 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_project_url)
680 680
681 681 def init_signal(self):
682 682 if not sys.platform.startswith('win'):
683 683 signal.signal(signal.SIGINT, self._handle_sigint)
684 684 signal.signal(signal.SIGTERM, self._signal_stop)
685 685 if hasattr(signal, 'SIGUSR1'):
686 686 # Windows doesn't support SIGUSR1
687 687 signal.signal(signal.SIGUSR1, self._signal_info)
688 688 if hasattr(signal, 'SIGINFO'):
689 689 # only on BSD-based systems
690 690 signal.signal(signal.SIGINFO, self._signal_info)
691 691
692 692 def _handle_sigint(self, sig, frame):
693 693 """SIGINT handler spawns confirmation dialog"""
694 694 # register more forceful signal handler for ^C^C case
695 695 signal.signal(signal.SIGINT, self._signal_stop)
696 696 # request confirmation dialog in bg thread, to avoid
697 697 # blocking the App
698 698 thread = threading.Thread(target=self._confirm_exit)
699 699 thread.daemon = True
700 700 thread.start()
701 701
702 702 def _restore_sigint_handler(self):
703 703 """callback for restoring original SIGINT handler"""
704 704 signal.signal(signal.SIGINT, self._handle_sigint)
705 705
706 706 def _confirm_exit(self):
707 707 """confirm shutdown on ^C
708 708
709 709 A second ^C, or answering 'y' within 5s will cause shutdown,
710 710 otherwise original SIGINT handler will be restored.
711 711
712 712 This doesn't work on Windows.
713 713 """
714 714 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
715 715 time.sleep(0.1)
716 716 info = self.log.info
717 717 info('interrupted')
718 718 print(self.notebook_info())
719 719 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
720 720 sys.stdout.flush()
721 721 r,w,x = select.select([sys.stdin], [], [], 5)
722 722 if r:
723 723 line = sys.stdin.readline()
724 724 if line.lower().startswith('y'):
725 725 self.log.critical("Shutdown confirmed")
726 726 ioloop.IOLoop.instance().stop()
727 727 return
728 728 else:
729 729 print("No answer for 5s:", end=' ')
730 730 print("resuming operation...")
731 731 # no answer, or answer is no:
732 732 # set it back to original SIGINT handler
733 733 # use IOLoop.add_callback because signal.signal must be called
734 734 # from main thread
735 735 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
736 736
737 737 def _signal_stop(self, sig, frame):
738 738 self.log.critical("received signal %s, stopping", sig)
739 739 ioloop.IOLoop.instance().stop()
740 740
741 741 def _signal_info(self, sig, frame):
742 742 print(self.notebook_info())
743 743
744 744 def init_components(self):
745 745 """Check the components submodule, and warn if it's unclean"""
746 746 status = submodule.check_submodule_status()
747 747 if status == 'missing':
748 748 self.log.warn("components submodule missing, running `git submodule update`")
749 749 submodule.update_submodules(submodule.ipython_parent())
750 750 elif status == 'unclean':
751 751 self.log.warn("components submodule unclean, you may see 404s on static/components")
752 752 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
753 753
754 754 @catch_config_error
755 755 def initialize(self, argv=None):
756 756 super(NotebookApp, self).initialize(argv)
757 757 self.init_logging()
758 758 self.init_kernel_argv()
759 759 self.init_configurables()
760 760 self.init_components()
761 761 self.init_webapp()
762 762 self.init_signal()
763 763
764 764 def cleanup_kernels(self):
765 765 """Shutdown all kernels.
766 766
767 767 The kernels will shutdown themselves when this process no longer exists,
768 768 but explicit shutdown allows the KernelManagers to cleanup the connection files.
769 769 """
770 770 self.log.info('Shutting down kernels')
771 771 self.kernel_manager.shutdown_all()
772 772
773 773 def notebook_info(self):
774 774 "Return the current working directory and the server url information"
775 775 info = self.notebook_manager.info_string() + "\n"
776 776 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
777 777 return info + "The IPython Notebook is running at: %s" % self.display_url
778 778
779 779 def server_info(self):
780 780 """Return a JSONable dict of information about this server."""
781 781 return {'url': self.connection_url,
782 782 'hostname': self.ip if self.ip else 'localhost',
783 783 'port': self.port,
784 784 'secure': bool(self.certfile),
785 785 'base_project_url': self.base_project_url,
786 786 'notebook_dir': os.path.abspath(self.notebook_manager.notebook_dir),
787 787 }
788 788
789 789 def write_server_info_file(self):
790 790 """Write the result of server_info() to the JSON file info_file."""
791 791 with open(self.info_file, 'w') as f:
792 792 json.dump(self.server_info(), f, indent=2)
793 793
794 794 def remove_server_info_file(self):
795 795 """Remove the nbserver-<pid>.json file created for this server.
796 796
797 797 Ignores the error raised when the file has already been removed.
798 798 """
799 799 try:
800 800 os.unlink(self.info_file)
801 801 except OSError as e:
802 802 if e.errno != errno.ENOENT:
803 803 raise
804 804
805 805 def start(self):
806 806 """ Start the IPython Notebook server app, after initialization
807 807
808 808 This method takes no arguments so all configuration and initialization
809 809 must be done prior to calling this method."""
810 810 if self.subapp is not None:
811 811 return self.subapp.start()
812 812
813 813 info = self.log.info
814 814 for line in self.notebook_info().split("\n"):
815 815 info(line)
816 816 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
817 817
818 818 self.write_server_info_file()
819 819
820 820 if self.open_browser or self.file_to_run:
821 821 try:
822 822 browser = webbrowser.get(self.browser or None)
823 823 except webbrowser.Error as e:
824 824 self.log.warn('No web browser found: %s.' % e)
825 825 browser = None
826 826
827 827 f = self.file_to_run
828 828 if f:
829 829 nbdir = os.path.abspath(self.notebook_manager.notebook_dir)
830 830 if f.startswith(nbdir):
831 831 f = f[len(nbdir):]
832 832 else:
833 833 self.log.warn(
834 834 "Probably won't be able to open notebook %s "
835 835 "because it is not in notebook_dir %s",
836 836 f, nbdir,
837 837 )
838 838
839 839 if os.path.isfile(self.file_to_run):
840 840 url = url_path_join('notebooks', f)
841 841 else:
842 842 url = url_path_join('tree', f)
843 843 if browser:
844 844 b = lambda : browser.open("%s%s" % (self.connection_url, url),
845 845 new=2)
846 846 threading.Thread(target=b).start()
847 847 try:
848 848 ioloop.IOLoop.instance().start()
849 849 except KeyboardInterrupt:
850 850 info("Interrupted...")
851 851 finally:
852 852 self.cleanup_kernels()
853 853 self.remove_server_info_file()
854 854
855 855
856 856 def list_running_servers(profile='default'):
857 857 """Iterate over the server info files of running notebook servers.
858 858
859 859 Given a profile name, find nbserver-* files in the security directory of
860 860 that profile, and yield dicts of their information, each one pertaining to
861 861 a currently running notebook server instance.
862 862 """
863 863 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
864 864 for file in os.listdir(pd.security_dir):
865 865 if file.startswith('nbserver-'):
866 866 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
867 867 yield json.load(f)
868 868
869 869 #-----------------------------------------------------------------------------
870 870 # Main entry point
871 871 #-----------------------------------------------------------------------------
872 872
873 873 launch_new_instance = NotebookApp.launch_instance
874 874
@@ -1,419 +1,425 b''
1 1 """A notebook manager that uses the local file system for storage.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 * Zach Sailer
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 io
21 21 import itertools
22 22 import os
23 23 import glob
24 24 import shutil
25 25
26 26 from tornado import web
27 27
28 28 from .nbmanager import NotebookManager
29 29 from IPython.nbformat import current, sign
30 30 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
31 31 from IPython.utils import tz
32 32
33 33 #-----------------------------------------------------------------------------
34 34 # Classes
35 35 #-----------------------------------------------------------------------------
36 36
37 37 class FileNotebookManager(NotebookManager):
38 38
39 39 save_script = Bool(False, config=True,
40 40 help="""Automatically create a Python script when saving the notebook.
41 41
42 42 For easier use of import, %run and %load across notebooks, a
43 43 <notebook-name>.py script will be created next to any
44 44 <notebook-name>.ipynb on each save. This can also be set with the
45 45 short `--script` flag.
46 46 """
47 47 )
48 48
49 49 checkpoint_dir = Unicode(config=True,
50 50 help="""The location in which to keep notebook checkpoints
51 51
52 52 By default, it is notebook-dir/.ipynb_checkpoints
53 53 """
54 54 )
55 55 def _checkpoint_dir_default(self):
56 56 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
57 57
58 58 def _checkpoint_dir_changed(self, name, old, new):
59 59 """do a bit of validation of the checkpoint dir"""
60 60 if not os.path.isabs(new):
61 61 # If we receive a non-absolute path, make it absolute.
62 62 abs_new = os.path.abspath(new)
63 63 self.checkpoint_dir = abs_new
64 64 return
65 65 if os.path.exists(new) and not os.path.isdir(new):
66 66 raise TraitError("checkpoint dir %r is not a directory" % new)
67 67 if not os.path.exists(new):
68 68 self.log.info("Creating checkpoint dir %s", new)
69 69 try:
70 70 os.mkdir(new)
71 71 except:
72 72 raise TraitError("Couldn't create checkpoint dir %r" % new)
73 73
74 74 def get_notebook_names(self, path=''):
75 75 """List all notebook names in the notebook dir and path."""
76 76 path = path.strip('/')
77 77 if not os.path.isdir(self.get_os_path(path=path)):
78 78 raise web.HTTPError(404, 'Directory not found: ' + path)
79 79 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
80 80 names = [os.path.basename(name)
81 81 for name in names]
82 82 return names
83 83
84 84 def increment_filename(self, basename, path='', ext='.ipynb'):
85 85 """Return a non-used filename of the form basename<int>."""
86 86 path = path.strip('/')
87 87 for i in itertools.count():
88 88 name = u'{basename}{i}{ext}'.format(basename=basename, i=i, ext=ext)
89 89 os_path = self.get_os_path(name, path)
90 90 if not os.path.isfile(os_path):
91 91 break
92 92 return name
93 93
94 94 def path_exists(self, path):
95 95 """Does the API-style path (directory) actually exist?
96 96
97 97 Parameters
98 98 ----------
99 99 path : string
100 100 The path to check. This is an API path (`/` separated,
101 101 relative to base notebook-dir).
102 102
103 103 Returns
104 104 -------
105 105 exists : bool
106 106 Whether the path is indeed a directory.
107 107 """
108 108 path = path.strip('/')
109 109 os_path = self.get_os_path(path=path)
110 110 return os.path.isdir(os_path)
111 111
112 112 def get_os_path(self, name=None, path=''):
113 113 """Given a notebook name and a URL path, return its file system
114 114 path.
115 115
116 116 Parameters
117 117 ----------
118 118 name : string
119 119 The name of a notebook file with the .ipynb extension
120 120 path : string
121 121 The relative URL path (with '/' as separator) to the named
122 122 notebook.
123 123
124 124 Returns
125 125 -------
126 126 path : string
127 127 A file system path that combines notebook_dir (location where
128 128 server started), the relative path, and the filename with the
129 129 current operating system's url.
130 130 """
131 131 parts = path.strip('/').split('/')
132 132 parts = [p for p in parts if p != ''] # remove duplicate splits
133 133 if name is not None:
134 134 parts.append(name)
135 135 path = os.path.join(self.notebook_dir, *parts)
136 136 return path
137 137
138 138 def notebook_exists(self, name, path=''):
139 139 """Returns a True if the notebook exists. Else, returns False.
140 140
141 141 Parameters
142 142 ----------
143 143 name : string
144 144 The name of the notebook you are checking.
145 145 path : string
146 146 The relative path to the notebook (with '/' as separator)
147 147
148 148 Returns
149 149 -------
150 150 bool
151 151 """
152 152 path = path.strip('/')
153 153 nbpath = self.get_os_path(name, path=path)
154 154 return os.path.isfile(nbpath)
155 155
156 156 def list_notebooks(self, path):
157 157 """Returns a list of dictionaries that are the standard model
158 158 for all notebooks in the relative 'path'.
159 159
160 160 Parameters
161 161 ----------
162 162 path : str
163 163 the URL path that describes the relative path for the
164 164 listed notebooks
165 165
166 166 Returns
167 167 -------
168 168 notebooks : list of dicts
169 169 a list of the notebook models without 'content'
170 170 """
171 171 path = path.strip('/')
172 172 notebook_names = self.get_notebook_names(path)
173 173 notebooks = []
174 174 for name in notebook_names:
175 175 model = self.get_notebook_model(name, path, content=False)
176 176 notebooks.append(model)
177 177 notebooks = sorted(notebooks, key=lambda item: item['name'])
178 178 return notebooks
179 179
180 180 def get_notebook_model(self, name, path='', content=True):
181 181 """ Takes a path and name for a notebook and returns its model
182 182
183 183 Parameters
184 184 ----------
185 185 name : str
186 186 the name of the notebook
187 187 path : str
188 188 the URL path that describes the relative path for
189 189 the notebook
190 190
191 191 Returns
192 192 -------
193 193 model : dict
194 194 the notebook model. If contents=True, returns the 'contents'
195 195 dict in the model as well.
196 196 """
197 197 path = path.strip('/')
198 198 if not self.notebook_exists(name=name, path=path):
199 199 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
200 200 os_path = self.get_os_path(name, path)
201 201 info = os.stat(os_path)
202 202 last_modified = tz.utcfromtimestamp(info.st_mtime)
203 203 created = tz.utcfromtimestamp(info.st_ctime)
204 204 # Create the notebook model.
205 205 model ={}
206 206 model['name'] = name
207 207 model['path'] = path
208 208 model['last_modified'] = last_modified
209 209 model['created'] = created
210 210 if content:
211 211 with io.open(os_path, 'r', encoding='utf-8') as f:
212 212 try:
213 213 nb = current.read(f, u'json')
214 214 except Exception as e:
215 215 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
216 216 model['content'] = nb
217 sign.mark_trusted_cells(nb, self.notary.secret, self.notary.scheme)
217 trusted = self.notary.check_signature(nb)
218 if not trusted:
219 self.log.warn("Notebook %s/%s is not trusted", model['path'], model['name'])
220 self.notary.mark_cells(nb, trusted)
218 221 return model
219 222
220 223 def save_notebook_model(self, model, name='', path=''):
221 224 """Save the notebook model and return the model with no content."""
222 225 path = path.strip('/')
223 226
224 227 if 'content' not in model:
225 228 raise web.HTTPError(400, u'No notebook JSON data provided')
226 229
227 230 # One checkpoint should always exist
228 231 if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
229 232 self.create_checkpoint(name, path)
230 233
231 234 new_path = model.get('path', path).strip('/')
232 235 new_name = model.get('name', name)
233 236
234 237 if path != new_path or name != new_name:
235 238 self.rename_notebook(name, path, new_name, new_path)
236 239
237 240 # Save the notebook file
238 241 os_path = self.get_os_path(new_name, new_path)
239 242 nb = current.to_notebook_json(model['content'])
240 243
241 if sign.check_trusted_cells(nb):
242 sign.trust_notebook(nb, self.notary.secret, self.notary.scheme)
244 if self.notary.check_cells(nb):
245 self.notary.sign(nb)
246 else:
247 self.log.warn("Saving untrusted notebook %s/%s", new_path, new_name)
248
243 249
244 250 if 'name' in nb['metadata']:
245 251 nb['metadata']['name'] = u''
246 252 try:
247 253 self.log.debug("Autosaving notebook %s", os_path)
248 254 with io.open(os_path, 'w', encoding='utf-8') as f:
249 255 current.write(nb, f, u'json')
250 256 except Exception as e:
251 257 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
252 258
253 259 # Save .py script as well
254 260 if self.save_script:
255 261 py_path = os.path.splitext(os_path)[0] + '.py'
256 262 self.log.debug("Writing script %s", py_path)
257 263 try:
258 264 with io.open(py_path, 'w', encoding='utf-8') as f:
259 265 current.write(nb, f, u'py')
260 266 except Exception as e:
261 267 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
262 268
263 269 model = self.get_notebook_model(new_name, new_path, content=False)
264 270 return model
265 271
266 272 def update_notebook_model(self, model, name, path=''):
267 273 """Update the notebook's path and/or name"""
268 274 path = path.strip('/')
269 275 new_name = model.get('name', name)
270 276 new_path = model.get('path', path).strip('/')
271 277 if path != new_path or name != new_name:
272 278 self.rename_notebook(name, path, new_name, new_path)
273 279 model = self.get_notebook_model(new_name, new_path, content=False)
274 280 return model
275 281
276 282 def delete_notebook_model(self, name, path=''):
277 283 """Delete notebook by name and path."""
278 284 path = path.strip('/')
279 285 os_path = self.get_os_path(name, path)
280 286 if not os.path.isfile(os_path):
281 287 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
282 288
283 289 # clear checkpoints
284 290 for checkpoint in self.list_checkpoints(name, path):
285 291 checkpoint_id = checkpoint['id']
286 292 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
287 293 if os.path.isfile(cp_path):
288 294 self.log.debug("Unlinking checkpoint %s", cp_path)
289 295 os.unlink(cp_path)
290 296
291 297 self.log.debug("Unlinking notebook %s", os_path)
292 298 os.unlink(os_path)
293 299
294 300 def rename_notebook(self, old_name, old_path, new_name, new_path):
295 301 """Rename a notebook."""
296 302 old_path = old_path.strip('/')
297 303 new_path = new_path.strip('/')
298 304 if new_name == old_name and new_path == old_path:
299 305 return
300 306
301 307 new_os_path = self.get_os_path(new_name, new_path)
302 308 old_os_path = self.get_os_path(old_name, old_path)
303 309
304 310 # Should we proceed with the move?
305 311 if os.path.isfile(new_os_path):
306 312 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
307 313 if self.save_script:
308 314 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
309 315 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
310 316 if os.path.isfile(new_py_path):
311 317 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
312 318
313 319 # Move the notebook file
314 320 try:
315 321 os.rename(old_os_path, new_os_path)
316 322 except Exception as e:
317 323 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
318 324
319 325 # Move the checkpoints
320 326 old_checkpoints = self.list_checkpoints(old_name, old_path)
321 327 for cp in old_checkpoints:
322 328 checkpoint_id = cp['id']
323 329 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
324 330 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
325 331 if os.path.isfile(old_cp_path):
326 332 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
327 333 os.rename(old_cp_path, new_cp_path)
328 334
329 335 # Move the .py script
330 336 if self.save_script:
331 337 os.rename(old_py_path, new_py_path)
332 338
333 339 # Checkpoint-related utilities
334 340
335 341 def get_checkpoint_path(self, checkpoint_id, name, path=''):
336 342 """find the path to a checkpoint"""
337 343 path = path.strip('/')
338 344 basename, _ = os.path.splitext(name)
339 345 filename = u"{name}-{checkpoint_id}{ext}".format(
340 346 name=basename,
341 347 checkpoint_id=checkpoint_id,
342 348 ext=self.filename_ext,
343 349 )
344 350 cp_path = os.path.join(path, self.checkpoint_dir, filename)
345 351 return cp_path
346 352
347 353 def get_checkpoint_model(self, checkpoint_id, name, path=''):
348 354 """construct the info dict for a given checkpoint"""
349 355 path = path.strip('/')
350 356 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
351 357 stats = os.stat(cp_path)
352 358 last_modified = tz.utcfromtimestamp(stats.st_mtime)
353 359 info = dict(
354 360 id = checkpoint_id,
355 361 last_modified = last_modified,
356 362 )
357 363 return info
358 364
359 365 # public checkpoint API
360 366
361 367 def create_checkpoint(self, name, path=''):
362 368 """Create a checkpoint from the current state of a notebook"""
363 369 path = path.strip('/')
364 370 nb_path = self.get_os_path(name, path)
365 371 # only the one checkpoint ID:
366 372 checkpoint_id = u"checkpoint"
367 373 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
368 374 self.log.debug("creating checkpoint for notebook %s", name)
369 375 if not os.path.exists(self.checkpoint_dir):
370 376 os.mkdir(self.checkpoint_dir)
371 377 shutil.copy2(nb_path, cp_path)
372 378
373 379 # return the checkpoint info
374 380 return self.get_checkpoint_model(checkpoint_id, name, path)
375 381
376 382 def list_checkpoints(self, name, path=''):
377 383 """list the checkpoints for a given notebook
378 384
379 385 This notebook manager currently only supports one checkpoint per notebook.
380 386 """
381 387 path = path.strip('/')
382 388 checkpoint_id = "checkpoint"
383 389 path = self.get_checkpoint_path(checkpoint_id, name, path)
384 390 if not os.path.exists(path):
385 391 return []
386 392 else:
387 393 return [self.get_checkpoint_model(checkpoint_id, name, path)]
388 394
389 395
390 396 def restore_checkpoint(self, checkpoint_id, name, path=''):
391 397 """restore a notebook to a checkpointed state"""
392 398 path = path.strip('/')
393 399 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
394 400 nb_path = self.get_os_path(name, path)
395 401 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
396 402 if not os.path.isfile(cp_path):
397 403 self.log.debug("checkpoint file does not exist: %s", cp_path)
398 404 raise web.HTTPError(404,
399 405 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
400 406 )
401 407 # ensure notebook is readable (never restore from an unreadable notebook)
402 408 with io.open(cp_path, 'r', encoding='utf-8') as f:
403 409 nb = current.read(f, u'json')
404 410 shutil.copy2(cp_path, nb_path)
405 411 self.log.debug("copying %s -> %s", cp_path, nb_path)
406 412
407 413 def delete_checkpoint(self, checkpoint_id, name, path=''):
408 414 """delete a notebook's checkpoint"""
409 415 path = path.strip('/')
410 416 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
411 417 if not os.path.isfile(cp_path):
412 418 raise web.HTTPError(404,
413 419 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
414 420 )
415 421 self.log.debug("unlinking %s", cp_path)
416 422 os.unlink(cp_path)
417 423
418 424 def info_string(self):
419 425 return "Serving notebooks from local directory: %s" % self.notebook_dir
@@ -1,199 +1,203 b''
1 1 """Functions for signing notebooks"""
2 2 #-----------------------------------------------------------------------------
3 3 # Copyright (C) 2014, The IPython Development Team
4 4 #
5 5 # Distributed under the terms of the BSD License. The full license is in
6 6 # the file COPYING, distributed as part of this software.
7 7 #-----------------------------------------------------------------------------
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Imports
11 11 #-----------------------------------------------------------------------------
12 12
13 13 import base64
14 14 from contextlib import contextmanager
15 15 import hashlib
16 16 from hmac import HMAC
17 17 import io
18 18 import os
19 19
20 20 from IPython.utils.py3compat import string_types, unicode_type, cast_bytes
21 21 from IPython.config import LoggingConfigurable
22 from IPython.utils.traitlets import Instance, Bytes, Enum
22 from IPython.utils.traitlets import Instance, Bytes, Enum, Any, Unicode
23 23
24 24 #-----------------------------------------------------------------------------
25 25 # Code
26 26 #-----------------------------------------------------------------------------
27 27
28 28
29 29 def yield_everything(obj):
30 30 """Yield every item in a container as bytes
31 31
32 32 Allows any JSONable object to be passed to an HMAC digester
33 33 without having to serialize the whole thing.
34 34 """
35 35 if isinstance(obj, dict):
36 36 for key in sorted(obj):
37 37 value = obj[key]
38 38 yield cast_bytes(key)
39 39 for b in yield_everything(value):
40 40 yield b
41 41 elif isinstance(obj, (list, tuple)):
42 42 for element in obj:
43 43 for b in yield_everything(element):
44 44 yield b
45 45 elif isinstance(obj, unicode_type):
46 46 yield obj.encode('utf8')
47 47 else:
48 48 yield unicode_type(obj).encode('utf8')
49 49
50 50
51 51 @contextmanager
52 52 def signature_removed(nb):
53 53 """Context manager for operating on a notebook with its signature removed
54 54
55 55 Used for excluding the previous signature when computing a notebook's signature.
56 56 """
57 57 save_signature = nb['metadata'].pop('signature', None)
58 58 try:
59 59 yield
60 60 finally:
61 61 if save_signature is not None:
62 62 nb['metadata']['signature'] = save_signature
63 63
64 64
65 def notebook_signature(nb, secret, scheme):
66 """Compute a notebook's signature
67
68 by hashing the entire contents of the notebook via HMAC digest.
69 scheme is the hashing scheme, which must be an attribute of the hashlib module,
70 as listed in hashlib.algorithms.
71 """
72 hmac = HMAC(secret, digestmod=getattr(hashlib, scheme))
73 # don't include the previous hash in the content to hash
74 with signature_removed(nb):
75 # sign the whole thing
76 for b in yield_everything(nb):
77 hmac.update(b)
78
79 return hmac.hexdigest()
80
81
82 def check_notebook_signature(nb, secret, scheme):
83 """Check a notebook's stored signature
84
85 If a signature is stored in the notebook's metadata,
86 a new signature is computed and compared with the stored value.
87
88 Returns True if the signature is found and matches, False otherwise.
89
90 The following conditions must all be met for a notebook to be trusted:
91 - a signature is stored in the form 'scheme:hexdigest'
92 - the stored scheme matches the requested scheme
93 - the requested scheme is available from hashlib
94 - the computed hash from notebook_signature matches the stored hash
95 """
96 stored_signature = nb['metadata'].get('signature', None)
97 if not stored_signature \
98 or not isinstance(stored_signature, string_types) \
99 or ':' not in stored_signature:
100 return False
101 stored_scheme, sig = stored_signature.split(':', 1)
102 if scheme != stored_scheme:
103 return False
104 try:
105 my_signature = notebook_signature(nb, secret, scheme)
106 except AttributeError:
107 return False
108 return my_signature == sig
109
110
111 def trust_notebook(nb, secret, scheme):
112 """Re-sign a notebook, indicating that its output is trusted
113
114 stores 'scheme:hmac-hexdigest' in notebook.metadata.signature
115
116 e.g. 'sha256:deadbeef123...'
117 """
118 signature = notebook_signature(nb, secret, scheme)
119 nb['metadata']['signature'] = "%s:%s" % (scheme, signature)
120
121
122 def mark_trusted_cells(nb, secret, scheme):
123 """Mark cells as trusted if the notebook's signature can be verified
124
125 Sets ``cell.trusted = True | False`` on all code cells,
126 depending on whether the stored signature can be verified.
127 """
128 if not nb['worksheets']:
129 # nothing to mark if there are no cells
130 return True
131 trusted = check_notebook_signature(nb, secret, scheme)
132 for cell in nb['worksheets'][0]['cells']:
133 if cell['cell_type'] == 'code':
134 cell['trusted'] = trusted
135 return trusted
136
137
138 def check_trusted_cells(nb):
139 """Return whether all code cells are trusted
140
141 If there are no code cells, return True.
142 """
143 if not nb['worksheets']:
144 return True
145 for cell in nb['worksheets'][0]['cells']:
146 if cell['cell_type'] != 'code':
147 continue
148 if not cell.get('trusted', False):
149 return False
150 return True
151
152
153 65 class NotebookNotary(LoggingConfigurable):
154 """A class for configuring notebook signatures
155
156 It stores the secret with which to sign notebooks,
157 and the hashing scheme to use for notebook signatures.
158 """
159
160 scheme = Enum(hashlib.algorithms, default_value='sha256', config=True,
161 help="""The hashing algorithm used to sign notebooks."""
162 )
66 """A class for computing and verifying notebook signatures."""
163 67
164 68 profile_dir = Instance("IPython.core.profiledir.ProfileDir")
165 69 def _profile_dir_default(self):
166 70 from IPython.core.application import BaseIPythonApplication
167 71 if BaseIPythonApplication.initialized():
168 72 app = BaseIPythonApplication.instance()
169 73 else:
170 74 # create an app, without the global instance
171 75 app = BaseIPythonApplication()
172 76 app.initialize()
173 77 return app.profile_dir
174 78
79 algorithm = Enum(hashlib.algorithms, default_value='sha256', config=True,
80 help="""The hashing algorithm used to sign notebooks."""
81 )
82 def _algorithm_changed(self, name, old, new):
83 self.digestmod = getattr(hashlib, self.algorithm)
84
85 digestmod = Any()
86 def _digestmod_default(self):
87 return getattr(hashlib, self.algorithm)
88
89 secret_file = Unicode()
90 def _secret_file_default(self):
91 if self.profile_dir is None:
92 return ''
93 return os.path.join(self.profile_dir.security_dir, 'notebook_secret')
94
175 95 secret = Bytes(config=True,
176 96 help="""The secret key with which notebooks are signed."""
177 97 )
178 98 def _secret_default(self):
179 99 # note : this assumes an Application is running
180 profile_dir = self.profile_dir
181 secret_file = os.path.join(profile_dir.security_dir, 'notebook_secret')
182 if os.path.exists(secret_file):
183 with io.open(secret_file, 'rb') as f:
100 if os.path.exists(self.secret_file):
101 with io.open(self.secret_file, 'rb') as f:
184 102 return f.read()
185 103 else:
186 104 secret = base64.encodestring(os.urandom(1024))
187 self.log.info("Writing output secret to %s", secret_file)
188 with io.open(secret_file, 'wb') as f:
189 f.write(secret)
190 try:
191 os.chmod(secret_file, 0o600)
192 except OSError:
193 self.log.warn(
194 "Could not set permissions on %s",
195 secret_file
196 )
105 self._write_secret_file(secret)
197 106 return secret
107
108 def _write_secret_file(self, secret):
109 """write my secret to my secret_file"""
110 self.log.info("Writing output secret to %s", self.secret_file)
111 with io.open(self.secret_file, 'wb') as f:
112 f.write(secret)
113 try:
114 os.chmod(self.secret_file, 0o600)
115 except OSError:
116 self.log.warn(
117 "Could not set permissions on %s",
118 self.secret_file
119 )
120 return secret
121
122 def compute_signature(self, nb):
123 """Compute a notebook's signature
124
125 by hashing the entire contents of the notebook via HMAC digest.
126 """
127 hmac = HMAC(self.secret, digestmod=self.digestmod)
128 # don't include the previous hash in the content to hash
129 with signature_removed(nb):
130 # sign the whole thing
131 for b in yield_everything(nb):
132 hmac.update(b)
133
134 return hmac.hexdigest()
135
136 def check_signature(self, nb):
137 """Check a notebook's stored signature
138
139 If a signature is stored in the notebook's metadata,
140 a new signature is computed and compared with the stored value.
141
142 Returns True if the signature is found and matches, False otherwise.
143
144 The following conditions must all be met for a notebook to be trusted:
145 - a signature is stored in the form 'scheme:hexdigest'
146 - the stored scheme matches the requested scheme
147 - the requested scheme is available from hashlib
148 - the computed hash from notebook_signature matches the stored hash
149 """
150 stored_signature = nb['metadata'].get('signature', None)
151 if not stored_signature \
152 or not isinstance(stored_signature, string_types) \
153 or ':' not in stored_signature:
154 return False
155 stored_algo, sig = stored_signature.split(':', 1)
156 if self.algorithm != stored_algo:
157 return False
158 my_signature = self.compute_signature(nb)
159 return my_signature == sig
160
161 def sign(self, nb):
162 """Sign a notebook, indicating that its output is trusted
163
164 stores 'algo:hmac-hexdigest' in notebook.metadata.signature
165
166 e.g. 'sha256:deadbeef123...'
167 """
168 signature = self.compute_signature(nb)
169 nb['metadata']['signature'] = "%s:%s" % (self.algorithm, signature)
170
171 def mark_cells(self, nb, trusted):
172 """Mark cells as trusted if the notebook's signature can be verified
173
174 Sets ``cell.trusted = True | False`` on all code cells,
175 depending on whether the stored signature can be verified.
176
177 This function is the inverse of check_cells
178 """
179 if not nb['worksheets']:
180 # nothing to mark if there are no cells
181 return
182 for cell in nb['worksheets'][0]['cells']:
183 if cell['cell_type'] == 'code':
184 cell['trusted'] = trusted
185
186 def check_cells(self, nb):
187 """Return whether all code cells are trusted
188
189 If there are no code cells, return True.
190
191 This function is the inverse of mark_cells.
192 """
193 if not nb['worksheets']:
194 return True
195 for cell in nb['worksheets'][0]['cells']:
196 if cell['cell_type'] != 'code':
197 continue
198 if not cell.get('trusted', False):
199 return False
200 return True
201
198 202
199 203 No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now