##// END OF EJS Templates
profile_dir
Sylvain Corlay -
Show More
@@ -1,396 +1,396 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """
2 """
3 An application for IPython.
3 An application for IPython.
4
4
5 All top-level applications should use the classes in this module for
5 All top-level applications should use the classes in this module for
6 handling configuration and creating configurables.
6 handling configuration and creating configurables.
7
7
8 The job of an :class:`Application` is to create the master configuration
8 The job of an :class:`Application` is to create the master configuration
9 object and then create the configurable objects, passing the config to them.
9 object and then create the configurable objects, passing the config to them.
10 """
10 """
11
11
12 # Copyright (c) IPython Development Team.
12 # Copyright (c) IPython Development Team.
13 # Distributed under the terms of the Modified BSD License.
13 # Distributed under the terms of the Modified BSD License.
14
14
15 import atexit
15 import atexit
16 import glob
16 import glob
17 import logging
17 import logging
18 import os
18 import os
19 import shutil
19 import shutil
20 import sys
20 import sys
21
21
22 from IPython.config.application import Application, catch_config_error
22 from IPython.config.application import Application, catch_config_error
23 from IPython.config.loader import ConfigFileNotFound, PyFileConfigLoader
23 from IPython.config.loader import ConfigFileNotFound, PyFileConfigLoader
24 from IPython.core import release, crashhandler
24 from IPython.core import release, crashhandler
25 from IPython.core.profiledir import ProfileDir, ProfileDirError
25 from IPython.core.profiledir import ProfileDir, ProfileDirError
26 from IPython.utils.path import get_ipython_dir, get_ipython_package_dir, ensure_dir_exists
26 from IPython.utils.path import get_ipython_dir, get_ipython_package_dir, ensure_dir_exists
27 from IPython.utils import py3compat
27 from IPython.utils import py3compat
28 from IPython.utils.traitlets import List, Unicode, Type, Bool, Dict, Set, Instance
28 from IPython.utils.traitlets import List, Unicode, Type, Bool, Dict, Set, Instance
29
29
30 if os.name == 'nt':
30 if os.name == 'nt':
31 programdata = os.environ.get('PROGRAMDATA', None)
31 programdata = os.environ.get('PROGRAMDATA', None)
32 if programdata:
32 if programdata:
33 SYSTEM_CONFIG_DIRS = [os.path.join(programdata, 'ipython')]
33 SYSTEM_CONFIG_DIRS = [os.path.join(programdata, 'ipython')]
34 else: # PROGRAMDATA is not defined by default on XP.
34 else: # PROGRAMDATA is not defined by default on XP.
35 SYSTEM_CONFIG_DIRS = []
35 SYSTEM_CONFIG_DIRS = []
36 else:
36 else:
37 SYSTEM_CONFIG_DIRS = [
37 SYSTEM_CONFIG_DIRS = [
38 "/usr/local/etc/ipython",
38 "/usr/local/etc/ipython",
39 "/etc/ipython",
39 "/etc/ipython",
40 ]
40 ]
41
41
42
42
43 # aliases and flags
43 # aliases and flags
44
44
45 base_aliases = {
45 base_aliases = {
46 'profile-dir' : 'ProfileDir.location',
46 'profile-dir' : 'ProfileDir.location',
47 'profile' : 'BaseIPythonApplication.profile',
47 'profile' : 'BaseIPythonApplication.profile',
48 'ipython-dir' : 'BaseIPythonApplication.ipython_dir',
48 'ipython-dir' : 'BaseIPythonApplication.ipython_dir',
49 'log-level' : 'Application.log_level',
49 'log-level' : 'Application.log_level',
50 'config' : 'BaseIPythonApplication.extra_config_file',
50 'config' : 'BaseIPythonApplication.extra_config_file',
51 }
51 }
52
52
53 base_flags = dict(
53 base_flags = dict(
54 debug = ({'Application' : {'log_level' : logging.DEBUG}},
54 debug = ({'Application' : {'log_level' : logging.DEBUG}},
55 "set log level to logging.DEBUG (maximize logging output)"),
55 "set log level to logging.DEBUG (maximize logging output)"),
56 quiet = ({'Application' : {'log_level' : logging.CRITICAL}},
56 quiet = ({'Application' : {'log_level' : logging.CRITICAL}},
57 "set log level to logging.CRITICAL (minimize logging output)"),
57 "set log level to logging.CRITICAL (minimize logging output)"),
58 init = ({'BaseIPythonApplication' : {
58 init = ({'BaseIPythonApplication' : {
59 'copy_config_files' : True,
59 'copy_config_files' : True,
60 'auto_create' : True}
60 'auto_create' : True}
61 }, """Initialize profile with default config files. This is equivalent
61 }, """Initialize profile with default config files. This is equivalent
62 to running `ipython profile create <profile>` prior to startup.
62 to running `ipython profile create <profile>` prior to startup.
63 """)
63 """)
64 )
64 )
65
65
66 class ProfileAwareConfigLoader(PyFileConfigLoader):
66 class ProfileAwareConfigLoader(PyFileConfigLoader):
67 """A Python file config loader that is aware of IPython profiles."""
67 """A Python file config loader that is aware of IPython profiles."""
68 def load_subconfig(self, fname, path=None, profile=None):
68 def load_subconfig(self, fname, path=None, profile=None):
69 if profile is not None:
69 if profile is not None:
70 try:
70 try:
71 profile_dir = ProfileDir.find_profile_dir_by_name(
71 profile_dir = ProfileDir.find_profile_dir_by_name(
72 get_ipython_dir(),
72 get_ipython_dir(),
73 profile,
73 profile,
74 )
74 )
75 except ProfileDirError:
75 except ProfileDirError:
76 return
76 return
77 path = profile_dir.location
77 path = profile_dir.location
78 return super(ProfileAwareConfigLoader, self).load_subconfig(fname, path=path)
78 return super(ProfileAwareConfigLoader, self).load_subconfig(fname, path=path)
79
79
80 class BaseIPythonApplication(Application):
80 class BaseIPythonApplication(Application):
81
81
82 name = Unicode(u'ipython')
82 name = Unicode(u'ipython')
83 description = Unicode(u'IPython: an enhanced interactive Python shell.')
83 description = Unicode(u'IPython: an enhanced interactive Python shell.')
84 version = Unicode(release.version)
84 version = Unicode(release.version)
85
85
86 aliases = Dict(base_aliases)
86 aliases = Dict(base_aliases)
87 flags = Dict(base_flags)
87 flags = Dict(base_flags)
88 classes = List([ProfileDir])
88 classes = List([ProfileDir])
89
89
90 # enable `load_subconfig('cfg.py', profile='name')`
90 # enable `load_subconfig('cfg.py', profile='name')`
91 python_config_loader_class = ProfileAwareConfigLoader
91 python_config_loader_class = ProfileAwareConfigLoader
92
92
93 # Track whether the config_file has changed,
93 # Track whether the config_file has changed,
94 # because some logic happens only if we aren't using the default.
94 # because some logic happens only if we aren't using the default.
95 config_file_specified = Set()
95 config_file_specified = Set()
96
96
97 config_file_name = Unicode()
97 config_file_name = Unicode()
98 def _config_file_name_default(self):
98 def _config_file_name_default(self):
99 return self.name.replace('-','_') + u'_config.py'
99 return self.name.replace('-','_') + u'_config.py'
100 def _config_file_name_changed(self, name, old, new):
100 def _config_file_name_changed(self, name, old, new):
101 if new != old:
101 if new != old:
102 self.config_file_specified.add(new)
102 self.config_file_specified.add(new)
103
103
104 # The directory that contains IPython's builtin profiles.
104 # The directory that contains IPython's builtin profiles.
105 builtin_profile_dir = Unicode(
105 builtin_profile_dir = Unicode(
106 os.path.join(get_ipython_package_dir(), u'config', u'profile', u'default')
106 os.path.join(get_ipython_package_dir(), u'config', u'profile', u'default')
107 )
107 )
108
108
109 config_file_paths = List(Unicode)
109 config_file_paths = List(Unicode)
110 def _config_file_paths_default(self):
110 def _config_file_paths_default(self):
111 return [py3compat.getcwd()]
111 return [py3compat.getcwd()]
112
112
113 extra_config_file = Unicode(config=True,
113 extra_config_file = Unicode(config=True,
114 help="""Path to an extra config file to load.
114 help="""Path to an extra config file to load.
115
115
116 If specified, load this config file in addition to any other IPython config.
116 If specified, load this config file in addition to any other IPython config.
117 """)
117 """)
118 def _extra_config_file_changed(self, name, old, new):
118 def _extra_config_file_changed(self, name, old, new):
119 try:
119 try:
120 self.config_files.remove(old)
120 self.config_files.remove(old)
121 except ValueError:
121 except ValueError:
122 pass
122 pass
123 self.config_file_specified.add(new)
123 self.config_file_specified.add(new)
124 self.config_files.append(new)
124 self.config_files.append(new)
125
125
126 profile = Unicode(u'default', config=True,
126 profile = Unicode(u'default', config=True,
127 help="""The IPython profile to use."""
127 help="""The IPython profile to use."""
128 )
128 )
129
129
130 def _profile_changed(self, name, old, new):
130 def _profile_changed(self, name, old, new):
131 self.builtin_profile_dir = os.path.join(
131 self.builtin_profile_dir = os.path.join(
132 get_ipython_package_dir(), u'config', u'profile', new
132 get_ipython_package_dir(), u'config', u'profile', new
133 )
133 )
134
134
135 ipython_dir = Unicode(config=True,
135 ipython_dir = Unicode(config=True,
136 help="""
136 help="""
137 The name of the IPython directory. This directory is used for logging
137 The name of the IPython directory. This directory is used for logging
138 configuration (through profiles), history storage, etc. The default
138 configuration (through profiles), history storage, etc. The default
139 is usually $HOME/.ipython. This option can also be specified through
139 is usually $HOME/.ipython. This option can also be specified through
140 the environment variable IPYTHONDIR.
140 the environment variable IPYTHONDIR.
141 """
141 """
142 )
142 )
143 def _ipython_dir_default(self):
143 def _ipython_dir_default(self):
144 d = get_ipython_dir()
144 d = get_ipython_dir()
145 self._ipython_dir_changed('ipython_dir', d, d)
145 self._ipython_dir_changed('ipython_dir', d, d)
146 return d
146 return d
147
147
148 _in_init_profile_dir = False
148 _in_init_profile_dir = False
149 profile_dir = Instance(ProfileDir)
149 profile_dir = Instance(ProfileDir, allow_none=True)
150 def _profile_dir_default(self):
150 def _profile_dir_default(self):
151 # avoid recursion
151 # avoid recursion
152 if self._in_init_profile_dir:
152 if self._in_init_profile_dir:
153 return
153 return
154 # profile_dir requested early, force initialization
154 # profile_dir requested early, force initialization
155 self.init_profile_dir()
155 self.init_profile_dir()
156 return self.profile_dir
156 return self.profile_dir
157
157
158 overwrite = Bool(False, config=True,
158 overwrite = Bool(False, config=True,
159 help="""Whether to overwrite existing config files when copying""")
159 help="""Whether to overwrite existing config files when copying""")
160 auto_create = Bool(False, config=True,
160 auto_create = Bool(False, config=True,
161 help="""Whether to create profile dir if it doesn't exist""")
161 help="""Whether to create profile dir if it doesn't exist""")
162
162
163 config_files = List(Unicode)
163 config_files = List(Unicode)
164 def _config_files_default(self):
164 def _config_files_default(self):
165 return [self.config_file_name]
165 return [self.config_file_name]
166
166
167 copy_config_files = Bool(False, config=True,
167 copy_config_files = Bool(False, config=True,
168 help="""Whether to install the default config files into the profile dir.
168 help="""Whether to install the default config files into the profile dir.
169 If a new profile is being created, and IPython contains config files for that
169 If a new profile is being created, and IPython contains config files for that
170 profile, then they will be staged into the new directory. Otherwise,
170 profile, then they will be staged into the new directory. Otherwise,
171 default config files will be automatically generated.
171 default config files will be automatically generated.
172 """)
172 """)
173
173
174 verbose_crash = Bool(False, config=True,
174 verbose_crash = Bool(False, config=True,
175 help="""Create a massive crash report when IPython encounters what may be an
175 help="""Create a massive crash report when IPython encounters what may be an
176 internal error. The default is to append a short message to the
176 internal error. The default is to append a short message to the
177 usual traceback""")
177 usual traceback""")
178
178
179 # The class to use as the crash handler.
179 # The class to use as the crash handler.
180 crash_handler_class = Type(crashhandler.CrashHandler)
180 crash_handler_class = Type(crashhandler.CrashHandler)
181
181
182 @catch_config_error
182 @catch_config_error
183 def __init__(self, **kwargs):
183 def __init__(self, **kwargs):
184 super(BaseIPythonApplication, self).__init__(**kwargs)
184 super(BaseIPythonApplication, self).__init__(**kwargs)
185 # ensure current working directory exists
185 # ensure current working directory exists
186 try:
186 try:
187 directory = py3compat.getcwd()
187 directory = py3compat.getcwd()
188 except:
188 except:
189 # exit if cwd doesn't exist
189 # exit if cwd doesn't exist
190 self.log.error("Current working directory doesn't exist.")
190 self.log.error("Current working directory doesn't exist.")
191 self.exit(1)
191 self.exit(1)
192
192
193 #-------------------------------------------------------------------------
193 #-------------------------------------------------------------------------
194 # Various stages of Application creation
194 # Various stages of Application creation
195 #-------------------------------------------------------------------------
195 #-------------------------------------------------------------------------
196
196
197 def init_crash_handler(self):
197 def init_crash_handler(self):
198 """Create a crash handler, typically setting sys.excepthook to it."""
198 """Create a crash handler, typically setting sys.excepthook to it."""
199 self.crash_handler = self.crash_handler_class(self)
199 self.crash_handler = self.crash_handler_class(self)
200 sys.excepthook = self.excepthook
200 sys.excepthook = self.excepthook
201 def unset_crashhandler():
201 def unset_crashhandler():
202 sys.excepthook = sys.__excepthook__
202 sys.excepthook = sys.__excepthook__
203 atexit.register(unset_crashhandler)
203 atexit.register(unset_crashhandler)
204
204
205 def excepthook(self, etype, evalue, tb):
205 def excepthook(self, etype, evalue, tb):
206 """this is sys.excepthook after init_crashhandler
206 """this is sys.excepthook after init_crashhandler
207
207
208 set self.verbose_crash=True to use our full crashhandler, instead of
208 set self.verbose_crash=True to use our full crashhandler, instead of
209 a regular traceback with a short message (crash_handler_lite)
209 a regular traceback with a short message (crash_handler_lite)
210 """
210 """
211
211
212 if self.verbose_crash:
212 if self.verbose_crash:
213 return self.crash_handler(etype, evalue, tb)
213 return self.crash_handler(etype, evalue, tb)
214 else:
214 else:
215 return crashhandler.crash_handler_lite(etype, evalue, tb)
215 return crashhandler.crash_handler_lite(etype, evalue, tb)
216
216
217 def _ipython_dir_changed(self, name, old, new):
217 def _ipython_dir_changed(self, name, old, new):
218 if old is not None:
218 if old is not None:
219 str_old = py3compat.cast_bytes_py2(os.path.abspath(old),
219 str_old = py3compat.cast_bytes_py2(os.path.abspath(old),
220 sys.getfilesystemencoding()
220 sys.getfilesystemencoding()
221 )
221 )
222 if str_old in sys.path:
222 if str_old in sys.path:
223 sys.path.remove(str_old)
223 sys.path.remove(str_old)
224 str_path = py3compat.cast_bytes_py2(os.path.abspath(new),
224 str_path = py3compat.cast_bytes_py2(os.path.abspath(new),
225 sys.getfilesystemencoding()
225 sys.getfilesystemencoding()
226 )
226 )
227 sys.path.append(str_path)
227 sys.path.append(str_path)
228 ensure_dir_exists(new)
228 ensure_dir_exists(new)
229 readme = os.path.join(new, 'README')
229 readme = os.path.join(new, 'README')
230 readme_src = os.path.join(get_ipython_package_dir(), u'config', u'profile', 'README')
230 readme_src = os.path.join(get_ipython_package_dir(), u'config', u'profile', 'README')
231 if not os.path.exists(readme) and os.path.exists(readme_src):
231 if not os.path.exists(readme) and os.path.exists(readme_src):
232 shutil.copy(readme_src, readme)
232 shutil.copy(readme_src, readme)
233 for d in ('extensions', 'nbextensions'):
233 for d in ('extensions', 'nbextensions'):
234 path = os.path.join(new, d)
234 path = os.path.join(new, d)
235 try:
235 try:
236 ensure_dir_exists(path)
236 ensure_dir_exists(path)
237 except OSError:
237 except OSError:
238 # this will not be EEXIST
238 # this will not be EEXIST
239 self.log.error("couldn't create path %s: %s", path, e)
239 self.log.error("couldn't create path %s: %s", path, e)
240 self.log.debug("IPYTHONDIR set to: %s" % new)
240 self.log.debug("IPYTHONDIR set to: %s" % new)
241
241
242 def load_config_file(self, suppress_errors=True):
242 def load_config_file(self, suppress_errors=True):
243 """Load the config file.
243 """Load the config file.
244
244
245 By default, errors in loading config are handled, and a warning
245 By default, errors in loading config are handled, and a warning
246 printed on screen. For testing, the suppress_errors option is set
246 printed on screen. For testing, the suppress_errors option is set
247 to False, so errors will make tests fail.
247 to False, so errors will make tests fail.
248 """
248 """
249 self.log.debug("Searching path %s for config files", self.config_file_paths)
249 self.log.debug("Searching path %s for config files", self.config_file_paths)
250 base_config = 'ipython_config.py'
250 base_config = 'ipython_config.py'
251 self.log.debug("Attempting to load config file: %s" %
251 self.log.debug("Attempting to load config file: %s" %
252 base_config)
252 base_config)
253 try:
253 try:
254 Application.load_config_file(
254 Application.load_config_file(
255 self,
255 self,
256 base_config,
256 base_config,
257 path=self.config_file_paths
257 path=self.config_file_paths
258 )
258 )
259 except ConfigFileNotFound:
259 except ConfigFileNotFound:
260 # ignore errors loading parent
260 # ignore errors loading parent
261 self.log.debug("Config file %s not found", base_config)
261 self.log.debug("Config file %s not found", base_config)
262 pass
262 pass
263
263
264 for config_file_name in self.config_files:
264 for config_file_name in self.config_files:
265 if not config_file_name or config_file_name == base_config:
265 if not config_file_name or config_file_name == base_config:
266 continue
266 continue
267 self.log.debug("Attempting to load config file: %s" %
267 self.log.debug("Attempting to load config file: %s" %
268 self.config_file_name)
268 self.config_file_name)
269 try:
269 try:
270 Application.load_config_file(
270 Application.load_config_file(
271 self,
271 self,
272 config_file_name,
272 config_file_name,
273 path=self.config_file_paths
273 path=self.config_file_paths
274 )
274 )
275 except ConfigFileNotFound:
275 except ConfigFileNotFound:
276 # Only warn if the default config file was NOT being used.
276 # Only warn if the default config file was NOT being used.
277 if config_file_name in self.config_file_specified:
277 if config_file_name in self.config_file_specified:
278 msg = self.log.warn
278 msg = self.log.warn
279 else:
279 else:
280 msg = self.log.debug
280 msg = self.log.debug
281 msg("Config file not found, skipping: %s", config_file_name)
281 msg("Config file not found, skipping: %s", config_file_name)
282 except:
282 except:
283 # For testing purposes.
283 # For testing purposes.
284 if not suppress_errors:
284 if not suppress_errors:
285 raise
285 raise
286 self.log.warn("Error loading config file: %s" %
286 self.log.warn("Error loading config file: %s" %
287 self.config_file_name, exc_info=True)
287 self.config_file_name, exc_info=True)
288
288
289 def init_profile_dir(self):
289 def init_profile_dir(self):
290 """initialize the profile dir"""
290 """initialize the profile dir"""
291 self._in_init_profile_dir = True
291 self._in_init_profile_dir = True
292 if self.profile_dir is not None:
292 if self.profile_dir is not None:
293 # already ran
293 # already ran
294 return
294 return
295 if 'ProfileDir.location' not in self.config:
295 if 'ProfileDir.location' not in self.config:
296 # location not specified, find by profile name
296 # location not specified, find by profile name
297 try:
297 try:
298 p = ProfileDir.find_profile_dir_by_name(self.ipython_dir, self.profile, self.config)
298 p = ProfileDir.find_profile_dir_by_name(self.ipython_dir, self.profile, self.config)
299 except ProfileDirError:
299 except ProfileDirError:
300 # not found, maybe create it (always create default profile)
300 # not found, maybe create it (always create default profile)
301 if self.auto_create or self.profile == 'default':
301 if self.auto_create or self.profile == 'default':
302 try:
302 try:
303 p = ProfileDir.create_profile_dir_by_name(self.ipython_dir, self.profile, self.config)
303 p = ProfileDir.create_profile_dir_by_name(self.ipython_dir, self.profile, self.config)
304 except ProfileDirError:
304 except ProfileDirError:
305 self.log.fatal("Could not create profile: %r"%self.profile)
305 self.log.fatal("Could not create profile: %r"%self.profile)
306 self.exit(1)
306 self.exit(1)
307 else:
307 else:
308 self.log.info("Created profile dir: %r"%p.location)
308 self.log.info("Created profile dir: %r"%p.location)
309 else:
309 else:
310 self.log.fatal("Profile %r not found."%self.profile)
310 self.log.fatal("Profile %r not found."%self.profile)
311 self.exit(1)
311 self.exit(1)
312 else:
312 else:
313 self.log.debug("Using existing profile dir: %r"%p.location)
313 self.log.debug("Using existing profile dir: %r"%p.location)
314 else:
314 else:
315 location = self.config.ProfileDir.location
315 location = self.config.ProfileDir.location
316 # location is fully specified
316 # location is fully specified
317 try:
317 try:
318 p = ProfileDir.find_profile_dir(location, self.config)
318 p = ProfileDir.find_profile_dir(location, self.config)
319 except ProfileDirError:
319 except ProfileDirError:
320 # not found, maybe create it
320 # not found, maybe create it
321 if self.auto_create:
321 if self.auto_create:
322 try:
322 try:
323 p = ProfileDir.create_profile_dir(location, self.config)
323 p = ProfileDir.create_profile_dir(location, self.config)
324 except ProfileDirError:
324 except ProfileDirError:
325 self.log.fatal("Could not create profile directory: %r"%location)
325 self.log.fatal("Could not create profile directory: %r"%location)
326 self.exit(1)
326 self.exit(1)
327 else:
327 else:
328 self.log.debug("Creating new profile dir: %r"%location)
328 self.log.debug("Creating new profile dir: %r"%location)
329 else:
329 else:
330 self.log.fatal("Profile directory %r not found."%location)
330 self.log.fatal("Profile directory %r not found."%location)
331 self.exit(1)
331 self.exit(1)
332 else:
332 else:
333 self.log.info("Using existing profile dir: %r"%location)
333 self.log.info("Using existing profile dir: %r"%location)
334 # if profile_dir is specified explicitly, set profile name
334 # if profile_dir is specified explicitly, set profile name
335 dir_name = os.path.basename(p.location)
335 dir_name = os.path.basename(p.location)
336 if dir_name.startswith('profile_'):
336 if dir_name.startswith('profile_'):
337 self.profile = dir_name[8:]
337 self.profile = dir_name[8:]
338
338
339 self.profile_dir = p
339 self.profile_dir = p
340 self.config_file_paths.append(p.location)
340 self.config_file_paths.append(p.location)
341 self._in_init_profile_dir = False
341 self._in_init_profile_dir = False
342
342
343 def init_config_files(self):
343 def init_config_files(self):
344 """[optionally] copy default config files into profile dir."""
344 """[optionally] copy default config files into profile dir."""
345 self.config_file_paths.extend(SYSTEM_CONFIG_DIRS)
345 self.config_file_paths.extend(SYSTEM_CONFIG_DIRS)
346 # copy config files
346 # copy config files
347 path = self.builtin_profile_dir
347 path = self.builtin_profile_dir
348 if self.copy_config_files:
348 if self.copy_config_files:
349 src = self.profile
349 src = self.profile
350
350
351 cfg = self.config_file_name
351 cfg = self.config_file_name
352 if path and os.path.exists(os.path.join(path, cfg)):
352 if path and os.path.exists(os.path.join(path, cfg)):
353 self.log.warn("Staging %r from %s into %r [overwrite=%s]"%(
353 self.log.warn("Staging %r from %s into %r [overwrite=%s]"%(
354 cfg, src, self.profile_dir.location, self.overwrite)
354 cfg, src, self.profile_dir.location, self.overwrite)
355 )
355 )
356 self.profile_dir.copy_config_file(cfg, path=path, overwrite=self.overwrite)
356 self.profile_dir.copy_config_file(cfg, path=path, overwrite=self.overwrite)
357 else:
357 else:
358 self.stage_default_config_file()
358 self.stage_default_config_file()
359 else:
359 else:
360 # Still stage *bundled* config files, but not generated ones
360 # Still stage *bundled* config files, but not generated ones
361 # This is necessary for `ipython profile=sympy` to load the profile
361 # This is necessary for `ipython profile=sympy` to load the profile
362 # on the first go
362 # on the first go
363 files = glob.glob(os.path.join(path, '*.py'))
363 files = glob.glob(os.path.join(path, '*.py'))
364 for fullpath in files:
364 for fullpath in files:
365 cfg = os.path.basename(fullpath)
365 cfg = os.path.basename(fullpath)
366 if self.profile_dir.copy_config_file(cfg, path=path, overwrite=False):
366 if self.profile_dir.copy_config_file(cfg, path=path, overwrite=False):
367 # file was copied
367 # file was copied
368 self.log.warn("Staging bundled %s from %s into %r"%(
368 self.log.warn("Staging bundled %s from %s into %r"%(
369 cfg, self.profile, self.profile_dir.location)
369 cfg, self.profile, self.profile_dir.location)
370 )
370 )
371
371
372
372
373 def stage_default_config_file(self):
373 def stage_default_config_file(self):
374 """auto generate default config file, and stage it into the profile."""
374 """auto generate default config file, and stage it into the profile."""
375 s = self.generate_config_file()
375 s = self.generate_config_file()
376 fname = os.path.join(self.profile_dir.location, self.config_file_name)
376 fname = os.path.join(self.profile_dir.location, self.config_file_name)
377 if self.overwrite or not os.path.exists(fname):
377 if self.overwrite or not os.path.exists(fname):
378 self.log.warn("Generating default config file: %r"%(fname))
378 self.log.warn("Generating default config file: %r"%(fname))
379 with open(fname, 'w') as f:
379 with open(fname, 'w') as f:
380 f.write(s)
380 f.write(s)
381
381
382 @catch_config_error
382 @catch_config_error
383 def initialize(self, argv=None):
383 def initialize(self, argv=None):
384 # don't hook up crash handler before parsing command-line
384 # don't hook up crash handler before parsing command-line
385 self.parse_command_line(argv)
385 self.parse_command_line(argv)
386 self.init_crash_handler()
386 self.init_crash_handler()
387 if self.subapp is not None:
387 if self.subapp is not None:
388 # stop here if subapp is taking over
388 # stop here if subapp is taking over
389 return
389 return
390 cl_config = self.config
390 cl_config = self.config
391 self.init_profile_dir()
391 self.init_profile_dir()
392 self.init_config_files()
392 self.init_config_files()
393 self.load_config_file()
393 self.load_config_file()
394 # enforce cl-opts override configfile opts:
394 # enforce cl-opts override configfile opts:
395 self.update_config(cl_config)
395 self.update_config(cl_config)
396
396
@@ -1,427 +1,427 b''
1 """Utilities for signing notebooks"""
1 """Utilities for signing notebooks"""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 import base64
6 import base64
7 from contextlib import contextmanager
7 from contextlib import contextmanager
8 from datetime import datetime
8 from datetime import datetime
9 import hashlib
9 import hashlib
10 from hmac import HMAC
10 from hmac import HMAC
11 import io
11 import io
12 import os
12 import os
13
13
14 try:
14 try:
15 import sqlite3
15 import sqlite3
16 except ImportError:
16 except ImportError:
17 try:
17 try:
18 from pysqlite2 import dbapi2 as sqlite3
18 from pysqlite2 import dbapi2 as sqlite3
19 except ImportError:
19 except ImportError:
20 sqlite3 = None
20 sqlite3 = None
21
21
22 from IPython.utils.io import atomic_writing
22 from IPython.utils.io import atomic_writing
23 from IPython.utils.py3compat import unicode_type, cast_bytes
23 from IPython.utils.py3compat import unicode_type, cast_bytes
24 from IPython.utils.traitlets import Instance, Bytes, Enum, Any, Unicode, Bool, Integer
24 from IPython.utils.traitlets import Instance, Bytes, Enum, Any, Unicode, Bool, Integer
25 from IPython.config import LoggingConfigurable, MultipleInstanceError
25 from IPython.config import LoggingConfigurable, MultipleInstanceError
26 from IPython.core.application import BaseIPythonApplication, base_flags
26 from IPython.core.application import BaseIPythonApplication, base_flags
27
27
28 from . import read, write, NO_CONVERT
28 from . import read, write, NO_CONVERT
29
29
30 try:
30 try:
31 # Python 3
31 # Python 3
32 algorithms = hashlib.algorithms_guaranteed
32 algorithms = hashlib.algorithms_guaranteed
33 except AttributeError:
33 except AttributeError:
34 algorithms = hashlib.algorithms
34 algorithms = hashlib.algorithms
35
35
36
36
37 def yield_everything(obj):
37 def yield_everything(obj):
38 """Yield every item in a container as bytes
38 """Yield every item in a container as bytes
39
39
40 Allows any JSONable object to be passed to an HMAC digester
40 Allows any JSONable object to be passed to an HMAC digester
41 without having to serialize the whole thing.
41 without having to serialize the whole thing.
42 """
42 """
43 if isinstance(obj, dict):
43 if isinstance(obj, dict):
44 for key in sorted(obj):
44 for key in sorted(obj):
45 value = obj[key]
45 value = obj[key]
46 yield cast_bytes(key)
46 yield cast_bytes(key)
47 for b in yield_everything(value):
47 for b in yield_everything(value):
48 yield b
48 yield b
49 elif isinstance(obj, (list, tuple)):
49 elif isinstance(obj, (list, tuple)):
50 for element in obj:
50 for element in obj:
51 for b in yield_everything(element):
51 for b in yield_everything(element):
52 yield b
52 yield b
53 elif isinstance(obj, unicode_type):
53 elif isinstance(obj, unicode_type):
54 yield obj.encode('utf8')
54 yield obj.encode('utf8')
55 else:
55 else:
56 yield unicode_type(obj).encode('utf8')
56 yield unicode_type(obj).encode('utf8')
57
57
58 def yield_code_cells(nb):
58 def yield_code_cells(nb):
59 """Iterator that yields all cells in a notebook
59 """Iterator that yields all cells in a notebook
60
60
61 nbformat version independent
61 nbformat version independent
62 """
62 """
63 if nb.nbformat >= 4:
63 if nb.nbformat >= 4:
64 for cell in nb['cells']:
64 for cell in nb['cells']:
65 if cell['cell_type'] == 'code':
65 if cell['cell_type'] == 'code':
66 yield cell
66 yield cell
67 elif nb.nbformat == 3:
67 elif nb.nbformat == 3:
68 for ws in nb['worksheets']:
68 for ws in nb['worksheets']:
69 for cell in ws['cells']:
69 for cell in ws['cells']:
70 if cell['cell_type'] == 'code':
70 if cell['cell_type'] == 'code':
71 yield cell
71 yield cell
72
72
73 @contextmanager
73 @contextmanager
74 def signature_removed(nb):
74 def signature_removed(nb):
75 """Context manager for operating on a notebook with its signature removed
75 """Context manager for operating on a notebook with its signature removed
76
76
77 Used for excluding the previous signature when computing a notebook's signature.
77 Used for excluding the previous signature when computing a notebook's signature.
78 """
78 """
79 save_signature = nb['metadata'].pop('signature', None)
79 save_signature = nb['metadata'].pop('signature', None)
80 try:
80 try:
81 yield
81 yield
82 finally:
82 finally:
83 if save_signature is not None:
83 if save_signature is not None:
84 nb['metadata']['signature'] = save_signature
84 nb['metadata']['signature'] = save_signature
85
85
86
86
87 class NotebookNotary(LoggingConfigurable):
87 class NotebookNotary(LoggingConfigurable):
88 """A class for computing and verifying notebook signatures."""
88 """A class for computing and verifying notebook signatures."""
89
89
90 profile_dir = Instance("IPython.core.profiledir.ProfileDir")
90 profile_dir = Instance("IPython.core.profiledir.ProfileDir", allow_none=True)
91 def _profile_dir_default(self):
91 def _profile_dir_default(self):
92 from IPython.core.application import BaseIPythonApplication
92 from IPython.core.application import BaseIPythonApplication
93 app = None
93 app = None
94 try:
94 try:
95 if BaseIPythonApplication.initialized():
95 if BaseIPythonApplication.initialized():
96 app = BaseIPythonApplication.instance()
96 app = BaseIPythonApplication.instance()
97 except MultipleInstanceError:
97 except MultipleInstanceError:
98 pass
98 pass
99 if app is None:
99 if app is None:
100 # create an app, without the global instance
100 # create an app, without the global instance
101 app = BaseIPythonApplication()
101 app = BaseIPythonApplication()
102 app.initialize(argv=[])
102 app.initialize(argv=[])
103 return app.profile_dir
103 return app.profile_dir
104
104
105 db_file = Unicode(config=True,
105 db_file = Unicode(config=True,
106 help="""The sqlite file in which to store notebook signatures.
106 help="""The sqlite file in which to store notebook signatures.
107 By default, this will be in your IPython profile.
107 By default, this will be in your IPython profile.
108 You can set it to ':memory:' to disable sqlite writing to the filesystem.
108 You can set it to ':memory:' to disable sqlite writing to the filesystem.
109 """)
109 """)
110 def _db_file_default(self):
110 def _db_file_default(self):
111 if self.profile_dir is None:
111 if self.profile_dir is None:
112 return ':memory:'
112 return ':memory:'
113 return os.path.join(self.profile_dir.security_dir, u'nbsignatures.db')
113 return os.path.join(self.profile_dir.security_dir, u'nbsignatures.db')
114
114
115 # 64k entries ~ 12MB
115 # 64k entries ~ 12MB
116 cache_size = Integer(65535, config=True,
116 cache_size = Integer(65535, config=True,
117 help="""The number of notebook signatures to cache.
117 help="""The number of notebook signatures to cache.
118 When the number of signatures exceeds this value,
118 When the number of signatures exceeds this value,
119 the oldest 25% of signatures will be culled.
119 the oldest 25% of signatures will be culled.
120 """
120 """
121 )
121 )
122 db = Any()
122 db = Any()
123 def _db_default(self):
123 def _db_default(self):
124 if sqlite3 is None:
124 if sqlite3 is None:
125 self.log.warn("Missing SQLite3, all notebooks will be untrusted!")
125 self.log.warn("Missing SQLite3, all notebooks will be untrusted!")
126 return
126 return
127 kwargs = dict(detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES)
127 kwargs = dict(detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES)
128 db = sqlite3.connect(self.db_file, **kwargs)
128 db = sqlite3.connect(self.db_file, **kwargs)
129 self.init_db(db)
129 self.init_db(db)
130 return db
130 return db
131
131
132 def init_db(self, db):
132 def init_db(self, db):
133 db.execute("""
133 db.execute("""
134 CREATE TABLE IF NOT EXISTS nbsignatures
134 CREATE TABLE IF NOT EXISTS nbsignatures
135 (
135 (
136 id integer PRIMARY KEY AUTOINCREMENT,
136 id integer PRIMARY KEY AUTOINCREMENT,
137 algorithm text,
137 algorithm text,
138 signature text,
138 signature text,
139 path text,
139 path text,
140 last_seen timestamp
140 last_seen timestamp
141 )""")
141 )""")
142 db.execute("""
142 db.execute("""
143 CREATE INDEX IF NOT EXISTS algosig ON nbsignatures(algorithm, signature)
143 CREATE INDEX IF NOT EXISTS algosig ON nbsignatures(algorithm, signature)
144 """)
144 """)
145 db.commit()
145 db.commit()
146
146
147 algorithm = Enum(algorithms, default_value='sha256', config=True,
147 algorithm = Enum(algorithms, default_value='sha256', config=True,
148 help="""The hashing algorithm used to sign notebooks."""
148 help="""The hashing algorithm used to sign notebooks."""
149 )
149 )
150 def _algorithm_changed(self, name, old, new):
150 def _algorithm_changed(self, name, old, new):
151 self.digestmod = getattr(hashlib, self.algorithm)
151 self.digestmod = getattr(hashlib, self.algorithm)
152
152
153 digestmod = Any()
153 digestmod = Any()
154 def _digestmod_default(self):
154 def _digestmod_default(self):
155 return getattr(hashlib, self.algorithm)
155 return getattr(hashlib, self.algorithm)
156
156
157 secret_file = Unicode(config=True,
157 secret_file = Unicode(config=True,
158 help="""The file where the secret key is stored."""
158 help="""The file where the secret key is stored."""
159 )
159 )
160 def _secret_file_default(self):
160 def _secret_file_default(self):
161 if self.profile_dir is None:
161 if self.profile_dir is None:
162 return ''
162 return ''
163 return os.path.join(self.profile_dir.security_dir, 'notebook_secret')
163 return os.path.join(self.profile_dir.security_dir, 'notebook_secret')
164
164
165 secret = Bytes(config=True,
165 secret = Bytes(config=True,
166 help="""The secret key with which notebooks are signed."""
166 help="""The secret key with which notebooks are signed."""
167 )
167 )
168 def _secret_default(self):
168 def _secret_default(self):
169 # note : this assumes an Application is running
169 # note : this assumes an Application is running
170 if os.path.exists(self.secret_file):
170 if os.path.exists(self.secret_file):
171 with io.open(self.secret_file, 'rb') as f:
171 with io.open(self.secret_file, 'rb') as f:
172 return f.read()
172 return f.read()
173 else:
173 else:
174 secret = base64.encodestring(os.urandom(1024))
174 secret = base64.encodestring(os.urandom(1024))
175 self._write_secret_file(secret)
175 self._write_secret_file(secret)
176 return secret
176 return secret
177
177
178 def _write_secret_file(self, secret):
178 def _write_secret_file(self, secret):
179 """write my secret to my secret_file"""
179 """write my secret to my secret_file"""
180 self.log.info("Writing notebook-signing key to %s", self.secret_file)
180 self.log.info("Writing notebook-signing key to %s", self.secret_file)
181 with io.open(self.secret_file, 'wb') as f:
181 with io.open(self.secret_file, 'wb') as f:
182 f.write(secret)
182 f.write(secret)
183 try:
183 try:
184 os.chmod(self.secret_file, 0o600)
184 os.chmod(self.secret_file, 0o600)
185 except OSError:
185 except OSError:
186 self.log.warn(
186 self.log.warn(
187 "Could not set permissions on %s",
187 "Could not set permissions on %s",
188 self.secret_file
188 self.secret_file
189 )
189 )
190 return secret
190 return secret
191
191
192 def compute_signature(self, nb):
192 def compute_signature(self, nb):
193 """Compute a notebook's signature
193 """Compute a notebook's signature
194
194
195 by hashing the entire contents of the notebook via HMAC digest.
195 by hashing the entire contents of the notebook via HMAC digest.
196 """
196 """
197 hmac = HMAC(self.secret, digestmod=self.digestmod)
197 hmac = HMAC(self.secret, digestmod=self.digestmod)
198 # don't include the previous hash in the content to hash
198 # don't include the previous hash in the content to hash
199 with signature_removed(nb):
199 with signature_removed(nb):
200 # sign the whole thing
200 # sign the whole thing
201 for b in yield_everything(nb):
201 for b in yield_everything(nb):
202 hmac.update(b)
202 hmac.update(b)
203
203
204 return hmac.hexdigest()
204 return hmac.hexdigest()
205
205
206 def check_signature(self, nb):
206 def check_signature(self, nb):
207 """Check a notebook's stored signature
207 """Check a notebook's stored signature
208
208
209 If a signature is stored in the notebook's metadata,
209 If a signature is stored in the notebook's metadata,
210 a new signature is computed and compared with the stored value.
210 a new signature is computed and compared with the stored value.
211
211
212 Returns True if the signature is found and matches, False otherwise.
212 Returns True if the signature is found and matches, False otherwise.
213
213
214 The following conditions must all be met for a notebook to be trusted:
214 The following conditions must all be met for a notebook to be trusted:
215 - a signature is stored in the form 'scheme:hexdigest'
215 - a signature is stored in the form 'scheme:hexdigest'
216 - the stored scheme matches the requested scheme
216 - the stored scheme matches the requested scheme
217 - the requested scheme is available from hashlib
217 - the requested scheme is available from hashlib
218 - the computed hash from notebook_signature matches the stored hash
218 - the computed hash from notebook_signature matches the stored hash
219 """
219 """
220 if nb.nbformat < 3:
220 if nb.nbformat < 3:
221 return False
221 return False
222 if self.db is None:
222 if self.db is None:
223 return False
223 return False
224 signature = self.compute_signature(nb)
224 signature = self.compute_signature(nb)
225 r = self.db.execute("""SELECT id FROM nbsignatures WHERE
225 r = self.db.execute("""SELECT id FROM nbsignatures WHERE
226 algorithm = ? AND
226 algorithm = ? AND
227 signature = ?;
227 signature = ?;
228 """, (self.algorithm, signature)).fetchone()
228 """, (self.algorithm, signature)).fetchone()
229 if r is None:
229 if r is None:
230 return False
230 return False
231 self.db.execute("""UPDATE nbsignatures SET last_seen = ? WHERE
231 self.db.execute("""UPDATE nbsignatures SET last_seen = ? WHERE
232 algorithm = ? AND
232 algorithm = ? AND
233 signature = ?;
233 signature = ?;
234 """,
234 """,
235 (datetime.utcnow(), self.algorithm, signature),
235 (datetime.utcnow(), self.algorithm, signature),
236 )
236 )
237 self.db.commit()
237 self.db.commit()
238 return True
238 return True
239
239
240 def sign(self, nb):
240 def sign(self, nb):
241 """Sign a notebook, indicating that its output is trusted on this machine
241 """Sign a notebook, indicating that its output is trusted on this machine
242
242
243 Stores hash algorithm and hmac digest in a local database of trusted notebooks.
243 Stores hash algorithm and hmac digest in a local database of trusted notebooks.
244 """
244 """
245 if nb.nbformat < 3:
245 if nb.nbformat < 3:
246 return
246 return
247 signature = self.compute_signature(nb)
247 signature = self.compute_signature(nb)
248 self.store_signature(signature, nb)
248 self.store_signature(signature, nb)
249
249
250 def store_signature(self, signature, nb):
250 def store_signature(self, signature, nb):
251 if self.db is None:
251 if self.db is None:
252 return
252 return
253 self.db.execute("""INSERT OR IGNORE INTO nbsignatures
253 self.db.execute("""INSERT OR IGNORE INTO nbsignatures
254 (algorithm, signature, last_seen) VALUES (?, ?, ?)""",
254 (algorithm, signature, last_seen) VALUES (?, ?, ?)""",
255 (self.algorithm, signature, datetime.utcnow())
255 (self.algorithm, signature, datetime.utcnow())
256 )
256 )
257 self.db.execute("""UPDATE nbsignatures SET last_seen = ? WHERE
257 self.db.execute("""UPDATE nbsignatures SET last_seen = ? WHERE
258 algorithm = ? AND
258 algorithm = ? AND
259 signature = ?;
259 signature = ?;
260 """,
260 """,
261 (datetime.utcnow(), self.algorithm, signature),
261 (datetime.utcnow(), self.algorithm, signature),
262 )
262 )
263 self.db.commit()
263 self.db.commit()
264 n, = self.db.execute("SELECT Count(*) FROM nbsignatures").fetchone()
264 n, = self.db.execute("SELECT Count(*) FROM nbsignatures").fetchone()
265 if n > self.cache_size:
265 if n > self.cache_size:
266 self.cull_db()
266 self.cull_db()
267
267
268 def unsign(self, nb):
268 def unsign(self, nb):
269 """Ensure that a notebook is untrusted
269 """Ensure that a notebook is untrusted
270
270
271 by removing its signature from the trusted database, if present.
271 by removing its signature from the trusted database, if present.
272 """
272 """
273 signature = self.compute_signature(nb)
273 signature = self.compute_signature(nb)
274 self.db.execute("""DELETE FROM nbsignatures WHERE
274 self.db.execute("""DELETE FROM nbsignatures WHERE
275 algorithm = ? AND
275 algorithm = ? AND
276 signature = ?;
276 signature = ?;
277 """,
277 """,
278 (self.algorithm, signature)
278 (self.algorithm, signature)
279 )
279 )
280 self.db.commit()
280 self.db.commit()
281
281
282 def cull_db(self):
282 def cull_db(self):
283 """Cull oldest 25% of the trusted signatures when the size limit is reached"""
283 """Cull oldest 25% of the trusted signatures when the size limit is reached"""
284 self.db.execute("""DELETE FROM nbsignatures WHERE id IN (
284 self.db.execute("""DELETE FROM nbsignatures WHERE id IN (
285 SELECT id FROM nbsignatures ORDER BY last_seen DESC LIMIT -1 OFFSET ?
285 SELECT id FROM nbsignatures ORDER BY last_seen DESC LIMIT -1 OFFSET ?
286 );
286 );
287 """, (max(int(0.75 * self.cache_size), 1),))
287 """, (max(int(0.75 * self.cache_size), 1),))
288
288
289 def mark_cells(self, nb, trusted):
289 def mark_cells(self, nb, trusted):
290 """Mark cells as trusted if the notebook's signature can be verified
290 """Mark cells as trusted if the notebook's signature can be verified
291
291
292 Sets ``cell.metadata.trusted = True | False`` on all code cells,
292 Sets ``cell.metadata.trusted = True | False`` on all code cells,
293 depending on whether the stored signature can be verified.
293 depending on whether the stored signature can be verified.
294
294
295 This function is the inverse of check_cells
295 This function is the inverse of check_cells
296 """
296 """
297 if nb.nbformat < 3:
297 if nb.nbformat < 3:
298 return
298 return
299
299
300 for cell in yield_code_cells(nb):
300 for cell in yield_code_cells(nb):
301 cell['metadata']['trusted'] = trusted
301 cell['metadata']['trusted'] = trusted
302
302
303 def _check_cell(self, cell, nbformat_version):
303 def _check_cell(self, cell, nbformat_version):
304 """Do we trust an individual cell?
304 """Do we trust an individual cell?
305
305
306 Return True if:
306 Return True if:
307
307
308 - cell is explicitly trusted
308 - cell is explicitly trusted
309 - cell has no potentially unsafe rich output
309 - cell has no potentially unsafe rich output
310
310
311 If a cell has no output, or only simple print statements,
311 If a cell has no output, or only simple print statements,
312 it will always be trusted.
312 it will always be trusted.
313 """
313 """
314 # explicitly trusted
314 # explicitly trusted
315 if cell['metadata'].pop("trusted", False):
315 if cell['metadata'].pop("trusted", False):
316 return True
316 return True
317
317
318 # explicitly safe output
318 # explicitly safe output
319 if nbformat_version >= 4:
319 if nbformat_version >= 4:
320 unsafe_output_types = ['execute_result', 'display_data']
320 unsafe_output_types = ['execute_result', 'display_data']
321 safe_keys = {"output_type", "execution_count", "metadata"}
321 safe_keys = {"output_type", "execution_count", "metadata"}
322 else: # v3
322 else: # v3
323 unsafe_output_types = ['pyout', 'display_data']
323 unsafe_output_types = ['pyout', 'display_data']
324 safe_keys = {"output_type", "prompt_number", "metadata"}
324 safe_keys = {"output_type", "prompt_number", "metadata"}
325
325
326 for output in cell['outputs']:
326 for output in cell['outputs']:
327 output_type = output['output_type']
327 output_type = output['output_type']
328 if output_type in unsafe_output_types:
328 if output_type in unsafe_output_types:
329 # if there are any data keys not in the safe whitelist
329 # if there are any data keys not in the safe whitelist
330 output_keys = set(output)
330 output_keys = set(output)
331 if output_keys.difference(safe_keys):
331 if output_keys.difference(safe_keys):
332 return False
332 return False
333
333
334 return True
334 return True
335
335
336 def check_cells(self, nb):
336 def check_cells(self, nb):
337 """Return whether all code cells are trusted
337 """Return whether all code cells are trusted
338
338
339 If there are no code cells, return True.
339 If there are no code cells, return True.
340
340
341 This function is the inverse of mark_cells.
341 This function is the inverse of mark_cells.
342 """
342 """
343 if nb.nbformat < 3:
343 if nb.nbformat < 3:
344 return False
344 return False
345 trusted = True
345 trusted = True
346 for cell in yield_code_cells(nb):
346 for cell in yield_code_cells(nb):
347 # only distrust a cell if it actually has some output to distrust
347 # only distrust a cell if it actually has some output to distrust
348 if not self._check_cell(cell, nb.nbformat):
348 if not self._check_cell(cell, nb.nbformat):
349 trusted = False
349 trusted = False
350
350
351 return trusted
351 return trusted
352
352
353
353
354 trust_flags = {
354 trust_flags = {
355 'reset' : (
355 'reset' : (
356 {'TrustNotebookApp' : { 'reset' : True}},
356 {'TrustNotebookApp' : { 'reset' : True}},
357 """Delete the trusted notebook cache.
357 """Delete the trusted notebook cache.
358 All previously signed notebooks will become untrusted.
358 All previously signed notebooks will become untrusted.
359 """
359 """
360 ),
360 ),
361 }
361 }
362 trust_flags.update(base_flags)
362 trust_flags.update(base_flags)
363 trust_flags.pop('init')
363 trust_flags.pop('init')
364
364
365
365
366 class TrustNotebookApp(BaseIPythonApplication):
366 class TrustNotebookApp(BaseIPythonApplication):
367
367
368 description="""Sign one or more IPython notebooks with your key,
368 description="""Sign one or more IPython notebooks with your key,
369 to trust their dynamic (HTML, Javascript) output.
369 to trust their dynamic (HTML, Javascript) output.
370
370
371 Trusting a notebook only applies to the current IPython profile.
371 Trusting a notebook only applies to the current IPython profile.
372 To trust a notebook for use with a profile other than default,
372 To trust a notebook for use with a profile other than default,
373 add `--profile [profile name]`.
373 add `--profile [profile name]`.
374
374
375 Otherwise, you will have to re-execute the notebook to see output.
375 Otherwise, you will have to re-execute the notebook to see output.
376 """
376 """
377
377
378 examples = """
378 examples = """
379 ipython trust mynotebook.ipynb and_this_one.ipynb
379 ipython trust mynotebook.ipynb and_this_one.ipynb
380 ipython trust --profile myprofile mynotebook.ipynb
380 ipython trust --profile myprofile mynotebook.ipynb
381 """
381 """
382
382
383 flags = trust_flags
383 flags = trust_flags
384
384
385 reset = Bool(False, config=True,
385 reset = Bool(False, config=True,
386 help="""If True, delete the trusted signature cache.
386 help="""If True, delete the trusted signature cache.
387 After reset, all previously signed notebooks will become untrusted.
387 After reset, all previously signed notebooks will become untrusted.
388 """
388 """
389 )
389 )
390
390
391 notary = Instance(NotebookNotary)
391 notary = Instance(NotebookNotary)
392 def _notary_default(self):
392 def _notary_default(self):
393 return NotebookNotary(parent=self, profile_dir=self.profile_dir)
393 return NotebookNotary(parent=self, profile_dir=self.profile_dir)
394
394
395 def sign_notebook(self, notebook_path):
395 def sign_notebook(self, notebook_path):
396 if not os.path.exists(notebook_path):
396 if not os.path.exists(notebook_path):
397 self.log.error("Notebook missing: %s" % notebook_path)
397 self.log.error("Notebook missing: %s" % notebook_path)
398 self.exit(1)
398 self.exit(1)
399 with io.open(notebook_path, encoding='utf8') as f:
399 with io.open(notebook_path, encoding='utf8') as f:
400 nb = read(f, NO_CONVERT)
400 nb = read(f, NO_CONVERT)
401 if self.notary.check_signature(nb):
401 if self.notary.check_signature(nb):
402 print("Notebook already signed: %s" % notebook_path)
402 print("Notebook already signed: %s" % notebook_path)
403 else:
403 else:
404 print("Signing notebook: %s" % notebook_path)
404 print("Signing notebook: %s" % notebook_path)
405 self.notary.sign(nb)
405 self.notary.sign(nb)
406 with atomic_writing(notebook_path) as f:
406 with atomic_writing(notebook_path) as f:
407 write(nb, f, NO_CONVERT)
407 write(nb, f, NO_CONVERT)
408
408
409 def generate_new_key(self):
409 def generate_new_key(self):
410 """Generate a new notebook signature key"""
410 """Generate a new notebook signature key"""
411 print("Generating new notebook key: %s" % self.notary.secret_file)
411 print("Generating new notebook key: %s" % self.notary.secret_file)
412 self.notary._write_secret_file(os.urandom(1024))
412 self.notary._write_secret_file(os.urandom(1024))
413
413
414 def start(self):
414 def start(self):
415 if self.reset:
415 if self.reset:
416 if os.path.exists(self.notary.db_file):
416 if os.path.exists(self.notary.db_file):
417 print("Removing trusted signature cache: %s" % self.notary.db_file)
417 print("Removing trusted signature cache: %s" % self.notary.db_file)
418 os.remove(self.notary.db_file)
418 os.remove(self.notary.db_file)
419 self.generate_new_key()
419 self.generate_new_key()
420 return
420 return
421 if not self.extra_args:
421 if not self.extra_args:
422 self.log.critical("Specify at least one notebook to sign.")
422 self.log.critical("Specify at least one notebook to sign.")
423 self.exit(1)
423 self.exit(1)
424
424
425 for notebook_path in self.extra_args:
425 for notebook_path in self.extra_args:
426 self.sign_notebook(notebook_path)
426 self.sign_notebook(notebook_path)
427
427
General Comments 0
You need to be logged in to leave comments. Login now