blob: 6d0bbf413f79f3ca28ceeb8da9bdc6a78af0fe3e [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.hadoop.ozone.web.client;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.hdds.scm.client.HddsClientUtils;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.ozone.client.rest.OzoneException;
import org.apache.hadoop.ozone.client.rest.headers.Header;
import org.apache.hadoop.ozone.web.response.ListVolumes;
import org.apache.hadoop.ozone.web.response.VolumeInfo;
import org.apache.hadoop.ozone.web.utils.OzoneUtils;
import org.apache.hadoop.util.Time;
import static org.apache.hadoop.hdds.server.ServerUtils.releaseConnection;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.FileEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import javax.ws.rs.core.HttpHeaders;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import static java.net.HttpURLConnection.HTTP_CREATED;
import static java.net.HttpURLConnection.HTTP_OK;
/**
* Ozone client that connects to an Ozone server. Please note that this class is
* not thread safe.
*/
public class OzoneRestClient implements Closeable {
private URI endPointURI;
private String userAuth;
/**
* Constructor for OzoneRestClient.
*/
public OzoneRestClient() {
}
/**
* Constructor for OzoneRestClient.
*/
public OzoneRestClient(String ozoneURI)
throws OzoneException, URISyntaxException {
setEndPoint(ozoneURI);
}
/**
* Constructor for OzoneRestClient.
*/
public OzoneRestClient(String ozoneURI, String userAuth)
throws OzoneException, URISyntaxException {
setEndPoint(ozoneURI);
setUserAuth(userAuth);
}
/**
* Returns the end Point.
*
* @return String
*/
public URI getEndPointURI() {
return endPointURI;
}
/**
* Sets the End Point info using an URI.
*
* @param endPointURI - URI
* @throws OzoneException
*/
public void setEndPointURI(URI endPointURI) throws OzoneException {
if ((endPointURI == null) || (endPointURI.toString().isEmpty())) {
throw new OzoneRestClientException("Invalid ozone URI");
}
this.endPointURI = endPointURI;
}
/**
* Set endPoint.
*
* @param clusterFQDN - cluster FQDN.
*/
public void setEndPoint(String clusterFQDN) throws
OzoneException, URISyntaxException {
setEndPointURI(new URI(clusterFQDN));
}
/**
* Get user Auth String.
*
* @return - User Auth String
*/
public String getUserAuth() {
return this.userAuth;
}
/**
* Set User Auth.
*
* @param userAuth - User Auth String
*/
public void setUserAuth(String userAuth) {
this.userAuth = userAuth;
}
/**
* create volume.
*
* @param volumeName - volume name 3 - 63 chars, small letters.
* @param onBehalfOf - The user on behalf we are making the call for
* @param quota - Quota's are specified in a specific format. it is
* integer(MB|GB|TB), for example 100TB.
* @throws OzoneRestClientException
*/
public OzoneVolume createVolume(String volumeName, String onBehalfOf,
String quota) throws OzoneException {
HttpPost httpPost = null;
try (CloseableHttpClient httpClient = newHttpClient()) {
OzoneUtils.verifyResourceName(volumeName);
URIBuilder builder = new URIBuilder(endPointURI);
builder.setPath("/" + volumeName);
if (quota != null) {
builder.setParameter(Header.OZONE_QUOTA_QUERY_TAG, quota);
}
httpPost = getHttpPost(onBehalfOf, builder.build().toString());
executeCreateVolume(httpPost, httpClient);
return getVolume(volumeName);
} catch (IOException | URISyntaxException | IllegalArgumentException ex) {
throw new OzoneRestClientException(ex.getMessage(), ex);
} finally {
releaseConnection(httpPost);
}
}
/**
* Returns information about an existing Volume. if the Volume does not exist,
* or if the user does not have access rights OzoneException is thrown
*
* @param volumeName - volume name 3 - 63 chars, small letters.
* @return OzoneVolume Ozone Client Volume Class.
* @throws OzoneException
*/
public OzoneVolume getVolume(String volumeName) throws OzoneException {
HttpGet httpGet = null;
try (CloseableHttpClient httpClient = newHttpClient()) {
OzoneUtils.verifyResourceName(volumeName);
URIBuilder builder = new URIBuilder(endPointURI);
builder.setPath("/" + volumeName)
.setParameter(Header.OZONE_INFO_QUERY_TAG,
Header.OZONE_INFO_QUERY_VOLUME)
.build();
httpGet = getHttpGet(builder.toString());
return executeInfoVolume(httpGet, httpClient);
} catch (IOException | URISyntaxException | IllegalArgumentException ex) {
throw new OzoneRestClientException(ex.getMessage(), ex);
} finally {
releaseConnection(httpGet);
}
}
/**
* List all the volumes owned by the user or Owned by the user specified in
* the behalf of string.
*
* @param onBehalfOf
* User Name of the user if it is not the caller. for example,
* an admin wants to list some other users volumes.
* @param prefix
* Return only volumes that match this prefix.
* @param maxKeys
* Maximum number of results to return, if the result set
* is smaller than requested size, it means that list is
* complete.
* @param previousVolume
* The previous volume name.
* @return List of Volumes
* @throws OzoneException
*/
public List<OzoneVolume> listVolumes(String onBehalfOf, String prefix,
int maxKeys, String previousVolume) throws OzoneException {
HttpGet httpGet = null;
try (CloseableHttpClient httpClient = newHttpClient()) {
URIBuilder builder = new URIBuilder(endPointURI);
if (!Strings.isNullOrEmpty(prefix)) {
builder.addParameter(Header.OZONE_LIST_QUERY_PREFIX, prefix);
}
if (maxKeys > 0) {
builder.addParameter(Header.OZONE_LIST_QUERY_MAXKEYS, Integer
.toString(maxKeys));
}
if (!Strings.isNullOrEmpty(previousVolume)) {
builder.addParameter(Header.OZONE_LIST_QUERY_PREVKEY,
previousVolume);
}
builder.setPath("/").build();
httpGet = getHttpGet(builder.toString());
if (onBehalfOf != null) {
httpGet.addHeader(Header.OZONE_USER, onBehalfOf);
}
return executeListVolume(httpGet, httpClient);
} catch (IOException | URISyntaxException ex) {
throw new OzoneRestClientException(ex.getMessage(), ex);
} finally {
releaseConnection(httpGet);
}
}
/**
* List all the volumes owned by the user or Owned by the user specified in
* the behalf of string.
*
* @param onBehalfOf - User Name of the user if it is not the caller. for
* example, an admin wants to list some other users
* volumes.
* @param prefix - Return only volumes that match this prefix.
* @param maxKeys - Maximum number of results to return, if the result set
* is smaller than requested size, it means that list is
* complete.
* @param prevKey - The last key that client got, server will continue
* returning results from that point.
* @return List of Volumes
* @throws OzoneException
*/
public List<OzoneVolume> listVolumes(String onBehalfOf, String prefix,
int maxKeys, OzoneVolume prevKey) throws OzoneException {
String volumeName = null;
if (prevKey != null) {
volumeName = prevKey.getVolumeName();
}
return listVolumes(onBehalfOf, prefix, maxKeys, volumeName);
}
/**
* List volumes of the current user or if onBehalfof is not null lists volume
* owned by that user. You need admin privilege to read other users volume
* lists.
*
* @param onBehalfOf - Name of the user you want to get volume list
* @return - Volume list.
* @throws OzoneException
*/
public List<OzoneVolume> listVolumes(String onBehalfOf)
throws OzoneException {
return listVolumes(onBehalfOf, null,
Integer.parseInt(Header.OZONE_DEFAULT_LIST_SIZE), StringUtils.EMPTY);
}
/**
* List all volumes in a cluster. This can be invoked only by an Admin.
*
* @param prefix - Returns only volumes that match this prefix.
* @param maxKeys - Maximum niumber of keys to return
* @param prevKey - Last Ozone Volume from the last Iteration.
* @return List of Volumes
* @throws OzoneException
*/
public List<OzoneVolume> listAllVolumes(String prefix, int maxKeys,
OzoneVolume prevKey) throws OzoneException {
HttpGet httpGet = null;
try (CloseableHttpClient httpClient = newHttpClient()) {
URIBuilder builder = new URIBuilder(endPointURI);
if (prefix != null) {
builder.addParameter(Header.OZONE_LIST_QUERY_PREFIX, prefix);
}
if (maxKeys > 0) {
builder.addParameter(Header.OZONE_LIST_QUERY_MAXKEYS, Integer
.toString(maxKeys));
}
if (prevKey != null) {
builder.addParameter(Header.OZONE_LIST_QUERY_PREVKEY,
prevKey.getOwnerName()+ "/" + prevKey.getVolumeName());
}
builder.addParameter(Header.OZONE_LIST_QUERY_ROOTSCAN, "true");
builder.setPath("/").build();
httpGet = getHttpGet(builder.toString());
return executeListVolume(httpGet, httpClient);
} catch (IOException | URISyntaxException ex) {
throw new OzoneRestClientException(ex.getMessage(), ex);
} finally {
releaseConnection(httpGet);
}
}
/**
* delete a given volume.
*
* @param volumeName - volume to be deleted.
* @throws OzoneException - Ozone Exception
*/
public void deleteVolume(String volumeName) throws OzoneException {
HttpDelete httpDelete = null;
try (CloseableHttpClient httpClient = newHttpClient()) {
OzoneUtils.verifyResourceName(volumeName);
URIBuilder builder = new URIBuilder(endPointURI);
builder.setPath("/" + volumeName).build();
httpDelete = getHttpDelete(builder.toString());
executeDeleteVolume(httpDelete, httpClient);
} catch (IOException | URISyntaxException | IllegalArgumentException ex) {
throw new OzoneRestClientException(ex.getMessage(), ex);
} finally {
releaseConnection(httpDelete);
}
}
/**
* Sets the Volume Owner.
*
* @param volumeName - Volume Name
* @param newOwner - New Owner Name
* @throws OzoneException
*/
public void setVolumeOwner(String volumeName, String newOwner)
throws OzoneException {
HttpPut putRequest = null;
if (newOwner == null || newOwner.isEmpty()) {
throw new OzoneRestClientException("Invalid new owner name");
}
try (CloseableHttpClient httpClient = newHttpClient()) {
OzoneUtils.verifyResourceName(volumeName);
URIBuilder builder = new URIBuilder(endPointURI);
builder.setPath("/" + volumeName).build();
putRequest = getHttpPut(builder.toString());
putRequest.addHeader(Header.OZONE_USER, newOwner);
executePutVolume(putRequest, httpClient);
} catch (URISyntaxException | IllegalArgumentException | IOException ex) {
throw new OzoneRestClientException(ex.getMessage(), ex);
} finally {
releaseConnection(putRequest);
}
}
/**
* Sets the Volume Quota. Quota's are specified in a specific format. it is
* <integer>|(MB|GB|TB. for example 100TB.
* <p>
* To Remove a quota you can specify Header.OZONE_QUOTA_REMOVE
*
* @param volumeName - volume name
* @param quota - Quota String or Header.OZONE_QUOTA_REMOVE
* @throws OzoneException
*/
public void setVolumeQuota(String volumeName, String quota)
throws OzoneException {
if (quota == null || quota.isEmpty()) {
throw new OzoneRestClientException("Invalid quota");
}
HttpPut putRequest = null;
try (CloseableHttpClient httpClient = newHttpClient()) {
OzoneUtils.verifyResourceName(volumeName);
URIBuilder builder = new URIBuilder(endPointURI);
builder.setPath("/" + volumeName)
.setParameter(Header.OZONE_QUOTA_QUERY_TAG, quota)
.build();
putRequest = getHttpPut(builder.toString());
executePutVolume(putRequest, httpClient);
} catch (URISyntaxException | IllegalArgumentException | IOException ex) {
throw new OzoneRestClientException(ex.getMessage(), ex);
} finally {
releaseConnection(putRequest);
}
}
/**
* Sends the create Volume request to the server.
*
* @param httppost - http post class
* @param httpClient - httpClient
* @throws IOException -
* @throws OzoneException
*/
private void executeCreateVolume(HttpPost httppost,
final CloseableHttpClient httpClient)
throws IOException, OzoneException {
HttpEntity entity = null;
try {
HttpResponse response = httpClient.execute(httppost);
int errorCode = response.getStatusLine().getStatusCode();
entity = response.getEntity();
if ((errorCode == HTTP_OK) || (errorCode == HTTP_CREATED)) {
return;
}
if (entity != null) {
throw OzoneException.parse(EntityUtils.toString(entity));
} else {
throw new OzoneRestClientException("Unexpected null in http payload");
}
} finally {
if (entity != null) {
EntityUtils.consume(entity);
}
}
}
/**
* Sends the create Volume request to the server.
*
* @param httpGet - httpGet
* @return OzoneVolume
* @throws IOException -
* @throws OzoneException
*/
private OzoneVolume executeInfoVolume(HttpGet httpGet,
final CloseableHttpClient httpClient)
throws IOException, OzoneException {
HttpEntity entity = null;
try {
HttpResponse response = httpClient.execute(httpGet);
int errorCode = response.getStatusLine().getStatusCode();
entity = response.getEntity();
if (entity == null) {
throw new OzoneRestClientException("Unexpected null in http payload");
}
if (errorCode == HTTP_OK) {
OzoneVolume volume = new OzoneVolume(this);
volume.setVolumeInfo(EntityUtils.toString(entity));
return volume;
} else {
throw OzoneException.parse(EntityUtils.toString(entity));
}
} finally {
if (entity != null) {
EntityUtils.consumeQuietly(entity);
}
}
}
/**
* Sends update volume requests to the server.
*
* @param putRequest http request
* @throws IOException
* @throws OzoneException
*/
private void executePutVolume(HttpPut putRequest,
final CloseableHttpClient httpClient)
throws IOException, OzoneException {
HttpEntity entity = null;
try {
HttpResponse response = httpClient.execute(putRequest);
int errorCode = response.getStatusLine().getStatusCode();
entity = response.getEntity();
if (errorCode != HTTP_OK) {
throw OzoneException.parse(EntityUtils.toString(entity));
}
} finally {
if (entity != null) {
EntityUtils.consume(entity);
}
}
}
/**
* List Volumes.
*
* @param httpGet - httpGet
* @return OzoneVolume
* @throws IOException -
* @throws OzoneException
*/
private List<OzoneVolume> executeListVolume(HttpGet httpGet,
final CloseableHttpClient httpClient)
throws IOException, OzoneException {
HttpEntity entity = null;
List<OzoneVolume> volList = new LinkedList<>();
try {
HttpResponse response = httpClient.execute(httpGet);
int errorCode = response.getStatusLine().getStatusCode();
entity = response.getEntity();
if (entity == null) {
throw new OzoneRestClientException("Unexpected null in http payload");
}
String temp = EntityUtils.toString(entity);
if (errorCode == HTTP_OK) {
ListVolumes listVolumes =
ListVolumes.parse(temp);
for (VolumeInfo info : listVolumes.getVolumes()) {
volList.add(new OzoneVolume(info, this));
}
return volList;
} else {
throw OzoneException.parse(EntityUtils.toString(entity));
}
} finally {
if (entity != null) {
EntityUtils.consumeQuietly(entity);
}
}
}
/**
* Delete Volume.
*
* @param httpDelete - Http Delete Request
* @throws IOException
* @throws OzoneException
*/
private void executeDeleteVolume(HttpDelete httpDelete,
final CloseableHttpClient httpClient)
throws IOException, OzoneException {
HttpEntity entity = null;
try {
HttpResponse response = httpClient.execute(httpDelete);
int errorCode = response.getStatusLine().getStatusCode();
entity = response.getEntity();
if (errorCode != HTTP_OK) {
throw OzoneException.parse(EntityUtils.toString(entity));
}
} finally {
if (entity != null) {
EntityUtils.consumeQuietly(entity);
}
}
}
/**
* Puts a Key in Ozone Bucket.
*
* @param volumeName - Name of the Volume
* @param bucketName - Name of the Bucket
* @param keyName - Name of the Key
* @param file - Stream that gets read to be put into Ozone.
* @throws OzoneException
*/
public void putKey(String volumeName, String bucketName, String keyName,
File file) throws OzoneException {
OzoneUtils.verifyResourceName(volumeName);
OzoneUtils.verifyResourceName(bucketName);
if (StringUtils.isEmpty(keyName)) {
throw new OzoneRestClientException("Invalid key Name");
}
if (file == null) {
throw new OzoneRestClientException("Invalid data stream");
}
HttpPut putRequest = null;
FileInputStream fis = null;
try (CloseableHttpClient httpClient = HddsClientUtils.newHttpClient()) {
URIBuilder builder = new URIBuilder(getEndPointURI());
builder.setPath("/" + volumeName + "/" + bucketName + "/" + keyName)
.build();
putRequest = getHttpPut(builder.toString());
FileEntity fileEntity = new FileEntity(file, ContentType
.APPLICATION_OCTET_STREAM);
putRequest.setEntity(fileEntity);
fis = new FileInputStream(file);
putRequest.setHeader(Header.CONTENT_MD5, DigestUtils.md5Hex(fis));
OzoneBucket.executePutKey(putRequest, httpClient);
} catch (IOException | URISyntaxException ex) {
throw new OzoneRestClientException(ex.getMessage(), ex);
} finally {
IOUtils.closeStream(fis);
releaseConnection(putRequest);
}
}
/**
* Gets a key from the Ozone server and writes to the file pointed by the
* downloadTo Path.
*
* @param volumeName - Volume Name in Ozone.
* @param bucketName - Bucket Name in Ozone.
* @param keyName - Key Name in Ozone.
* @param downloadTo File Name to download the Key's Data to
*/
public void getKey(String volumeName, String bucketName, String keyName,
Path downloadTo) throws OzoneException {
OzoneUtils.verifyResourceName(volumeName);
OzoneUtils.verifyResourceName(bucketName);
if (StringUtils.isEmpty(keyName)) {
throw new OzoneRestClientException("Invalid key Name");
}
if (downloadTo == null) {
throw new OzoneRestClientException("Invalid download path");
}
FileOutputStream outPutFile = null;
HttpGet getRequest = null;
try (CloseableHttpClient httpClient = HddsClientUtils.newHttpClient()) {
outPutFile = new FileOutputStream(downloadTo.toFile());
URIBuilder builder = new URIBuilder(getEndPointURI());
builder.setPath("/" + volumeName + "/" + bucketName + "/" + keyName)
.build();
getRequest = getHttpGet(builder.toString());
OzoneBucket.executeGetKey(getRequest, httpClient, outPutFile);
outPutFile.flush();
} catch (IOException | URISyntaxException ex) {
throw new OzoneRestClientException(ex.getMessage(), ex);
} finally {
IOUtils.closeStream(outPutFile);
releaseConnection(getRequest);
}
}
/**
* List all keys in the given bucket.
*
* @param volumeName - Volume name
* @param bucketName - Bucket name
* @param resultLength The max length of listing result.
* @param previousKey The key from where listing should start,
* this key is excluded in the result.
* @param prefix The prefix that return list keys start with.
*
* @return List of OzoneKeys
*/
public List<OzoneKey> listKeys(String volumeName, String bucketName,
String resultLength, String previousKey, String prefix)
throws OzoneException {
OzoneUtils.verifyResourceName(volumeName);
OzoneUtils.verifyResourceName(bucketName);
HttpGet getRequest = null;
try (CloseableHttpClient httpClient = HddsClientUtils.newHttpClient()) {
URIBuilder builder = new URIBuilder(getEndPointURI());
builder.setPath("/" + volumeName + "/" + bucketName).build();
if (!Strings.isNullOrEmpty(resultLength)) {
builder.addParameter(Header.OZONE_LIST_QUERY_MAXKEYS, resultLength);
}
if (!Strings.isNullOrEmpty(previousKey)) {
builder.addParameter(Header.OZONE_LIST_QUERY_PREVKEY, previousKey);
}
if (!Strings.isNullOrEmpty(prefix)) {
builder.addParameter(Header.OZONE_LIST_QUERY_PREFIX, prefix);
}
getRequest = getHttpGet(builder.toString());
return OzoneBucket.executeListKeys(getRequest, httpClient);
} catch (IOException | URISyntaxException e) {
throw new OzoneRestClientException(e.getMessage(), e);
} finally {
releaseConnection(getRequest);
}
}
/**
* Returns a standard HttpPost Object to use for ozone post requests.
*
* @param onBehalfOf - If the use is being made on behalf of user, that user
* @param uriString - UriString
* @return HttpPost
*/
public HttpPost getHttpPost(String onBehalfOf, String uriString) {
HttpPost httpPost = new HttpPost(uriString);
addOzoneHeaders(httpPost);
if (onBehalfOf != null) {
httpPost.addHeader(Header.OZONE_USER, onBehalfOf);
}
return httpPost;
}
/**
* Returns a standard HttpGet Object to use for ozone Get requests.
*
* @param uriString - The full Uri String
* @return HttpGet
*/
public HttpGet getHttpGet(String uriString) {
HttpGet httpGet = new HttpGet(uriString);
addOzoneHeaders(httpGet);
return httpGet;
}
/**
* Returns httpDelete.
*
* @param uriString - uri
* @return HttpDelete
*/
public HttpDelete getHttpDelete(String uriString) {
HttpDelete httpDel = new HttpDelete(uriString);
addOzoneHeaders(httpDel);
return httpDel;
}
/**
* returns an HttpPut Object.
*
* @param uriString - Uri
* @return HttpPut
*/
public HttpPut getHttpPut(String uriString) {
HttpPut httpPut = new HttpPut(uriString);
addOzoneHeaders(httpPut);
return httpPut;
}
/**
* Add Ozone Headers.
*
* @param httpRequest - Http Request
*/
private void addOzoneHeaders(HttpRequestBase httpRequest) {
SimpleDateFormat format =
new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss ZZZ", Locale.US);
httpRequest.addHeader(Header.OZONE_VERSION_HEADER,
Header.OZONE_V1_VERSION_HEADER);
httpRequest.addHeader(HttpHeaders.DATE,
format.format(new Date(Time.monotonicNow())));
if (getUserAuth() != null) {
httpRequest.addHeader(HttpHeaders.AUTHORIZATION,
Header.OZONE_SIMPLE_AUTHENTICATION_SCHEME + " " +
getUserAuth());
}
}
/**
* Closes this stream and releases any system resources associated with it. If
* the stream is already closed then invoking this method has no effect.
*
* @throws IOException if an I/O error occurs
*/
@Override
public void close() throws IOException {
// TODO : Currently we create a new HTTP client. We should switch
// This to a Pool and cleanup the pool here.
}
@VisibleForTesting
public CloseableHttpClient newHttpClient() {
return HddsClientUtils.newHttpClient();
}
}