blob: 7effcb78314be4b8713c58f272880bbdd934e7e1 [file] [log] [blame]
/*
* 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.cloudstack.storage.datastore.driver;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.inject.Inject;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
import org.apache.cloudstack.storage.datastore.db.ObjectStoreDao;
import org.apache.cloudstack.storage.datastore.db.ObjectStoreDetailsDao;
import org.apache.cloudstack.storage.datastore.db.ObjectStoreVO;
import org.apache.cloudstack.storage.object.BaseObjectStoreDriverImpl;
import org.apache.cloudstack.storage.object.Bucket;
import org.apache.cloudstack.storage.object.BucketObject;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import com.amazonaws.services.s3.model.AccessControlList;
import com.amazonaws.services.s3.model.BucketPolicy;
import com.cloud.agent.api.to.DataStoreTO;
import com.cloud.storage.BucketVO;
import com.cloud.storage.dao.BucketDao;
import com.cloud.user.Account;
import com.cloud.user.AccountDetailsDao;
import com.cloud.user.dao.AccountDao;
import com.cloud.utils.exception.CloudRuntimeException;
import io.minio.BucketExistsArgs;
import io.minio.DeleteBucketEncryptionArgs;
import io.minio.MakeBucketArgs;
import io.minio.MinioClient;
import io.minio.RemoveBucketArgs;
import io.minio.SetBucketEncryptionArgs;
import io.minio.SetBucketPolicyArgs;
import io.minio.SetBucketVersioningArgs;
import io.minio.admin.MinioAdminClient;
import io.minio.admin.QuotaUnit;
import io.minio.admin.UserInfo;
import io.minio.admin.messages.DataUsageInfo;
import io.minio.messages.SseConfiguration;
import io.minio.messages.VersioningConfiguration;
public class MinIOObjectStoreDriverImpl extends BaseObjectStoreDriverImpl {
protected static final String ACS_PREFIX = "acs";
@Inject
AccountDao _accountDao;
@Inject
AccountDetailsDao _accountDetailsDao;
@Inject
ObjectStoreDao _storeDao;
@Inject
BucketDao _bucketDao;
@Inject
ObjectStoreDetailsDao _storeDetailsDao;
private static final String ACCESS_KEY = "accesskey";
private static final String SECRET_KEY = "secretkey";
protected static final String MINIO_ACCESS_KEY = "minio-accesskey";
protected static final String MINIO_SECRET_KEY = "minio-secretkey";
@Override
public DataStoreTO getStoreTO(DataStore store) {
return null;
}
protected String getUserOrAccessKeyForAccount(Account account) {
return String.format("%s-%s", ACS_PREFIX, account.getUuid());
}
@Override
public Bucket createBucket(Bucket bucket, boolean objectLock) {
//ToDo Client pool mgmt
String bucketName = bucket.getName();
long storeId = bucket.getObjectStoreId();
long accountId = bucket.getAccountId();
MinioClient minioClient = getMinIOClient(storeId);
Account account = _accountDao.findById(accountId);
if ((_accountDetailsDao.findDetail(accountId, MINIO_ACCESS_KEY) == null)
|| (_accountDetailsDao.findDetail(accountId, MINIO_SECRET_KEY) == null)) {
throw new CloudRuntimeException("Bucket access credentials unavailable for account: "+account.getAccountName());
}
try {
if(minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {
throw new CloudRuntimeException("Bucket already exists with name "+ bucketName);
}
} catch (Exception e) {
throw new CloudRuntimeException(e);
}
try {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).objectLock(objectLock).build());
} catch (Exception e) {
throw new CloudRuntimeException(e);
}
List<BucketVO> buckets = _bucketDao.listByObjectStoreIdAndAccountId(storeId, accountId);
StringBuilder resources_builder = new StringBuilder();
for(BucketVO exitingBucket : buckets) {
resources_builder.append("\"arn:aws:s3:::"+exitingBucket.getName()+"/*\",\n");
}
resources_builder.append("\"arn:aws:s3:::"+bucketName+"/*\"\n");
String policy = " {\n" +
" \"Statement\": [\n" +
" {\n" +
" \"Action\": \"s3:*\",\n" +
" \"Effect\": \"Allow\",\n" +
" \"Principal\": \"*\",\n" +
" \"Resource\": ["+resources_builder+"]" +
" }\n" +
" ],\n" +
" \"Version\": \"2012-10-17\"\n" +
" }";
MinioAdminClient minioAdminClient = getMinIOAdminClient(storeId);
String policyName = getUserOrAccessKeyForAccount(account) + "-policy";
String userName = getUserOrAccessKeyForAccount(account);
try {
minioAdminClient.addCannedPolicy(policyName, policy);
minioAdminClient.setPolicy(userName, false, policyName);
} catch (Exception e) {
throw new CloudRuntimeException(e);
}
String accessKey = _accountDetailsDao.findDetail(accountId, MINIO_ACCESS_KEY).getValue();
String secretKey = _accountDetailsDao.findDetail(accountId, MINIO_SECRET_KEY).getValue();
ObjectStoreVO store = _storeDao.findById(storeId);
BucketVO bucketVO = _bucketDao.findById(bucket.getId());
bucketVO.setAccessKey(accessKey);
bucketVO.setSecretKey(secretKey);
bucketVO.setBucketURL(store.getUrl()+"/"+bucketName);
_bucketDao.update(bucket.getId(), bucketVO);
return bucket;
}
@Override
public List<Bucket> listBuckets(long storeId) {
MinioClient minioClient = getMinIOClient(storeId);
List<Bucket> bucketsList = new ArrayList<>();
try {
List<io.minio.messages.Bucket> minIOBuckets = minioClient.listBuckets();
for(io.minio.messages.Bucket minIObucket : minIOBuckets) {
Bucket bucket = new BucketObject();
bucket.setName(minIObucket.name());
bucketsList.add(bucket);
}
} catch (Exception e) {
throw new CloudRuntimeException(e);
}
return bucketsList;
}
@Override
public boolean deleteBucket(String bucketName, long storeId) {
MinioClient minioClient = getMinIOClient(storeId);
try {
if(!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {
throw new CloudRuntimeException("Bucket doesn't exist: "+ bucketName);
}
} catch (Exception e) {
throw new CloudRuntimeException(e);
}
//ToDo: check bucket empty
try {
minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
} catch (Exception e) {
throw new CloudRuntimeException(e);
}
return true;
}
@Override
public AccessControlList getBucketAcl(String bucketName, long storeId) {
return null;
}
@Override
public void setBucketAcl(String bucketName, AccessControlList acl, long storeId) {
}
@Override
public void setBucketPolicy(String bucketName, String policy, long storeId) {
String privatePolicy = "{\"Version\":\"2012-10-17\",\"Statement\":[]}";
StringBuilder builder = new StringBuilder();
builder.append("{\n");
builder.append(" \"Statement\": [\n");
builder.append(" {\n");
builder.append(" \"Action\": [\n");
builder.append(" \"s3:GetBucketLocation\",\n");
builder.append(" \"s3:ListBucket\"\n");
builder.append(" ],\n");
builder.append(" \"Effect\": \"Allow\",\n");
builder.append(" \"Principal\": \"*\",\n");
builder.append(" \"Resource\": \"arn:aws:s3:::"+bucketName+"\"\n");
builder.append(" },\n");
builder.append(" {\n");
builder.append(" \"Action\": \"s3:GetObject\",\n");
builder.append(" \"Effect\": \"Allow\",\n");
builder.append(" \"Principal\": \"*\",\n");
builder.append(" \"Resource\": \"arn:aws:s3:::"+bucketName+"/*\"\n");
builder.append(" }\n");
builder.append(" ],\n");
builder.append(" \"Version\": \"2012-10-17\"\n");
builder.append("}\n");
String publicPolicy = builder.toString();
//ToDo Support custom policy
String policyConfig = (policy.equalsIgnoreCase("public"))? publicPolicy : privatePolicy;
MinioClient minioClient = getMinIOClient(storeId);
try {
minioClient.setBucketPolicy(
SetBucketPolicyArgs.builder().bucket(bucketName).config(policyConfig).build());
} catch (Exception e) {
throw new CloudRuntimeException(e);
}
}
@Override
public BucketPolicy getBucketPolicy(String bucketName, long storeId) {
return null;
}
@Override
public void deleteBucketPolicy(String bucketName, long storeId) {
}
protected void updateAccountCredentials(final long accountId, final String accessKey, final String secretKey, final boolean checkIfNotPresent) {
Map<String, String> details = _accountDetailsDao.findDetails(accountId);
boolean updateNeeded = false;
if (!checkIfNotPresent || StringUtils.isBlank(details.get(MINIO_ACCESS_KEY))) {
details.put(MINIO_ACCESS_KEY, accessKey);
updateNeeded = true;
}
if (StringUtils.isAllBlank(secretKey, details.get(MINIO_SECRET_KEY))) {
logger.error(String.format("Failed to retrieve secret key for MinIO user: %s from store and account details", accessKey));
}
if (StringUtils.isNotBlank(secretKey) && (!checkIfNotPresent || StringUtils.isBlank(details.get(MINIO_SECRET_KEY)))) {
details.put(MINIO_SECRET_KEY, secretKey);
updateNeeded = true;
}
if (!updateNeeded) {
return;
}
_accountDetailsDao.persist(accountId, details);
}
@Override
public boolean createUser(long accountId, long storeId) {
Account account = _accountDao.findById(accountId);
MinioAdminClient minioAdminClient = getMinIOAdminClient(storeId);
String accessKey = getUserOrAccessKeyForAccount(account);
// Check user exists
try {
UserInfo userInfo = minioAdminClient.getUserInfo(accessKey);
if(userInfo != null) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("Skipping user creation as the user already exists in MinIO store: %s", accessKey));
}
updateAccountCredentials(accountId, accessKey, userInfo.secretKey(), true);
return true;
}
} catch (NoSuchAlgorithmException | IOException | InvalidKeyException e) {
logger.error(String.format("Error encountered while retrieving user: %s for existing MinIO store user check", accessKey), e);
return false;
} catch (RuntimeException e) { // MinIO lib may throw RuntimeException with code: XMinioAdminNoSuchUser
if (logger.isDebugEnabled()) {
logger.debug(String.format("Ignoring error encountered while retrieving user: %s for existing MinIO store user check", accessKey));
}
logger.trace("Exception during MinIO user check", e);
}
if (logger.isDebugEnabled()) {
logger.debug(String.format("MinIO store user does not exist. Creating user: %s", accessKey));
}
KeyGenerator generator = null;
try {
generator = KeyGenerator.getInstance("HmacSHA1");
} catch (NoSuchAlgorithmException e) {
throw new CloudRuntimeException(e);
}
SecretKey key = generator.generateKey();
String secretKey = Base64.encodeBase64URLSafeString(key.getEncoded());
try {
minioAdminClient.addUser(accessKey, UserInfo.Status.ENABLED, secretKey, "", new ArrayList<String>());
} catch (Exception e) {
throw new CloudRuntimeException(e);
}
// Store user credentials
updateAccountCredentials(accountId, accessKey, secretKey, false);
return true;
}
@Override
public boolean setBucketEncryption(String bucketName, long storeId) {
MinioClient minioClient = getMinIOClient(storeId);
try {
minioClient.setBucketEncryption(SetBucketEncryptionArgs.builder()
.bucket(bucketName)
.config(SseConfiguration.newConfigWithSseS3Rule())
.build()
);
} catch (Exception e) {
throw new CloudRuntimeException(e);
}
return true;
}
@Override
public boolean deleteBucketEncryption(String bucketName, long storeId) {
MinioClient minioClient = getMinIOClient(storeId);
try {
minioClient.deleteBucketEncryption(DeleteBucketEncryptionArgs.builder()
.bucket(bucketName)
.build()
);
} catch (Exception e) {
throw new CloudRuntimeException(e);
}
return true;
}
@Override
public boolean setBucketVersioning(String bucketName, long storeId) {
MinioClient minioClient = getMinIOClient(storeId);
try {
minioClient.setBucketVersioning(SetBucketVersioningArgs.builder()
.bucket(bucketName)
.config(new VersioningConfiguration(VersioningConfiguration.Status.ENABLED, null))
.build()
);
} catch (Exception e) {
throw new CloudRuntimeException(e);
}
return true;
}
@Override
public boolean deleteBucketVersioning(String bucketName, long storeId) {
MinioClient minioClient = getMinIOClient(storeId);
try {
minioClient.setBucketVersioning(SetBucketVersioningArgs.builder()
.bucket(bucketName)
.config(new VersioningConfiguration(VersioningConfiguration.Status.SUSPENDED, null))
.build()
);
} catch (Exception e) {
throw new CloudRuntimeException(e);
}
return true;
}
@Override
public void setBucketQuota(String bucketName, long storeId, long size) {
MinioAdminClient minioAdminClient = getMinIOAdminClient(storeId);
try {
minioAdminClient.setBucketQuota(bucketName, size, QuotaUnit.GB);
} catch (Exception e) {
throw new CloudRuntimeException(e);
}
}
@Override
public Map<String, Long> getAllBucketsUsage(long storeId) {
MinioAdminClient minioAdminClient = getMinIOAdminClient(storeId);
try {
DataUsageInfo dataUsageInfo = minioAdminClient.getDataUsageInfo();
return dataUsageInfo.bucketsSizes();
} catch (Exception e) {
throw new CloudRuntimeException(e);
}
}
protected MinioClient getMinIOClient(long storeId) {
ObjectStoreVO store = _storeDao.findById(storeId);
Map<String, String> storeDetails = _storeDetailsDao.getDetails(storeId);
String url = store.getUrl();
String accessKey = storeDetails.get(ACCESS_KEY);
String secretKey = storeDetails.get(SECRET_KEY);
MinioClient minioClient =
MinioClient.builder()
.endpoint(url)
.credentials(accessKey,secretKey)
.build();
if(minioClient == null){
throw new CloudRuntimeException("Error while creating MinIO client");
}
return minioClient;
}
protected MinioAdminClient getMinIOAdminClient(long storeId) {
ObjectStoreVO store = _storeDao.findById(storeId);
Map<String, String> storeDetails = _storeDetailsDao.getDetails(storeId);
String url = store.getUrl();
String accessKey = storeDetails.get(ACCESS_KEY);
String secretKey = storeDetails.get(SECRET_KEY);
MinioAdminClient minioAdminClient =
MinioAdminClient.builder()
.endpoint(url)
.credentials(accessKey,secretKey)
.build();
if(minioAdminClient == null){
throw new CloudRuntimeException("Error while creating MinIO client");
}
return minioAdminClient;
}
}