|
|
"""A notebook manager that uses Azure blob storage.
|
|
|
|
|
|
Authors:
|
|
|
|
|
|
* Brian Granger
|
|
|
"""
|
|
|
|
|
|
#-----------------------------------------------------------------------------
|
|
|
# Copyright (C) 2012 The IPython Development Team
|
|
|
#
|
|
|
# Distributed under the terms of the BSD License. The full license is in
|
|
|
# the file COPYING, distributed as part of this software.
|
|
|
#-----------------------------------------------------------------------------
|
|
|
|
|
|
#-----------------------------------------------------------------------------
|
|
|
# Imports
|
|
|
#-----------------------------------------------------------------------------
|
|
|
|
|
|
import datetime
|
|
|
|
|
|
import azure
|
|
|
from azure.storage import BlobService
|
|
|
|
|
|
from tornado import web
|
|
|
|
|
|
from .basenbmanager import BaseNotebookManager
|
|
|
from IPython.nbformat import current
|
|
|
from IPython.utils.traitlets import Unicode, Instance
|
|
|
|
|
|
|
|
|
#-----------------------------------------------------------------------------
|
|
|
# Classes
|
|
|
#-----------------------------------------------------------------------------
|
|
|
|
|
|
class AzureNotebookManager(BaseNotebookManager):
|
|
|
|
|
|
account_name = Unicode('', config=True, help='Azure storage account name.')
|
|
|
account_key = Unicode('', config=True, help='Azure storage account key.')
|
|
|
container = Unicode('', config=True, help='Container name for notebooks.')
|
|
|
|
|
|
blob_service_host_base = Unicode('.blob.core.windows.net', config=True,
|
|
|
help='The basename for the blob service URL. If running on the preview site this '
|
|
|
'will be .blob.core.azure-preview.com.')
|
|
|
def _blob_service_host_base_changed(self, new):
|
|
|
self._update_service_host_base(new)
|
|
|
|
|
|
blob_service = Instance('azure.storage.BlobService')
|
|
|
def _blob_service_default(self):
|
|
|
return BlobService(account_name=self.account_name, account_key=self.account_key)
|
|
|
|
|
|
def __init__(self, **kwargs):
|
|
|
super(AzureNotebookManager, self).__init__(**kwargs)
|
|
|
self._update_service_host_base(self.blob_service_host_base)
|
|
|
self._create_container()
|
|
|
|
|
|
def _update_service_host_base(self, shb):
|
|
|
azure.BLOB_SERVICE_HOST_BASE = shb
|
|
|
|
|
|
def _create_container(self):
|
|
|
self.blob_service.create_container(self.container)
|
|
|
|
|
|
def load_notebook_names(self):
|
|
|
"""On startup load the notebook ids and names from Azure.
|
|
|
|
|
|
The blob names are the notebook ids and the notebook names are stored
|
|
|
as blob metadata.
|
|
|
"""
|
|
|
self.mapping = {}
|
|
|
blobs = self.blob_service.list_blobs(self.container)
|
|
|
ids = [blob.name for blob in blobs]
|
|
|
|
|
|
for id in ids:
|
|
|
md = self.blob_service.get_blob_metadata(self.container, id)
|
|
|
name = md['x-ms-meta-nbname']
|
|
|
self.mapping[id] = name
|
|
|
|
|
|
def list_notebooks(self):
|
|
|
"""List all notebooks in the container.
|
|
|
|
|
|
This version uses `self.mapping` as the authoritative notebook list.
|
|
|
"""
|
|
|
data = [dict(notebook_id=id,name=name) for id, name in self.mapping.items()]
|
|
|
data = sorted(data, key=lambda item: item['name'])
|
|
|
return data
|
|
|
|
|
|
def read_notebook_object(self, notebook_id):
|
|
|
"""Get the object representation of a notebook by notebook_id."""
|
|
|
if not self.notebook_exists(notebook_id):
|
|
|
raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
|
|
|
try:
|
|
|
s = self.blob_service.get_blob(self.container, notebook_id)
|
|
|
except:
|
|
|
raise web.HTTPError(500, u'Notebook cannot be read.')
|
|
|
try:
|
|
|
# v1 and v2 and json in the .ipynb files.
|
|
|
nb = current.reads(s, u'json')
|
|
|
except:
|
|
|
raise web.HTTPError(500, u'Unreadable JSON notebook.')
|
|
|
# Todo: The last modified should actually be saved in the notebook document.
|
|
|
# We are just using the current datetime until that is implemented.
|
|
|
last_modified = datetime.datetime.utcnow()
|
|
|
return last_modified, nb
|
|
|
|
|
|
def write_notebook_object(self, nb, notebook_id=None):
|
|
|
"""Save an existing notebook object by notebook_id."""
|
|
|
try:
|
|
|
new_name = nb.metadata.name
|
|
|
except AttributeError:
|
|
|
raise web.HTTPError(400, u'Missing notebook name')
|
|
|
|
|
|
if notebook_id is None:
|
|
|
notebook_id = self.new_notebook_id(new_name)
|
|
|
|
|
|
if notebook_id not in self.mapping:
|
|
|
raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
|
|
|
|
|
|
try:
|
|
|
data = current.writes(nb, u'json')
|
|
|
except Exception as e:
|
|
|
raise web.HTTPError(400, u'Unexpected error while saving notebook: %s' % e)
|
|
|
|
|
|
metadata = {'nbname': new_name}
|
|
|
try:
|
|
|
self.blob_service.put_blob(self.container, notebook_id, data, 'BlockBlob', x_ms_meta_name_values=metadata)
|
|
|
except Exception as e:
|
|
|
raise web.HTTPError(400, u'Unexpected error while saving notebook: %s' % e)
|
|
|
|
|
|
self.mapping[notebook_id] = new_name
|
|
|
return notebook_id
|
|
|
|
|
|
def delete_notebook(self, notebook_id):
|
|
|
"""Delete notebook by notebook_id."""
|
|
|
if not self.notebook_exists(notebook_id):
|
|
|
raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
|
|
|
try:
|
|
|
self.blob_service.delete_blob(self.container, notebook_id)
|
|
|
except Exception as e:
|
|
|
raise web.HTTPError(400, u'Unexpected error while deleting notebook: %s' % e)
|
|
|
else:
|
|
|
self.delete_notebook_id(notebook_id)
|
|
|
|
|
|
def log_info(self):
|
|
|
self.log.info("Serving notebooks from Azure storage: %s, %s", self.account_name, self.container)
|
|
|
|