/* * Copyright (C) 2012 Atomes SARL * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * */ package org.jgroups.protocols; import org.jgroups.Address; import org.jgroups.annotations.Experimental; import org.jgroups.annotations.Property; import org.jgroups.logging.Log; import org.jgroups.logging.LogFactory; import org.jgroups.util.Util; import org.w3c.dom.Document; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.xpath.*; import java.io.*; import java.net.HttpURLConnection; import java.net.ProtocolException; import java.net.URL; import java.util.*; /** * Discovery protocol based on Openstack Swift (object storage). * <p/> * This implementation is derived from Gustavo Fernandes work on RACKSPACE_PING * * @author tsegismont * @since 3.1 */ @Experimental public class SWIFT_PING extends FILE_PING { private static final Log log = LogFactory.getLog(SWIFT_PING.class); protected SwiftClient swiftClient = null; @Property(description = "Authentication url") protected String auth_url = null; @Property(description = "Authentication type") protected String auth_type = "keystone_v_2_0"; @Property(description = "Openstack Keystone tenant name") protected String tenant = null; @Property(description = "Username") protected String username = null; @Property(description = "Password") protected String password = null; @Property(description = "Name of the root container") protected String container = "jgroups"; @Override public void init() throws Exception { Utils.validateNotEmpty(auth_url, "auth_url"); Utils.validateNotEmpty(auth_type, "auth_type"); Utils.validateNotEmpty(username, "username"); Utils.validateNotEmpty(password, "password"); Utils.validateNotEmpty(container, "container"); Authenticator authenticator = createAuthenticator(); authenticator.validateParams(); swiftClient = new SwiftClient(authenticator); // Authenticate now to record credential swiftClient.authenticate(); Runtime.getRuntime().addShutdownHook(new Thread() { public void run() { remove(group_addr, local_addr); } }); super.init(); } private Authenticator createAuthenticator() throws Exception { AUTH_TYPE authType = AUTH_TYPE.getByConfigName(auth_type); if (authType == null) { throw new IllegalArgumentException("Invalid 'auth_type' : " + auth_type); } URL authUrl = new URL(auth_url); Authenticator authenticator = null; switch (authType) { case KEYSTONE_V_2_0: authenticator = new Keystone_V_2_0_Auth(tenant, authUrl, username, password); break; default: // We shouldn't come here since we checked auth_type throw new IllegalStateException("Could not select authenticator"); } return authenticator; } @Override protected void remove(String clustername, Address addr) { String fileName = clustername + "/" + addressAsString(addr); try { swiftClient.deleteObject(container, fileName); } catch (Exception e) { log.error("failure removing data", e); } } @Override protected List<PingData> readAll(String clustername) { List<PingData> pingDataList = new ArrayList<PingData>(); try { List<String> objects = swiftClient.listObjects(container); for (String object : objects) { byte[] bytes = swiftClient.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); swiftClient.createObject(container, filename, Util.objectToByteBuffer(data)); } catch (Exception e) { log.error("Error marshalling object", e); } } @Override protected void createRootDir() { try { swiftClient.createContainer(container); } catch (Exception e) { log.error("failure creating container", e); } } private static class HttpHeaders { private static final String CONTENT_TYPE_HEADER = "Content-type"; 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"; } /** * Supported Swift authentication providers */ private static enum AUTH_TYPE { KEYSTONE_V_2_0("keystone_v_2_0"); private static final Map<String, AUTH_TYPE> LOOKUP = new HashMap<String, AUTH_TYPE>(); static { for (AUTH_TYPE type : EnumSet.allOf(AUTH_TYPE.class)) LOOKUP.put(type.configName, type); } private String configName; private AUTH_TYPE(String externalName) { this.configName = externalName; } public static AUTH_TYPE getByConfigName(String configName) { return LOOKUP.get(configName); } } /** * Result of a successfully authenticated session */ private static class Credentials { private final String authToken; private final String storageUrl; public Credentials(String authToken, String storageUrl) { this.authToken = authToken; this.storageUrl = storageUrl; } } /** * Contract for Swift authentication providers */ private static interface Authenticator { /** * Validate SWIFT_PING config parameters */ void validateParams(); Credentials authenticate() throws Exception; } /** * Openstack Keytsone v2.0 authentication provider. Thread safe * implementation */ private static class Keystone_V_2_0_Auth implements Authenticator { private static XPathExpression tokenIdExpression; private static XPathExpression publicUrlExpression; static { XPathFactory xPathFactory = XPathFactory.newInstance(); XPath xpath = xPathFactory.newXPath(); try { tokenIdExpression = xpath.compile("/access/token/@id"); publicUrlExpression = xpath .compile("/access/serviceCatalog/service[@type='object-store']/endpoint/@publicURL"); } catch (XPathExpressionException e) { // Do nothing } } private String tenant; private URL authUrl; private String username; private String password; public Keystone_V_2_0_Auth(String tenant, URL authUrl, String username, String password) { this.tenant = tenant; this.authUrl = authUrl; this.username = username; this.password = password; } public void validateParams() { // All others params already validated Utils.validateNotEmpty(tenant, "tenant"); } public Credentials authenticate() throws Exception { HttpURLConnection urlConnection = new ConnBuilder(authUrl) .addHeader(HttpHeaders.CONTENT_TYPE_HEADER, "application/json") .addHeader(HttpHeaders.ACCEPT_HEADER, "application/xml") .getConnection(); StringBuilder jsonBuilder = new StringBuilder(); jsonBuilder.append("{\"auth\": {\"tenantName\": \"").append(tenant) .append("\", \"passwordCredentials\": {\"username\": \"") .append(username).append("\", \"password\": \"") .append(password).append("\"}}}"); HttpResponse response = Utils.doOperation(urlConnection, jsonBuilder.toString().getBytes(), true); if (response.isSuccessCode()) { DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory .newInstance(); DocumentBuilder builder = documentBuilderFactory .newDocumentBuilder(); Document doc = builder.parse(new ByteArrayInputStream( response.payload)); String authToken = (String) tokenIdExpression.evaluate(doc, XPathConstants.STRING); String storageUrl = (String) publicUrlExpression.evaluate(doc, XPathConstants.STRING); log.trace("Authentication successful"); return new Credentials(authToken, storageUrl); } else { throw new IllegalStateException( "Error authenticating to the service. Please check your credentials. Code = " + response.code); } } } /** * Build HttpURLConnections with adequate headers and method */ private static 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(); } 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; } } /** * Response for a Swift API call */ private static class HttpResponse { // For later use private final Map<String, List<String>> headers; private final int code; private final byte[] payload; HttpResponse(Map<String, List<String>> headers, int code, byte[] payload) { this.headers = headers; this.code = code; this.payload = payload; } 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; } public boolean isSuccessCode() { return Utils.isSuccessCode(code); } public boolean isAuthDenied() { return Utils.isAuthDenied(code); } } /** * A thread safe Swift client */ protected static class SwiftClient { private Authenticator authenticator; private volatile Credentials credentials = null; /** * Constructor * * @param authenticator Swift auth provider */ public SwiftClient(Authenticator authenticator) { this.authenticator = authenticator; } /** * Authenticate * * @throws Exception */ public void authenticate() throws Exception { credentials = authenticator.authenticate(); } /** * Delete a object (=file) from the storage * * @param containerName Folder name * @param objectName File name * @throws IOException */ public void deleteObject(String containerName, String objectName) throws Exception { HttpURLConnection urlConnection = getConnBuilder(containerName, objectName).method("DELETE").getConnection(); HttpResponse response = Utils.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 * @throws IOException */ public void createContainer(String containerName) throws Exception { HttpURLConnection urlConnection = getConnBuilder(containerName, null).method("PUT").getConnection(); HttpResponse response = Utils.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 * @throws IOException */ public void createObject(String containerName, String objectName, byte[] contents) throws Exception { HttpURLConnection conn = getConnBuilder(containerName, objectName) .method("PUT") .addHeader(HttpHeaders.CONTENT_LENGTH_HEADER, String.valueOf(contents.length)).getConnection(); HttpResponse response = Utils.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 * @throws IOException */ public byte[] readObject(String containerName, String objectName) throws Exception { HttpURLConnection urlConnection = getConnBuilder(containerName, objectName).getConnection(); HttpResponse response = Utils.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 * @throws IOException */ public List<String> listObjects(String containerName) throws Exception { HttpURLConnection urlConnection = getConnBuilder(containerName, null).getConnection(); HttpResponse response = Utils.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(); } private ConnBuilder getConnBuilder(String container, String object) { ConnBuilder connBuilder = new ConnBuilder(credentials, container, object); connBuilder.addHeader(HttpHeaders.STORAGE_TOKEN_HEADER, credentials.authToken); connBuilder.addHeader(HttpHeaders.ACCEPT_HEADER, "*/*"); return connBuilder; } } private static class Utils { public static void validateNotEmpty(String arg, String argname) { if (arg == null || arg.trim().length() == 0) { throw new IllegalArgumentException("'" + argname + "' cannot be empty"); } } /** * Is http response code in success range ? * * @param code * @return */ public static boolean isSuccessCode(int code) { return code >= 200 && code < 300; } /** * Is http Unauthorized response code ? * * @param code * @return */ public static boolean isAuthDenied(int code) { return code == 401; } /** * 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 * @throws IOException */ public static HttpResponse doOperation(HttpURLConnection urlConnection, byte[] inputData, boolean hasOutput) throws IOException { HttpResponse response = null; InputStream inputStream = null; OutputStream outputStream = null; byte[] payload = null; try { if (inputData != null) { urlConnection.setDoOutput(true); outputStream = urlConnection.getOutputStream(); outputStream.write(inputData); } /* * Get response code first. HttpURLConnection does not allow to * read inputstream if response code is not success code */ int responseCode = urlConnection.getResponseCode(); if (hasOutput && isSuccessCode(responseCode)) { payload = getBytes(urlConnection.getInputStream()); } response = new HttpResponse(urlConnection.getHeaderFields(), responseCode, payload); } finally { Util.close(inputStream); Util.close(outputStream); } return response; } /** * Get bytes of this {@link InputStream} * * @param inputStream * @return * @throws IOException */ public static byte[] getBytes(InputStream inputStream) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buffer = new byte[4096]; int len; for (; ; ) { len = inputStream.read(buffer); if (len == -1) { break; } baos.write(buffer, 0, len); } return baos.toByteArray(); } /** * Do a operation that does not write or read from HttpURLConnection, * except for the headers * * @param urlConnection the connection * @return Response * @throws IOException */ public static HttpResponse doVoidOperation( HttpURLConnection urlConnection) throws IOException { 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 * @throws IOException */ public static HttpResponse doSendOperation( HttpURLConnection urlConnection, byte[] content) throws IOException { return doOperation(urlConnection, content, false); } /** * Do a operation that reads from the httpconnection * * @param urlConnection The connections * @return Response * @throws IOException */ public static HttpResponse doReadOperation( HttpURLConnection urlConnection) throws IOException { return doOperation(urlConnection, null, true); } } }