/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.zeppelin.notebook.repo;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.URISyntaxException;
import java.security.InvalidKeyException;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.zeppelin.conf.ZeppelinConfiguration;
import org.apache.zeppelin.notebook.Note;
import org.apache.zeppelin.notebook.NoteInfo;
import org.apache.zeppelin.notebook.NotebookImportDeserializer;
import org.apache.zeppelin.notebook.Paragraph;
import org.apache.zeppelin.scheduler.Job;
import org.apache.zeppelin.user.AuthenticationInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.microsoft.azure.storage.CloudStorageAccount;
import com.microsoft.azure.storage.StorageException;
import com.microsoft.azure.storage.file.CloudFile;
import com.microsoft.azure.storage.file.CloudFileClient;
import com.microsoft.azure.storage.file.CloudFileDirectory;
import com.microsoft.azure.storage.file.CloudFileShare;
import com.microsoft.azure.storage.file.ListFileItem;
/**
* Azure storage backend for notebooks
*/
public class AzureNotebookRepo implements NotebookRepo {
private static final Logger LOG = LoggerFactory.getLogger(S3NotebookRepo.class);
private final ZeppelinConfiguration conf;
private final String user;
private final String shareName;
private final CloudFileDirectory rootDir;
public AzureNotebookRepo(ZeppelinConfiguration conf)
throws URISyntaxException, InvalidKeyException, StorageException {
this.conf = conf;
user = conf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_NOTEBOOK_AZURE_USER);
shareName = conf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_NOTEBOOK_AZURE_SHARE);
CloudStorageAccount account = CloudStorageAccount.parse(
conf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_NOTEBOOK_AZURE_CONNECTION_STRING));
CloudFileClient client = account.createCloudFileClient();
CloudFileShare share = client.getShareReference(shareName);
share.createIfNotExists();
CloudFileDirectory userDir = StringUtils.isBlank(user) ?
share.getRootDirectoryReference() :
share.getRootDirectoryReference().getDirectoryReference(user);
userDir.createIfNotExists();
rootDir = userDir.getDirectoryReference("notebook");
rootDir.createIfNotExists();
}
@Override
public List<NoteInfo> list(AuthenticationInfo subject) throws IOException {
List<NoteInfo> infos = new LinkedList<>();
NoteInfo info = null;
for (ListFileItem item : rootDir.listFilesAndDirectories()) {
if (item.getClass() == CloudFileDirectory.class) {
CloudFileDirectory dir = (CloudFileDirectory) item;
try {
if (dir.getFileReference("note.json").exists()) {
info = new NoteInfo(getNote(dir.getName()));
if (info != null) {
infos.add(info);
}
}
} catch (StorageException | URISyntaxException e) {
String msg = "Error enumerating notebooks from Azure storage";
LOG.error(msg, e);
} catch (Exception e) {
LOG.error(e.getMessage(), e);
}
}
}
return infos;
}
private Note getNote(String noteId) throws IOException {
InputStream ins = null;
try {
CloudFileDirectory dir = rootDir.getDirectoryReference(noteId);
CloudFile file = dir.getFileReference("note.json");
ins = file.openRead();
} catch (URISyntaxException | StorageException e) {
String msg = String.format("Error reading notebook %s from Azure storage", noteId);
LOG.error(msg, e);
throw new IOException(msg, e);
}
String json = IOUtils.toString(ins,
conf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_ENCODING));
ins.close();
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.setPrettyPrinting();
Gson gson = gsonBuilder.registerTypeAdapter(Date.class, new NotebookImportDeserializer())
.create();
Note note = Note.fromJson(json);
for (Paragraph p : note.getParagraphs()) {
if (p.getStatus() == Job.Status.PENDING || p.getStatus() == Job.Status.RUNNING) {
p.setStatus(Job.Status.ABORT);
}
}
return note;
}
@Override
public Note get(String noteId, AuthenticationInfo subject) throws IOException {
return getNote(noteId);
}
@Override
public void save(Note note, AuthenticationInfo subject) throws IOException {
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.setPrettyPrinting();
Gson gson = gsonBuilder.create();
String json = gson.toJson(note);
ByteArrayOutputStream output = new ByteArrayOutputStream();
Writer writer = new OutputStreamWriter(output);
writer.write(json);
writer.close();
output.close();
byte[] buffer = output.toByteArray();
try {
CloudFileDirectory dir = rootDir.getDirectoryReference(note.getId());
dir.createIfNotExists();
CloudFile cloudFile = dir.getFileReference("note.json");
cloudFile.uploadFromByteArray(buffer, 0, buffer.length);
} catch (URISyntaxException | StorageException e) {
String msg = String.format("Error saving notebook %s to Azure storage", note.getId());
LOG.error(msg, e);
throw new IOException(msg, e);
}
}
// unfortunately, we need to use a recursive delete here
private void delete(ListFileItem item) throws StorageException {
if (item.getClass() == CloudFileDirectory.class) {
CloudFileDirectory dir = (CloudFileDirectory) item;
for (ListFileItem subItem : dir.listFilesAndDirectories()) {
delete(subItem);
}
dir.deleteIfExists();
} else if (item.getClass() == CloudFile.class) {
CloudFile file = (CloudFile) item;
file.deleteIfExists();
}
}
@Override
public void remove(String noteId, AuthenticationInfo subject) throws IOException {
try {
CloudFileDirectory dir = rootDir.getDirectoryReference(noteId);
delete(dir);
} catch (URISyntaxException | StorageException e) {
String msg = String.format("Error deleting notebook %s from Azure storage", noteId);
LOG.error(msg, e);
throw new IOException(msg, e);
}
}
@Override
public void close() {
}
@Override
public Revision checkpoint(String noteId, String checkpointMsg, AuthenticationInfo subject)
throws IOException {
// no-op
LOG.warn("Checkpoint feature isn't supported in {}", this.getClass().toString());
return Revision.EMPTY;
}
@Override
public Note get(String noteId, String revId, AuthenticationInfo subject) throws IOException {
LOG.warn("Get note revision feature isn't supported in {}", this.getClass().toString());
return null;
}
@Override
public List<Revision> revisionHistory(String noteId, AuthenticationInfo subject) {
LOG.warn("Get Note revisions feature isn't supported in {}", this.getClass().toString());
return Collections.emptyList();
}
@Override
public List<NotebookRepoSettingsInfo> getSettings(AuthenticationInfo subject) {
LOG.warn("Method not implemented");
return Collections.emptyList();
}
@Override
public void updateSettings(Map<String, String> settings, AuthenticationInfo subject) {
LOG.warn("Method not implemented");
}
@Override
public Note setNoteRevision(String noteId, String revId, AuthenticationInfo subject)
throws IOException {
// Auto-generated method stub
return null;
}
}