/** * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. * * This software is licensed under the GNU General Public License v3 or later. * * It is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or any later version. * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package com.cloud.api; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.log4j.Logger; import com.cloud.async.AsyncJobResult; import com.cloud.async.AsyncJobVO; import com.cloud.serializer.SerializerHelper; import com.cloud.server.ManagementServer; import com.cloud.user.Account; import com.cloud.utils.Pair; public abstract class BaseCmd { private static final Logger s_logger = Logger.getLogger(BaseCmd.class.getName()); public static final int PROGRESS_INSTANCE_CREATED = 1; public static final String RESPONSE_TYPE_XML = "xml"; public static final String RESPONSE_TYPE_JSON = "json"; public enum CommandType { BOOLEAN, DATE, FLOAT, INTEGER, LIST, LONG, OBJECT, MAP, STRING, TZDATE } public enum Manager { ConfigManager, ManagementServer, NetworkGroupManager, NetworkManager, StorageManager, UserVmManager } // FIXME: Extract these out into a separate file // Client error codes public static final int MALFORMED_PARAMETER_ERROR = 430; public static final int VM_INVALID_PARAM_ERROR = 431; public static final int NET_INVALID_PARAM_ERROR = 432; public static final int VM_ALLOCATION_ERROR = 433; public static final int IP_ALLOCATION_ERROR = 434; public static final int SNAPSHOT_INVALID_PARAM_ERROR = 435; public static final int PARAM_ERROR = 436; // Server error codes public static final int INTERNAL_ERROR = 530; public static final int ACCOUNT_ERROR = 531; public static final int UNSUPPORTED_ACTION_ERROR = 532; public static final int VM_DEPLOY_ERROR = 540; public static final int VM_DESTROY_ERROR = 541; public static final int VM_REBOOT_ERROR = 542; public static final int VM_START_ERROR = 543; public static final int VM_STOP_ERROR = 544; public static final int VM_RESET_PASSWORD_ERROR = 545; public static final int VM_CHANGE_SERVICE_ERROR = 546; public static final int VM_LIST_ERROR = 547; public static final int VM_RECOVER_ERROR = 548; public static final int SNAPSHOT_LIST_ERROR = 549; public static final int CREATE_VOLUME_FROM_SNAPSHOT_ERROR = 550; public static final int VM_INSUFFICIENT_CAPACITY = 551; public static final int CREATE_PRIVATE_TEMPLATE_ERROR = 552; public static final int VM_HOST_LICENSE_EXPIRED = 553; public static final int NET_IP_ASSOC_ERROR = 560; public static final int NET_IP_DIASSOC_ERROR = 561; public static final int NET_CREATE_IPFW_RULE_ERROR = 562; public static final int NET_DELETE_IPFW_RULE_ERROR = 563; public static final int NET_CONFLICT_IPFW_RULE_ERROR = 564; public static final int NET_CREATE_LB_RULE_ERROR = 566; public static final int NET_DELETE_LB_RULE_ERROR = 567; public static final int NET_CONFLICT_LB_RULE_ERROR = 568; public static final int NET_LIST_ERROR = 570; public static final int STORAGE_RESOURCE_IN_USE = 580; public static final DateFormat INPUT_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); private static final DateFormat _outputFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); private Object _responseObject = null; public abstract String getName(); public abstract String getResponse(); public Object getResponseObject() { return _responseObject; } public void setResponseObject(Object responseObject) { _responseObject = responseObject; } public String getDateString(Date date) { if (date == null) { return ""; } String formattedString = null; synchronized(_outputFormat) { formattedString = _outputFormat.format(date); } return formattedString; } public Map validateParams(Map params, boolean decode) { // List> properties = getProperties(); // step 1 - all parameter names passed in will be converted to lowercase Map processedParams = lowercaseParams(params, decode); return processedParams; /* // step 2 - make sure all required params exist, and all existing params adhere to the appropriate data type Map validatedParams = new HashMap(); for (Pair propertyPair : properties) { Properties prop = (Properties)propertyPair.first(); Object param = processedParams.get(prop.getName()); // possible validation errors are // - NULL (not specified) // - MALFORMED if (param != null) { short propertyType = prop.getDataType(); String decodedParam = null; if ((propertyType != TYPE_OBJECT) && (propertyType != TYPE_OBJECT_MAP)) { decodedParam = (String)param; if (decode) { try { decodedParam = URLDecoder.decode((String)param, "UTF-8"); } catch (UnsupportedEncodingException usex) { s_logger.warn(prop.getName() + " could not be decoded, value = " + param); throw new ServerApiException(PARAM_ERROR, prop.getName() + " could not be decoded"); } } } switch (propertyType) { case TYPE_INT: try { validatedParams.put(prop.getName(), Integer.valueOf(Integer.parseInt(decodedParam))); } catch (NumberFormatException ex) { s_logger.warn(prop.getName() + " (type is int) is malformed, value = " + decodedParam); throw new ServerApiException(MALFORMED_PARAMETER_ERROR, prop.getName() + " is malformed"); } break; case TYPE_LONG: try { validatedParams.put(prop.getName(), Long.valueOf(Long.parseLong(decodedParam))); } catch (NumberFormatException ex) { s_logger.warn(prop.getName() + " (type is long) is malformed, value = " + decodedParam); throw new ServerApiException(MALFORMED_PARAMETER_ERROR, prop.getName() + " is malformed"); } break; case TYPE_DATE: try { synchronized(_format) { // SimpleDataFormat is not thread safe, synchronize on it to avoid parse errors validatedParams.put(prop.getName(), _format.parse(decodedParam)); } } catch (ParseException ex) { s_logger.warn(prop.getName() + " (type is date) is malformed, value = " + decodedParam); throw new ServerApiException(MALFORMED_PARAMETER_ERROR, prop.getName() + " uses an unsupported date format"); } break; case TYPE_TZDATE: try { validatedParams.put(prop.getName(), DateUtil.parseTZDateString(decodedParam)); } catch (ParseException ex) { s_logger.warn(prop.getName() + " (type is date) is malformed, value = " + decodedParam); throw new ServerApiException(MALFORMED_PARAMETER_ERROR, prop.getName() + " uses an unsupported date format"); } break; case TYPE_FLOAT: try { validatedParams.put(prop.getName(), Float.valueOf(Float.parseFloat(decodedParam))); } catch (NumberFormatException ex) { s_logger.warn(prop.getName() + " (type is float) is malformed, value = " + decodedParam); throw new ServerApiException(MALFORMED_PARAMETER_ERROR, prop.getName() + " is malformed"); } break; case TYPE_BOOLEAN: validatedParams.put(prop.getName(), Boolean.valueOf(Boolean.parseBoolean(decodedParam))); break; case TYPE_STRING: validatedParams.put(prop.getName(), decodedParam); break; default: validatedParams.put(prop.getName(), param); break; } } else if (propertyPair.second().booleanValue() == true) { s_logger.warn("missing parameter, " + prop.getTagName() + " is not specified"); throw new ServerApiException(MALFORMED_PARAMETER_ERROR, prop.getTagName() + " is not specified"); } } return validatedParams; */ } private Map lowercaseParams(Map params, boolean decode) { Map lowercaseParams = new HashMap(); for (String key : params.keySet()) { lowercaseParams.put(key.toLowerCase(), params.get(key)); } return lowercaseParams; } // FIXME: move this to a utils method so that maps can be unpacked and integer/long values can be appropriately cast @SuppressWarnings("unchecked") public Map unpackParams(Map params) { Map lowercaseParams = new HashMap(); for (String key : params.keySet()) { int arrayStartIndex = key.indexOf('['); int arrayStartLastIndex = key.lastIndexOf('['); if (arrayStartIndex != arrayStartLastIndex) { throw new ServerApiException(MALFORMED_PARAMETER_ERROR, "Unable to decode parameter " + key + "; if specifying an object array, please use parameter[index].field=XXX, e.g. userGroupList[0].group=httpGroup"); } if (arrayStartIndex > 0) { int arrayEndIndex = key.indexOf(']'); int arrayEndLastIndex = key.lastIndexOf(']'); if ((arrayEndIndex < arrayStartIndex) || (arrayEndIndex != arrayEndLastIndex)) { // malformed parameter throw new ServerApiException(MALFORMED_PARAMETER_ERROR, "Unable to decode parameter " + key + "; if specifying an object array, please use parameter[index].field=XXX, e.g. userGroupList[0].group=httpGroup"); } // Now that we have an array object, check for a field name in the case of a complex object int fieldIndex = key.indexOf('.'); String fieldName = null; if (fieldIndex < arrayEndIndex) { throw new ServerApiException(MALFORMED_PARAMETER_ERROR, "Unable to decode parameter " + key + "; if specifying an object array, please use parameter[index].field=XXX, e.g. userGroupList[0].group=httpGroup"); } else { fieldName = key.substring(fieldIndex + 1); } // parse the parameter name as the text before the first '[' character String paramName = key.substring(0, arrayStartIndex); paramName = paramName.toLowerCase(); Map mapArray = null; Map mapValue = null; String indexStr = key.substring(arrayStartIndex+1, arrayEndIndex); int index = 0; boolean parsedIndex = false; try { if (indexStr != null) { index = Integer.parseInt(indexStr); parsedIndex = true; } } catch (NumberFormatException nfe) { s_logger.warn("Invalid parameter " + key + " received, unable to parse object array, returning an error."); } if (!parsedIndex) { throw new ServerApiException(MALFORMED_PARAMETER_ERROR, "Unable to decode parameter " + key + "; if specifying an object array, please use parameter[index].field=XXX, e.g. userGroupList[0].group=httpGroup"); } Object value = lowercaseParams.get(paramName); if (value == null) { // for now, assume object array with sub fields mapArray = new HashMap(); mapValue = new HashMap(); mapArray.put(Integer.valueOf(index), mapValue); } else if (value instanceof Map) { mapArray = (HashMap)value; mapValue = mapArray.get(Integer.valueOf(index)); if (mapValue == null) { mapValue = new HashMap(); mapArray.put(Integer.valueOf(index), mapValue); } } // we are ready to store the value for a particular field into the map for this object mapValue.put(fieldName, (String)params.get(key)); lowercaseParams.put(paramName, mapArray); } else { lowercaseParams.put(key.toLowerCase(), params.get(key)); } } return lowercaseParams; } public String buildResponse(ServerApiException apiException, String responseType) { StringBuffer sb = new StringBuffer(); if (RESPONSE_TYPE_JSON.equalsIgnoreCase(responseType)) { // JSON response sb.append("{ \"" + getName() + "\" : { \"errorcode\" : \"" + apiException.getErrorCode() + "\", \"description\" : \"" + apiException.getDescription() + "\" } }"); } else { sb.append(""); sb.append("<" + getName() + ">"); sb.append("" + apiException.getErrorCode() + ""); sb.append("" + escapeXml(apiException.getDescription()) + ""); sb.append(""); } return sb.toString(); } public String buildResponse(List> tagList, String responseType) { StringBuffer sb = new StringBuffer(); // set up the return value with the name of the response if (RESPONSE_TYPE_JSON.equalsIgnoreCase(responseType)) { sb.append("{ \"" + getName() + "\" : { "); } else { sb.append(""); sb.append("<" + getName() + ">"); } int i = 0; for (Pair tagData : tagList) { String tagName = tagData.first(); Object tagValue = tagData.second(); if (tagValue instanceof Object[]) { Object[] subObjects = (Object[])tagValue; if (subObjects.length < 1) continue; writeObjectArray(responseType, sb, i++, tagName, subObjects); } else { writeNameValuePair(sb, tagName, tagValue, responseType, i++); } } // close the response if (RESPONSE_TYPE_JSON.equalsIgnoreCase(responseType)) { sb.append("} }"); } else { sb.append(""); } return sb.toString(); } private void writeNameValuePair(StringBuffer sb, String tagName, Object tagValue, String responseType, int propertyCount) { if (tagValue == null) { return; } if (tagValue instanceof Object[]) { Object[] subObjects = (Object[])tagValue; if (subObjects.length < 1) return; writeObjectArray(responseType, sb, propertyCount, tagName, subObjects); } else { if (RESPONSE_TYPE_JSON.equalsIgnoreCase(responseType)) { String seperator = ((propertyCount > 0) ? ", " : ""); sb.append(seperator + "\"" + tagName + "\" : \"" + escapeJSON(tagValue.toString()) + "\""); } else { sb.append("<" + tagName + ">" + escapeXml(tagValue.toString()) + ""); } } } private void writeObjectArray(String responseType, StringBuffer sb, int propertyCount, String tagName, Object[] subObjects) { if (RESPONSE_TYPE_JSON.equalsIgnoreCase(responseType)) { String separator = ((propertyCount > 0) ? ", " : ""); sb.append(separator); } int j = 0; for (Object subObject : subObjects) { if (subObject instanceof List) { List subObjList = (List)subObject; writeSubObject(sb, tagName, subObjList, responseType, j++); } } if (RESPONSE_TYPE_JSON.equalsIgnoreCase(responseType)) { sb.append("]"); } } private void writeSubObject(StringBuffer sb, String tagName, List tagList, String responseType, int objectCount) { if (RESPONSE_TYPE_JSON.equalsIgnoreCase(responseType)) { sb.append(((objectCount == 0) ? "\"" + tagName + "\" : [ { " : ", { ")); } else { sb.append("<" + tagName + ">"); } int i = 0; for (Object tag : tagList) { if (tag instanceof Pair) { Pair nameValuePair = (Pair)tag; writeNameValuePair(sb, (String)nameValuePair.first(), nameValuePair.second(), responseType, i++); } } if (RESPONSE_TYPE_JSON.equalsIgnoreCase(responseType)) { sb.append("}"); } else { sb.append(""); } } /** * Escape xml response set to false by default. API commands to override this method to allow escaping */ public boolean requireXmlEscape() { return true; } private String escapeXml(String xml){ if(!requireXmlEscape()){ return xml; } int iLen = xml.length(); if (iLen == 0) return xml; StringBuffer sOUT = new StringBuffer(iLen + 256); int i = 0; for (; i < iLen; i++) { char c = xml.charAt(i); if (c == '<') sOUT.append("<"); else if (c == '>') sOUT.append(">"); else if (c == '&') sOUT.append("&"); else if (c == '"') sOUT.append("""); else if (c == '\'') sOUT.append("'"); else sOUT.append(c); } return sOUT.toString(); } private static String escapeJSON(String str) { if (str == null) { return str; } return str.replace("\"", "\\\""); } protected long waitInstanceCreation(long jobId) { ManagementServer mgr = getManagementServer(); long instanceId = 0; AsyncJobVO job = null; boolean interruped = false; // as job may be executed in other management server, we need to do a database polling here try { boolean quit = false; while(!quit) { job = mgr.findAsyncJobById(jobId); if(job == null) { s_logger.error("Async command " + this.getClass().getName() + " waitInstanceCreation error: job-" + jobId + " no longer exists"); break; } switch(job.getStatus()) { case AsyncJobResult.STATUS_IN_PROGRESS : if(job.getProcessStatus() == BaseCmd.PROGRESS_INSTANCE_CREATED) { Long id = (Long)SerializerHelper.fromSerializedString(job.getResult()); if(id != null) { instanceId = id.longValue(); if(s_logger.isDebugEnabled()) s_logger.debug("Async command " + this.getClass().getName() + " succeeded in waiting for new instance to be created, instance Id: " + instanceId); } else { s_logger.warn("Async command " + this.getClass().getName() + " has new instance created, but value as null?"); } quit = true; } break; case AsyncJobResult.STATUS_SUCCEEDED : instanceId = getInstanceIdFromJobSuccessResult(job.getResult()); quit = true; break; case AsyncJobResult.STATUS_FAILED : s_logger.error("Async command " + this.getClass().getName() + " executing job-" + jobId + " failed, result: " + job.getResult()); quit = true; break; } if(quit) break; try { Thread.sleep(1000); } catch (InterruptedException e) { interruped = true; } } } finally { if(interruped) Thread.currentThread().interrupt(); } return instanceId; } protected long getInstanceIdFromJobSuccessResult(String result) { s_logger.debug("getInstanceIdFromJobSuccessResult not overridden in subclass " + this.getClass().getName()); return 0; } public static boolean isAdmin(short accountType) { return ((accountType == Account.ACCOUNT_TYPE_ADMIN) || (accountType == Account.ACCOUNT_TYPE_DOMAIN_ADMIN) || (accountType == Account.ACCOUNT_TYPE_READ_ONLY_ADMIN)); } private Account getAccount(Map params) throws ServerApiException { // FIXME: This should go into the context! Long domainId = (Long) params.get("domainid"); Account account = (Account)params.get("accountobj"); String accountName = (String) params.get("account"); Long accountId = null; Account finalAccount = null; ManagementServer managementServer = getManagementServer(); if ((account == null) || isAdmin(account.getType())) { if (domainId != null) { if ((account != null) && !managementServer.isChildDomain(account.getDomainId(), domainId)) { throw new ServerApiException(PARAM_ERROR, "Invalid domain id (" + domainId + ") "); } if (accountName != null) { Account userAccount = managementServer.findActiveAccount(accountName, domainId); if (userAccount == null) { throw new ServerApiException(PARAM_ERROR, "Unable to find account " + accountName + " in domain " + domainId); } accountId = userAccount.getId(); } } else { accountId = ((account != null) ? account.getId() : null); } } else { accountId = account.getId(); } if (accountId != null) { finalAccount = managementServer.findAccountById(accountId); } return finalAccount; } protected Long checkAccountPermissions(Map params, long targetAccountId, long targetDomainId, String targetDesc, long targetId) throws ServerApiException { Long accountId = null; Account account = getAccount(params); if (account != null) { if (!isAdmin(account.getType())) { if (account.getId().longValue() != targetAccountId) { throw new ServerApiException(BaseCmd.PARAM_ERROR, "Unable to find a " + targetDesc + " with id " + targetId + " for this account"); } } else if (!getManagementServer().isChildDomain(account.getDomainId(), targetDomainId)) { throw new ServerApiException(BaseCmd.PARAM_ERROR, "Unable to perform operation for " + targetDesc + " with id " + targetId + ", permission denied."); } accountId = account.getId(); } return accountId; } }