blob: a7f0f05e76280c7093913b1c512cac13ea8b45d3 [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 com.cloud.network.bigswitch;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Type;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpMethodBase;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.cookie.CookiePolicy;
import org.apache.commons.httpclient.methods.DeleteMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.PutMethod;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.apache.commons.httpclient.protocol.Protocol;
import org.apache.commons.httpclient.protocol.ProtocolSocketFactory;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
public class BigSwitchBcfApi {
private static final Logger S_LOGGER = Logger.getLogger(BigSwitchBcfApi.class);
private final static String S_PROTOCOL = "https";
private final static String S_NS_BASE_URL = "/networkService/v1.1";
private final static String CONTENT_TYPE = "content-type";
private final static String ACCEPT = "Accept";
private final static String CONTENT_JSON = "application/json";
private final static String HTTP_HEADER_INSTANCE_ID = "Instance-ID";
private final static String CLOUDSTACK_INSTANCE_ID = "cloudstack";
private final static String HASH_MATCH = "X-BSN-BVS-HASH-MATCH";
private final static MultiThreadedHttpConnectionManager S_HTTP_CLIENT_MANAGER = new MultiThreadedHttpConnectionManager();
private String host;
private String username;
private String password;
private String hash;
private String zoneId;
private Boolean nat;
private boolean isPrimary;
private int _port = 8000;
private HttpClient _client;
private Gson gson = new Gson();
public final static String HASH_CONFLICT = "HASH_CONFLICT";
public final static String HASH_IGNORE = "HASH_IGNORE";
/* This factory method is protected so we can extend this
* in the unittests.
*/
protected HttpClient createHttpClient() {
return new HttpClient(S_HTTP_CLIENT_MANAGER);
}
protected HttpMethod createMethod(final String type, final String uri, final int port) throws BigSwitchBcfApiException {
String url;
try {
url = new URL(S_PROTOCOL, host, port, uri).toString();
} catch (MalformedURLException e) {
S_LOGGER.error("Unable to build Big Switch API URL", e);
throw new BigSwitchBcfApiException("Unable to build Big Switch API URL", e);
}
if ("post".equalsIgnoreCase(type)) {
return new PostMethod(url);
} else if ("get".equalsIgnoreCase(type)) {
return new GetMethod(url);
} else if ("delete".equalsIgnoreCase(type)) {
return new DeleteMethod(url);
} else if ("put".equalsIgnoreCase(type)) {
return new PutMethod(url);
} else {
throw new BigSwitchBcfApiException("Requesting unknown method type");
}
}
public BigSwitchBcfApi() {
_client = createHttpClient();
_client.getParams().setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);
try {
// Cast to ProtocolSocketFactory to avoid the deprecated constructor with the SecureProtocolSocketFactory parameter
Protocol.registerProtocol("https", new Protocol("https", (ProtocolSocketFactory) new TrustingProtocolSocketFactory(), _port));
} catch (IOException e) {
S_LOGGER.warn("Failed to register the TrustingProtocolSocketFactory, falling back to default SSLSocketFactory", e);
}
}
/**
* Setter used by UI to set BSN controller address
* @param address
*/
public void setControllerAddress(final String address) {
this.host = address;
}
/**
* Setter used by UI to set BSN controller user name
* @param username
*/
public void setControllerUsername(final String username) {
this.username = username;
}
/**
* Setter used by UI to set BSN controller password
* @param password
*/
public void setControllerPassword(final String password) {
this.password = password;
}
/**
* Setter used by UI to set BSN controller NAT mode
* @param nat
*/
public void setControllerNat(final Boolean nat) {
this.nat = nat;
}
public boolean isNatEnabled() {
return this.nat;
}
public void setZoneId(final String zoneId) {
this.zoneId = zoneId;
}
public String createNetwork(final NetworkData network) throws BigSwitchBcfApiException {
String uri = S_NS_BASE_URL + "/tenants/" + network.getNetwork().getTenantId() + "/networks";
return executeCreateObject(network, uri, Collections.<String, String> emptyMap());
}
public String deleteNetwork(final String tenantId, final String networkId) throws BigSwitchBcfApiException {
String uri = S_NS_BASE_URL + "/tenants/" + tenantId + "/networks/" + networkId;
return executeDeleteObject(uri);
}
public String createAttachment(final String tenantId, final String networkId,
final AttachmentData attachment) throws BigSwitchBcfApiException {
String uri = S_NS_BASE_URL + "/tenants/" + tenantId + "/networks/" + networkId + "/ports/" + attachment.getAttachment().getId() + "/attachment";
return executeCreateObject(attachment, uri, Collections.<String, String> emptyMap());
}
public String modifyAttachment(final String tenantId, final String networkId,
final AttachmentData attachment) throws BigSwitchBcfApiException {
String uri = S_NS_BASE_URL + "/tenants/" + tenantId + "/networks/" + networkId + "/ports/" + attachment.getAttachment().getId() + "/attachment";
return executeUpdateObject(attachment, uri, Collections.<String, String> emptyMap());
}
public String deleteAttachment(final String tenantId, final String networkId,
final String attachmentId) throws BigSwitchBcfApiException {
String uri = S_NS_BASE_URL + "/tenants/" + tenantId + "/networks/" + networkId + "/ports/" + attachmentId + "/attachment";
return executeDeleteObject(uri);
}
public String createRouter(final String tenantId, final RouterData router) throws BigSwitchBcfApiException {
String uri = S_NS_BASE_URL + "/tenants/" + tenantId + "/routers";
return executeCreateObject(router, uri, Collections.<String, String> emptyMap());
}
public String modifyRouter(final String tenantId, final RouterData router) throws BigSwitchBcfApiException,
IllegalArgumentException{
String uri = S_NS_BASE_URL + "/tenants/" + tenantId + "/routers";
return executeCreateObject(router, uri, Collections.<String, String> emptyMap());
}
public String createRouterInterface(final String tenantId, final String routerId,
final RouterInterfaceData routerInterface) throws BigSwitchBcfApiException {
String uri = S_NS_BASE_URL + "/tenants/" + tenantId + "/routers/" + routerId + "/interfaces";
return executeCreateObject(routerInterface, uri, Collections.<String, String> emptyMap());
}
public String createFloatingIp(final String tenantId, final FloatingIpData fip) throws BigSwitchBcfApiException {
String uri = S_NS_BASE_URL + "/tenants/" + tenantId + "/floatingips";
return executeCreateObject(fip, uri, Collections.<String, String> emptyMap());
}
public String deleteFloatingIp(final String tenantId, final String fipId) throws BigSwitchBcfApiException {
String uri = S_NS_BASE_URL + "/tenants/" + tenantId + "/floatingips/" + fipId;
return executeDeleteObject(uri);
}
public ControlClusterStatus getControlClusterStatus() throws BigSwitchBcfApiException {
String uri = S_NS_BASE_URL + "/health";
ControlClusterStatus ccs = executeRetrieveObject(new TypeToken<ControlClusterStatus>() {
}.getType(), uri, null);
ccs.setStatus(true);
return ccs;
}
public Capabilities getCapabilities() throws BigSwitchBcfApiException {
String uri = S_NS_BASE_URL + "/capabilities";
List<String> capslist = executeRetrieveObject(new TypeToken<List<String>>() {
}.getType(), uri, null);
Capabilities caps = new Capabilities();
caps.setCapabilities(capslist);
return caps;
}
public String syncTopology(final TopologyData topo) throws BigSwitchBcfApiException {
String uri = S_NS_BASE_URL + "/topology";
return executeCreateObject(topo, uri, Collections.<String, String> emptyMap());
}
public ControllerData getControllerData() {
return new ControllerData(host, isPrimary);
}
private void checkInvariants() throws BigSwitchBcfApiException{
if (host == null || host.isEmpty()) {
throw new BigSwitchBcfApiException("Hostname is null or empty");
}
if (username == null || username.isEmpty()){
throw new BigSwitchBcfApiException("Username is null or empty");
}
if (password == null || password.isEmpty()){
throw new BigSwitchBcfApiException("Password is null or empty");
}
}
private String checkResponse(final HttpMethodBase m, final String errorMessageBase) throws BigSwitchBcfApiException,
IllegalArgumentException{
String customErrorMsg = null;
if (m.getStatusCode() == HttpStatus.SC_OK) {
String hash = "";
if (m.getResponseHeader(HASH_MATCH) != null) {
hash = m.getResponseHeader(HASH_MATCH).getValue();
set_hash(hash);
}
return hash;
}
if (m.getStatusCode() == HttpStatus.SC_CONFLICT) {
if(m instanceof GetMethod) {
return HASH_CONFLICT;
}
throw new BigSwitchBcfApiException("BCF topology sync required", true);
}
if (m.getStatusCode() == HttpStatus.SC_SEE_OTHER) {
isPrimary = false;
set_hash(HASH_IGNORE);
return HASH_IGNORE;
}
if (m.getStatusCode() == HttpStatus.SC_NOT_FOUND) {
if (m instanceof DeleteMethod){
return "";
}
}
if (m.getStatusCode() == HttpStatus.SC_BAD_REQUEST) {
customErrorMsg = " Invalid data in BCF request";
throw new IllegalArgumentException(customErrorMsg);
}
String errorMessage = responseToErrorMessage(m);
m.releaseConnection();
S_LOGGER.error(errorMessageBase + errorMessage);
throw new BigSwitchBcfApiException(errorMessageBase + errorMessage + customErrorMsg);
}
private void setHttpHeader(final HttpMethodBase m) {
m.setRequestHeader(CONTENT_TYPE, CONTENT_JSON);
m.setRequestHeader(ACCEPT, CONTENT_JSON);
m.setRequestHeader(HTTP_HEADER_INSTANCE_ID, CLOUDSTACK_INSTANCE_ID + "-" + zoneId);
if (StringUtils.isNotEmpty(hash)) {
m.setRequestHeader(HASH_MATCH, hash);
}
String authString = username + ":" + password;
String encodedAuthString = "Basic " + Base64.encodeBase64String(authString.getBytes(Charset.forName("UTF-8")));
m.setRequestHeader("Authorization", encodedAuthString);
}
protected <T> String executeUpdateObject(final T newObject, final String uri,
final Map<String, String> parameters) throws BigSwitchBcfApiException,
IllegalArgumentException{
checkInvariants();
PutMethod pm = (PutMethod)createMethod("put", uri, _port);
setHttpHeader(pm);
try {
pm.setRequestEntity(new StringRequestEntity(gson.toJson(newObject), CONTENT_JSON, null));
} catch (UnsupportedEncodingException e) {
throw new BigSwitchBcfApiException("Failed to encode json request body", e);
}
executeMethod(pm);
String hash = checkResponse(pm, "BigSwitch HTTP update failed: ");
pm.releaseConnection();
return hash;
}
protected <T> String executeCreateObject(final T newObject, final String uri,
final Map<String, String> parameters) throws BigSwitchBcfApiException {
checkInvariants();
PostMethod pm = (PostMethod)createMethod("post", uri, _port);
setHttpHeader(pm);
try {
pm.setRequestEntity(new StringRequestEntity(gson.toJson(newObject), CONTENT_JSON, null));
} catch (UnsupportedEncodingException e) {
throw new BigSwitchBcfApiException("Failed to encode json request body", e);
}
executeMethod(pm);
String hash = checkResponse(pm, "BigSwitch HTTP create failed: ");
pm.releaseConnection();
return hash;
}
protected String executeDeleteObject(final String uri) throws BigSwitchBcfApiException {
checkInvariants();
DeleteMethod dm = (DeleteMethod)createMethod("delete", uri, _port);
setHttpHeader(dm);
executeMethod(dm);
String hash = checkResponse(dm, "BigSwitch HTTP delete failed: ");
dm.releaseConnection();
return hash;
}
@SuppressWarnings("unchecked")
protected <T> T executeRetrieveObject(final Type returnObjectType,
final String uri, final Map<String, String> parameters) throws BigSwitchBcfApiException {
checkInvariants();
GetMethod gm = (GetMethod)createMethod("get", uri, _port);
setHttpHeader(gm);
if (parameters != null && !parameters.isEmpty()) {
List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(parameters.size());
for (Entry<String, String> e : parameters.entrySet()) {
nameValuePairs.add(new NameValuePair(e.getKey(), e.getValue()));
}
gm.setQueryString(nameValuePairs.toArray(new NameValuePair[0]));
}
executeMethod(gm);
String hash = checkResponse(gm, "BigSwitch HTTP get failed: ");
T returnValue;
try {
// CAUTIOUS: Safety margin of 2048 characters - extend if needed.
returnValue = (T)gson.fromJson(gm.getResponseBodyAsString(2048), returnObjectType);
} catch (IOException e) {
S_LOGGER.error("IOException while retrieving response body", e);
throw new BigSwitchBcfApiException(e);
} finally {
gm.releaseConnection();
}
if(returnValue instanceof ControlClusterStatus) {
if(HASH_CONFLICT.equals(hash)) {
isPrimary = true;
((ControlClusterStatus) returnValue).setTopologySyncRequested(true);
} else if (!HASH_IGNORE.equals(hash) && !isPrimary) {
isPrimary = true;
((ControlClusterStatus) returnValue).setTopologySyncRequested(true);
}
}
return returnValue;
}
protected void executeMethod(final HttpMethodBase method) throws BigSwitchBcfApiException {
try {
_client.executeMethod(method);
if (method.getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
method.releaseConnection();
}
} catch (HttpException e) {
S_LOGGER.error("HttpException caught while trying to connect to the BigSwitch Controller", e);
method.releaseConnection();
throw new BigSwitchBcfApiException("API call to BigSwitch Controller Failed", e);
} catch (IOException e) {
S_LOGGER.error("IOException caught while trying to connect to the BigSwitch Controller", e);
method.releaseConnection();
throw new BigSwitchBcfApiException("API call to BigSwitch Controller Failed", e);
}
}
private String responseToErrorMessage(final HttpMethodBase method) {
assert method.isRequestSent() : "no use getting an error message unless the request is sent";
if ("text/html".equals(method.getResponseHeader(CONTENT_TYPE).getValue())) {
// The error message is the response content
// Safety margin of 2048 characters, anything longer is probably useless
// and will clutter the logs
try {
return method.getResponseBodyAsString(2048);
} catch (IOException e) {
S_LOGGER.debug("Error while loading response body", e);
}
}
// The default
return method.getStatusText();
}
public static String getCloudstackInstanceId() {
return CLOUDSTACK_INSTANCE_ID;
}
public String get_hash() {
return hash;
}
public void set_hash(final String hash) {
this.hash = hash;
}
}