/*
* The Alluxio Open Foundation licenses this work under the Apache License, version 2.0
* (the "License"). You may not use this work except in compliance with the License, which is
* available at www.apache.org/licenses/LICENSE-2.0
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied, as more fully set forth in the License.
*
* See the NOTICE file distributed with this work for information regarding copyright ownership.
*/
package alluxio.underfs.swift;
import alluxio.AlluxioURI;
import alluxio.Constants;
import alluxio.PropertyKey;
import alluxio.exception.ExceptionMessage;
import alluxio.exception.FileDoesNotExistException;
import alluxio.underfs.ObjectUnderFileSystem;
import alluxio.underfs.UnderFileSystem;
import alluxio.underfs.UnderFileSystemConfiguration;
import alluxio.underfs.options.OpenOptions;
import alluxio.underfs.swift.http.SwiftDirectClient;
import alluxio.util.UnderFileSystemUtils;
import alluxio.util.io.PathUtils;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig;
import org.javaswift.joss.client.factory.AccountConfig;
import org.javaswift.joss.client.factory.AccountFactory;
import org.javaswift.joss.client.factory.AuthenticationMethod;
import org.javaswift.joss.exception.CommandException;
import org.javaswift.joss.model.Access;
import org.javaswift.joss.model.Account;
import org.javaswift.joss.model.Container;
import org.javaswift.joss.model.DirectoryOrObject;
import org.javaswift.joss.model.PaginationMap;
import org.javaswift.joss.model.StoredObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.List;
import javax.annotation.concurrent.ThreadSafe;
/**
* OpenStack Swift {@link UnderFileSystem} implementation based on the JOSS library. The mkdir
* operation creates a zero-byte object. A suffix {@link SwiftUnderFileSystem#PATH_SEPARATOR} in the
* object name denotes a folder. JOSS directory listing API requires that the suffix be a single
* character.
*/
@ThreadSafe
public class SwiftUnderFileSystem extends ObjectUnderFileSystem {
private static final Logger LOG = LoggerFactory.getLogger(SwiftUnderFileSystem.class);
/** Regexp for Swift container ACL separator, including optional whitespaces. */
private static final String ACL_SEPARATOR_REGEXP = "\\s*,\\s*";
/** Suffix for an empty file to flag it as a directory. */
private static final String FOLDER_SUFFIX = PATH_SEPARATOR;
/** Number of retries in case of Swift internal errors. */
private static final int NUM_RETRIES = 3;
/** Swift account. */
private final Account mAccount;
/** Container name of user's configured Alluxio container. */
private final String mContainerName;
/** JOSS access object. */
private final Access mAccess;
/** Determine whether to run JOSS in simulation mode. */
private boolean mSimulationMode;
/** The name of the account owner. */
private String mAccountOwner;
/** The permission mode that the account owner has to the container. */
private short mAccountMode;
/**
* Constructs a new Swift {@link UnderFileSystem}.
*
* @param uri the {@link AlluxioURI} for this UFS
* @param conf the configuration for this UFS
* @throws FileDoesNotExistException when specified container does not exist
*/
public SwiftUnderFileSystem(AlluxioURI uri, UnderFileSystemConfiguration conf)
throws FileDoesNotExistException {
super(uri, conf);
String containerName = UnderFileSystemUtils.getBucketName(uri);
LOG.debug("Constructor init: {}", containerName);
AccountConfig config = new AccountConfig();
// Whether to run against a simulated Swift backend
mSimulationMode = false;
if (conf.containsKey(PropertyKey.SWIFT_SIMULATION)) {
mSimulationMode = Boolean.valueOf(conf.getValue(PropertyKey.SWIFT_SIMULATION));
}
if (mSimulationMode) {
// We do not need access credentials in simulation mode
config.setMock(true);
config.setMockAllowEveryone(true);
} else {
if (conf.containsKey(PropertyKey.SWIFT_API_KEY)) {
config.setPassword(conf.getValue(PropertyKey.SWIFT_API_KEY));
} else if (conf.containsKey(PropertyKey.SWIFT_PASSWORD_KEY)) {
config.setPassword(conf.getValue(PropertyKey.SWIFT_PASSWORD_KEY));
}
config.setAuthUrl(conf.getValue(PropertyKey.SWIFT_AUTH_URL_KEY));
String authMethod = conf.getValue(PropertyKey.SWIFT_AUTH_METHOD_KEY);
if (authMethod != null) {
config.setUsername(conf.getValue(PropertyKey.SWIFT_USER_KEY));
config.setTenantName(conf.getValue(PropertyKey.SWIFT_TENANT_KEY));
switch (authMethod) {
case Constants.SWIFT_AUTH_KEYSTONE:
config.setAuthenticationMethod(AuthenticationMethod.KEYSTONE);
if (conf.containsKey(PropertyKey.SWIFT_REGION_KEY)) {
config.setPreferredRegion(conf.getValue(PropertyKey.SWIFT_REGION_KEY));
}
break;
case Constants.SWIFT_AUTH_KEYSTONE_V3:
if (conf.containsKey(PropertyKey.SWIFT_REGION_KEY)) {
config.setPreferredRegion(conf.getValue(PropertyKey.SWIFT_REGION_KEY));
}
config.setAuthenticationMethod(AuthenticationMethod.EXTERNAL);
KeystoneV3AccessProvider accessProvider = new KeystoneV3AccessProvider(config);
config.setAccessProvider(accessProvider);
break;
case Constants.SWIFT_AUTH_SWIFTAUTH:
// swiftauth authenticates directly against swift
// note: this method is supported in swift object storage api v1
config.setAuthenticationMethod(AuthenticationMethod.BASIC);
// swiftauth requires authentication header to be of the form tenant:user.
// JOSS however generates header of the form user:tenant.
// To resolve this, we switch user with tenant
config.setTenantName(conf.getValue(PropertyKey.SWIFT_USER_KEY));
config.setUsername(conf.getValue(PropertyKey.SWIFT_TENANT_KEY));
break;
default:
config.setAuthenticationMethod(AuthenticationMethod.TEMPAUTH);
// tempauth requires authentication header to be of the form tenant:user.
// JOSS however generates header of the form user:tenant.
// To resolve this, we switch user with tenant
config.setTenantName(conf.getValue(PropertyKey.SWIFT_USER_KEY));
config.setUsername(conf.getValue(PropertyKey.SWIFT_TENANT_KEY));
}
}
}
ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationConfig.Feature.WRAP_ROOT_VALUE, true);
mContainerName = containerName;
mAccount = new AccountFactory(config).createAccount();
// Do not allow container cache to avoid stale directory listings
mAccount.setAllowContainerCaching(false);
mAccess = mAccount.authenticate();
Container container = mAccount.getContainer(containerName);
if (!container.exists()) {
throw new FileDoesNotExistException(ExceptionMessage.PATH_DOES_NOT_EXIST
.getMessage("Container %s does not exist", containerName));
}
// Assume the Swift user name has 1-1 mapping to Alluxio username.
mAccountOwner = conf.getValue(PropertyKey.SWIFT_USER_KEY);
short mode = (short) 0;
List<String> readAcl =
Arrays.asList(container.getContainerReadPermission().split(ACL_SEPARATOR_REGEXP));
// If there is any container ACL for the Swift user, translates it to Alluxio permission.
if (readAcl.contains(mAccountOwner) || readAcl.contains("*") || readAcl.contains(".r:*")) {
mode |= (short) 0500;
}
List<String> writeAcl =
Arrays.asList(container.getcontainerWritePermission().split(ACL_SEPARATOR_REGEXP));
if (writeAcl.contains(mAccountOwner) || writeAcl.contains("*") || writeAcl.contains(".w:*")) {
mode |= (short) 0200;
}
// If there is no container ACL but the user can still access the container, the only
// possibility is that the user has the admin role. In this case, the user should have 0700
// mode to the Swift container.
if (mode == 0 && mAccess.getToken() != null) {
mode = (short) 0700;
}
mAccountMode = mode;
}
@Override
public String getUnderFSType() {
return "swift";
}
// Setting Swift owner via Alluxio is not supported yet. This is a no-op.
@Override
public void setOwner(String path, String user, String group) {}
// Setting Swift mode via Alluxio is not supported yet. This is a no-op.
@Override
public void setMode(String path, short mode) throws IOException {}
@Override
protected boolean copyObject(String source, String destination) {
LOG.debug("copy from {} to {}", source, destination);
// Retry copy for a few times, in case some Swift internal errors happened during copy.
for (int i = 0; i < NUM_RETRIES; i++) {
try {
Container container = mAccount.getContainer(mContainerName);
container.getObject(source).copyObject(container, container.getObject(destination));
return true;
} catch (CommandException e) {
LOG.error("Source path {} does not exist", source);
return false;
} catch (Exception e) {
LOG.error("Failed to copy file {} to {}", source, destination, e.getMessage());
if (i != NUM_RETRIES - 1) {
LOG.error("Retrying copying file {} to {}", source, destination);
}
}
}
LOG.error("Failed to copy file {} to {}, after {} retries", source, destination, NUM_RETRIES);
return false;
}
@Override
protected boolean createEmptyObject(String key) {
try {
Container container = mAccount.getContainer(mContainerName);
StoredObject object = container.getObject(key);
object.uploadObject(new byte[0]);
return true;
} catch (CommandException e) {
LOG.error("Failed to create object: {}", key, e);
return false;
}
}
@Override
protected OutputStream createObject(String key) throws IOException {
if (mSimulationMode) {
return new SwiftMockOutputStream(mAccount, mContainerName, key);
}
return SwiftDirectClient.put(mAccess,
PathUtils.concatPath(PathUtils.normalizePath(mContainerName, PATH_SEPARATOR), key));
}
@Override
protected boolean deleteObject(String path) throws IOException {
try {
Container container = mAccount.getContainer(mContainerName);
StoredObject object = container.getObject(path);
if (object != null) {
object.delete();
return true;
}
} catch (CommandException e) {
LOG.debug("Object {} not found", path);
}
return false;
}
@Override
protected String getFolderSuffix() {
return FOLDER_SUFFIX;
}
@Override
protected ObjectListingChunk getObjectListingChunk(String key, boolean recursive)
throws IOException {
Container container = mAccount.getContainer(mContainerName);
String prefix = PathUtils.normalizePath(key, PATH_SEPARATOR);
// In case key is root (empty string) do not normalize prefix
prefix = prefix.equals(PATH_SEPARATOR) ? "" : prefix;
PaginationMap paginationMap = container.getPaginationMap(prefix, getListingChunkLength());
if (paginationMap != null && paginationMap.getNumberOfPages() > 0) {
return new SwiftObjectListingChunk(paginationMap, 0, recursive);
}
return null;
}
/**
* Wrapper over JOSS {@link PaginationMap}.
*/
private final class SwiftObjectListingChunk implements ObjectListingChunk {
final PaginationMap mPaginationMap;
final int mPage;
final boolean mRecursive;
SwiftObjectListingChunk(PaginationMap paginationMap, int page, boolean recursive) {
mPaginationMap = paginationMap;
mPage = page;
mRecursive = recursive;
}
@Override
public ObjectStatus[] getObjectStatuses() {
ArrayDeque<DirectoryOrObject> objects = new ArrayDeque<>();
Container container = mAccount.getContainer(mContainerName);
if (!mRecursive) {
objects.addAll(container.listDirectory(mPaginationMap.getPrefix(), PATH_SEPARATOR_CHAR,
mPaginationMap.getMarker(mPage), mPaginationMap.getPageSize()));
} else {
objects.addAll(container.list(mPaginationMap, mPage));
}
int i = 0;
ObjectStatus[] res = new ObjectStatus[objects.size()];
for (DirectoryOrObject object : objects) {
if (object.isObject()) {
res[i++] = new ObjectStatus(object.getName(), object.getAsObject().getContentLength(),
object.getAsObject().getLastModifiedAsDate().getTime());
} else {
res[i++] = new ObjectStatus(object.getName());
}
}
return res;
}
@Override
public String[] getCommonPrefixes() {
// When a delimiter is used, the Swift backend infers pseudo-directories and returns the
// directory names (including a trailing '/') as part of the object names returned.
return new String[0];
}
@Override
public ObjectListingChunk getNextChunk() throws IOException {
int nextPage = mPage + 1;
if (nextPage >= mPaginationMap.getNumberOfPages()) {
return null;
}
return new SwiftObjectListingChunk(mPaginationMap, nextPage, mRecursive);
}
}
@Override
protected ObjectStatus getObjectStatus(String key) {
Container container = mAccount.getContainer(mContainerName);
StoredObject meta = container.getObject(key);
if (meta != null && meta.exists()) {
return new ObjectStatus(key, meta.getContentLength(), meta.getLastModifiedAsDate().getTime());
}
return null;
}
// No group in Swift ACL, returns the account owner for group.
@Override
protected ObjectPermissions getPermissions() {
return new ObjectPermissions(mAccountOwner, mAccountOwner, mAccountMode);
}
@Override
protected String getRootKey() {
return Constants.HEADER_SWIFT + mContainerName + PATH_SEPARATOR;
}
@Override
protected InputStream openObject(String key, OpenOptions options) throws IOException {
return new SwiftInputStream(mAccount, mContainerName, key, options.getOffset());
}
}