package org.jgroups.protocols; import org.jgroups.Address; import org.jgroups.annotations.Property; import org.jgroups.logging.Log; import org.jgroups.logging.LogFactory; import org.jgroups.util.Util; import java.io.*; import java.net.HttpURLConnection; import java.net.ProtocolException; import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * Discovery protocol based on Rackspace Cloud Files storage solution * * @author Gustavo Fernandes */ public class RACKSPACE_PING extends FILE_PING { protected static final Log log = LogFactory.getLog(RACKSPACE_PING.class); private static final String UKService = "https://lon.auth.api.rackspacecloud.com/v1.0"; private static final String USService = "https://auth.api.rackspacecloud.com/v1.0"; protected RackspaceClient rackspaceClient = null; @Property(description = "Rackspace username") protected String username = null; @Property(description = "Rackspace API access key") protected String apiKey = null; @Property(description = "Rackspace region, either UK or US") protected String region = null; @Property(description = "Name of the root container") protected String container = "jgroups"; @Override public void init() throws Exception { if (username == null) { throw new IllegalArgumentException("Rackspace 'username' must not be null"); } if (apiKey == null) { throw new IllegalArgumentException("Rackspace 'apiKey' must not be null"); } if (region == null || (!region.equals("UK") && !region.equals("US"))) { throw new IllegalArgumentException("Invalid 'region', must be UK or US"); } URL authURL = new URL(region.equals("UK") ? UKService : USService); rackspaceClient = new RackspaceClient(authURL, username, apiKey); Runtime.getRuntime().addShutdownHook(new Thread() { public void run() { remove(group_addr, local_addr); } }); super.init(); } @Override protected void remove(String clustername, Address addr) { String fileName = clustername + "/" + addressAsString(addr); rackspaceClient.deleteObject(container, fileName); } @Override protected List<PingData> readAll(String clustername) { List<PingData> pingDataList = new ArrayList<PingData>(); try { List<String> objects = rackspaceClient.listObjects(container); for (String object : objects) { byte[] bytes = rackspaceClient.readObject(container, object); PingData pingData = (PingData) Util.objectFromByteBuffer(bytes); pingDataList.add(pingData); } } catch (Exception e) { log.error("Error unmarhsalling object", e); } return pingDataList; } @Override protected void writeToFile(PingData data, String clustername) { try { String filename = clustername + "/" + addressAsString(local_addr); rackspaceClient.createObject(container, filename, Util.objectToByteBuffer(data)); } catch (Exception e) { log.error("Error marshalling object", e); } } @Override protected void createRootDir() { rackspaceClient.authenticate(); rackspaceClient.createContainer(container); } /** * A thread safe Rackspace ReST client */ static class RackspaceClient { private static final String ACCEPT_HEADER = "Accept"; private static final String AUTH_HEADER = "X-Auth-User"; private static final String AUTH_KEY_HEADER = "X-Auth-Key"; private static final String STORAGE_TOKEN_HEADER = "X-Storage-Token"; private static final String STORAGE_URL_HEADER = "X-Storage-Url"; private static final String CONTENT_LENGTH_HEADER = "Content-Length"; private final URL apiEndpoint; private final String username; private final String apiKey; private volatile Credentials credentials = null; /** * Constructor * * @param apiEndpoint UK or US authentication endpoint * @param username Rackspace username * @param apiKey Rackspace apiKey */ public RackspaceClient(URL apiEndpoint, String username, String apiKey) { this.apiEndpoint = apiEndpoint; this.username = username; this.apiKey = apiKey; } /** * Authenticate */ public void authenticate() { HttpURLConnection urlConnection = new ConnBuilder(apiEndpoint) .addHeader(AUTH_HEADER, username) .addHeader(AUTH_KEY_HEADER, apiKey) .getConnection(); Response response = doAuthOperation(urlConnection); if (response.isSuccessCode()) { credentials = new Credentials( response.getHeader(STORAGE_TOKEN_HEADER), response.getHeader(STORAGE_URL_HEADER) ); log.trace("Authentication successful"); } else { throw new IllegalStateException("Error authenticating to the service. Please check your credentials. Code = " + response.code); } } /** * Delete a object (=file) from the storage * * @param containerName Folder name * @param objectName File name */ public void deleteObject(String containerName, String objectName) { HttpURLConnection urlConnection = new ConnBuilder(credentials, containerName, objectName) .method("DELETE") .getConnection(); Response response = doVoidOperation(urlConnection); if (!response.isSuccessCode()) { if (response.isAuthDenied()) { log.warn("Refreshing credentials and retrying"); authenticate(); deleteObject(containerName, objectName); } else { log.error("Error deleting object " + objectName + " from container " + containerName + ",code = " + response.code); } } } /** * Create a container, which is equivalent to a bucket * * @param containerName Name of the container */ public void createContainer(String containerName) { HttpURLConnection urlConnection = new ConnBuilder(credentials, containerName, null) .method("PUT") .getConnection(); Response response = doVoidOperation(urlConnection); if (!response.isSuccessCode()) { if (response.isAuthDenied()) { log.warn("Refreshing credentials and retrying"); authenticate(); createContainer(containerName); } else { log.error("Error creating container " + containerName + " ,code = " + response.code); } } } /** * Create an object (=file) * * @param containerName Name of the container * @param objectName Name of the file * @param contents Binary content of the file */ public void createObject(String containerName, String objectName, byte[] contents) { HttpURLConnection conn = new ConnBuilder(credentials, containerName, objectName) .method("PUT") .addHeader(CONTENT_LENGTH_HEADER, String.valueOf(contents.length)) .getConnection(); Response response = doSendOperation(conn, contents); if (!response.isSuccessCode()) { if (response.isAuthDenied()) { log.warn("Refreshing credentials and retrying"); authenticate(); createObject(containerName, objectName, contents); } else { log.error("Error creating object " + objectName + " in container " + containerName + ",code = " + response.code); } } } /** * Read the content of a file * * @param containerName Name of the folder * @param objectName name of the file * @return Content of the files */ public byte[] readObject(String containerName, String objectName) { HttpURLConnection urlConnection = new ConnBuilder(credentials, containerName, objectName).getConnection(); Response response = doReadOperation(urlConnection); if (!response.isSuccessCode()) { if (response.isAuthDenied()) { log.warn("Refreshing credentials and retrying"); authenticate(); return readObject(containerName, objectName); } else { log.error("Error reading object " + objectName + " from container " + containerName + ", code = " + response.code); } } return response.payload; } /** * List files in a folder * * @param containerName Folder name * @return List of file names */ public List<String> listObjects(String containerName) { HttpURLConnection urlConnection = new ConnBuilder(credentials, containerName, null).getConnection(); Response response = doReadOperation(urlConnection); if (!response.isSuccessCode()) { if (response.isAuthDenied()) { log.warn("Refreshing credentials and retrying"); authenticate(); return listObjects(containerName); } else { log.error("Error listing container " + containerName + ", code = " + response.code); } } return response.payloadAsLines(); } /** * Do a http operation * * @param urlConnection the HttpURLConnection to be used * @param inputData if not null,will be written to the urlconnection. * @param hasOutput if true, read content back from the urlconnection * @return Response */ private Response doOperation(HttpURLConnection urlConnection, byte[] inputData, boolean hasOutput) { Response response = null; InputStream inputStream = null; OutputStream outputStream = null; byte[] payload = null; try { if (inputData != null) { urlConnection.setDoOutput(true); outputStream = urlConnection.getOutputStream(); outputStream.write(inputData); } if (hasOutput) { inputStream = urlConnection.getInputStream(); payload = Util.readFileContents(urlConnection.getInputStream()); } response = new Response(urlConnection.getHeaderFields(), urlConnection.getResponseCode(), payload); } catch (IOException e) { log.error("Error calling service", e); } finally { Util.close(inputStream); Util.close(outputStream); } return response; } /** * Do a http auth operation, will not handle 401 permission denied errors * * @param urlConnection the HttpURLConnection to be used * @return Response Response */ private Response doAuthOperation(HttpURLConnection urlConnection) { return doOperation(urlConnection, null, false); } /** * Do a operation that does not write or read from HttpURLConnection, except for the headers * * @param urlConnection the connection * @return Response */ private Response doVoidOperation(HttpURLConnection urlConnection) { return doOperation(urlConnection, null, false); } /** * Do a operation that writes content to the HttpURLConnection * * @param urlConnection the connection * @param content The content to send * @return Response */ private Response doSendOperation(HttpURLConnection urlConnection, byte[] content) { return doOperation(urlConnection, content, false); } /** * Do a operation that reads from the httpconnection * * @param urlConnection The connections * @return Response */ private Response doReadOperation(HttpURLConnection urlConnection) { return doOperation(urlConnection, null, true); } /** * Build HttpURLConnections with adequate headers and method */ private class ConnBuilder { private HttpURLConnection con; public ConnBuilder(URL url) { try { con = (HttpURLConnection) url.openConnection(); } catch (IOException e) { log.error("Error building URL", e); } } public ConnBuilder(Credentials credentials, String container, String object) { try { String url = credentials.storageURL + "/" + container; if (object != null) { url = url + "/" + object; } con = (HttpURLConnection) new URL(url).openConnection(); con.addRequestProperty(STORAGE_TOKEN_HEADER, credentials.authToken); con.addRequestProperty(ACCEPT_HEADER, "*/*"); } catch (IOException e) { log.error("Error creating connection", e); } } public ConnBuilder method(String method) { try { con.setRequestMethod(method); } catch (ProtocolException e) { log.error("Protocol error", e); } return this; } public ConnBuilder addHeader(String key, String value) { con.setRequestProperty(key, value); return this; } public HttpURLConnection getConnection() { return con; } } /** * Result of an successfully authenticated session */ private class Credentials { private final String authToken; private final String storageURL; public Credentials(String authToken, String storageURL) { this.authToken = authToken; this.storageURL = storageURL; } } /** * Response for a Rackspace API call */ private class Response { private final Map<String, List<String>> headers; private final int code; private final byte[] payload; Response(Map<String, List<String>> headers, int code, byte[] payload) { this.headers = headers; this.code = code; this.payload = payload; } private String getHeader(String name) { return headers.get(name).get(0); } public boolean isSuccessCode() { return code >= 200 && code < 300; } public boolean isAuthDenied() { return code == 401; } public List<String> payloadAsLines() { List<String> lines = new ArrayList<String>(); BufferedReader in; try { String line; in = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(payload))); while ((line = in.readLine()) != null) { lines.add(line); } in.close(); } catch (IOException e) { log.error("Error reading objects", e); } return lines; } } } }