diff --git a/api/src/org/apache/cloudstack/api/ApiConstants.java b/api/src/org/apache/cloudstack/api/ApiConstants.java index 6d8400ae6fd..b4ce24c2bc9 100644 --- a/api/src/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/org/apache/cloudstack/api/ApiConstants.java @@ -91,6 +91,7 @@ public class ApiConstants { public static final String INTERVAL_TYPE = "intervaltype"; public static final String IP_ADDRESS = "ipaddress"; public static final String IP_ADDRESS_ID = "ipaddressid"; + public static final String IS_ASYNC = "isasync"; public static final String IP_AVAILABLE = "ipavailable"; public static final String IP_LIMIT = "iplimit"; public static final String IP_TOTAL = "iptotal"; @@ -106,6 +107,7 @@ public class ApiConstants { public static final String JOB_STATUS = "jobstatus"; public static final String LASTNAME = "lastname"; public static final String LEVEL = "level"; + public static final String LENGTH = "length"; public static final String LIMIT_CPU_USE = "limitcpuuse"; public static final String LOCK = "lock"; public static final String LUN = "lun"; @@ -126,6 +128,7 @@ public class ApiConstants { public static final String OP = "op"; public static final String OS_CATEGORY_ID = "oscategoryid"; public static final String OS_TYPE_ID = "ostypeid"; + public static final String PARAMS = "params"; public static final String PARENT_DOMAIN_ID = "parentdomainid"; public static final String PASSWORD = "password"; public static final String NEW_PASSWORD = "new_password"; @@ -159,6 +162,7 @@ public class ApiConstants { public static final String SCHEDULE = "schedule"; public static final String SCOPE = "scope"; public static final String SECRET_KEY = "usersecretkey"; + public static final String SINCE = "since"; public static final String KEY = "key"; public static final String SEARCH_BASE = "searchbase"; public static final String SECURITY_GROUP_IDS = "securitygroupids"; @@ -324,6 +328,7 @@ public class ApiConstants { public static final String SOURCE_NAT_SUPPORTED = "sourcenatsupported"; public static final String RESOURCE_STATE = "resourcestate"; public static final String PROJECT_INVITE_REQUIRED = "projectinviterequired"; + public static final String REQUIRED = "required"; public static final String RESTART_REQUIRED = "restartrequired"; public static final String ALLOW_USER_CREATE_PROJECTS = "allowusercreateprojects"; public static final String CONSERVE_MODE = "conservemode"; diff --git a/api/src/org/apache/cloudstack/discovery/ApiDiscoveryService.java b/api/src/org/apache/cloudstack/discovery/ApiDiscoveryService.java new file mode 100644 index 00000000000..12206949db3 --- /dev/null +++ b/api/src/org/apache/cloudstack/discovery/ApiDiscoveryService.java @@ -0,0 +1,29 @@ +// 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.cloudstack.discovery; + +import com.cloud.utils.component.Adapter; +import com.cloud.utils.component.PluggableService; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.response.ListResponse; + +import java.util.Map; + +public interface ApiDiscoveryService extends Adapter, PluggableService { + ListResponse listApis(); + Map> getApiNameCmdClassMapping(); +} diff --git a/client/pom.xml b/client/pom.xml index b703cbe966f..1bbae1f7d08 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -30,6 +30,11 @@ cloud-plugin-acl-static-role-based ${project.version} + + org.apache.cloudstack + cloud-plugin-api-discovery + ${project.version} + org.apache.cloudstack cloud-plugin-user-authenticator-ldap diff --git a/client/tomcatconf/api-discovery_commands.properties.in b/client/tomcatconf/api-discovery_commands.properties.in new file mode 100644 index 00000000000..49ddfde42d8 --- /dev/null +++ b/client/tomcatconf/api-discovery_commands.properties.in @@ -0,0 +1,23 @@ +# 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. + +# bitmap of permissions at the end of each classname, 1 = ADMIN, 2 = +# RESOURCE_DOMAIN_ADMIN, 4 = DOMAIN_ADMIN, 8 = USER +# Please standardize naming conventions to camel-case (even for acronyms). + +# CloudStack API Discovery service command +listApis=15 diff --git a/client/tomcatconf/components.xml.in b/client/tomcatconf/components.xml.in index a7378bd9f49..b9feed15a88 100755 --- a/client/tomcatconf/components.xml.in +++ b/client/tomcatconf/components.xml.in @@ -56,6 +56,9 @@ under the License. + + + @@ -177,6 +180,7 @@ under the License. + diff --git a/plugins/api/discovery/pom.xml b/plugins/api/discovery/pom.xml new file mode 100644 index 00000000000..a61b275addc --- /dev/null +++ b/plugins/api/discovery/pom.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + cloud-plugin-api-discovery + Apache CloudStack Plugin - API Discovery + + org.apache.cloudstack + cloudstack-plugins + 4.1.0-SNAPSHOT + ../../pom.xml + + + + org.apache.cloudstack + cloud-api + ${project.version} + + + org.apache.cloudstack + cloud-utils + ${project.version} + + + diff --git a/plugins/api/discovery/src/org/apache/cloudstack/api/command/user/discovery/ListApisCmd.java b/plugins/api/discovery/src/org/apache/cloudstack/api/command/user/discovery/ListApisCmd.java new file mode 100644 index 00000000000..a7e60e340b2 --- /dev/null +++ b/plugins/api/discovery/src/org/apache/cloudstack/api/command/user/discovery/ListApisCmd.java @@ -0,0 +1,55 @@ +// 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.cloudstack.api.command.user.discovery; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.PlugService; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.discovery.ApiDiscoveryService; +import org.apache.cloudstack.api.response.ApiDiscoveryResponse; + +import org.apache.log4j.Logger; + +@APICommand(name = "listApis", responseObject = ApiDiscoveryResponse.class, description = "lists all apis available to the user as per their account type", since = "4.1.0") +public class ListApisCmd extends BaseListCmd { + + public static final Logger s_logger = Logger.getLogger(ListApisCmd.class.getName()); + private static final String s_name = "listapisresponse"; + + @PlugService + ApiDiscoveryService _apiDiscoveryService; + + @Override + public void execute() throws ServerApiException { + if (_apiDiscoveryService != null) { + ListResponse response = (ListResponse) _apiDiscoveryService.listApis(); + if (response == null) { + throw new ServerApiException(BaseCmd.INTERNAL_ERROR, "Api Discovery plugin was unable to find and process any apis"); + } + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } + } + + @Override + public String getCommandName() { + return s_name; + } +} diff --git a/plugins/api/discovery/src/org/apache/cloudstack/api/response/ApiDiscoveryResponse.java b/plugins/api/discovery/src/org/apache/cloudstack/api/response/ApiDiscoveryResponse.java new file mode 100644 index 00000000000..b6a4f124954 --- /dev/null +++ b/plugins/api/discovery/src/org/apache/cloudstack/api/response/ApiDiscoveryResponse.java @@ -0,0 +1,75 @@ +// 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.cloudstack.api.response; + +import com.cloud.user.Account; +import org.apache.cloudstack.api.ApiConstants; +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; + +import java.util.HashSet; +import java.util.Set; + +@SuppressWarnings("unused") +@EntityReference(value = Account.class) +public class ApiDiscoveryResponse extends BaseResponse { + @SerializedName(ApiConstants.NAME) @Param(description="the name of the api command") + private String name; + + @SerializedName(ApiConstants.DESCRIPTION) @Param(description="description of the api") + private String description; + + @SerializedName(ApiConstants.SINCE) @Param(description="version of CloudStack the api was introduced in") + private String since; + + @SerializedName(ApiConstants.IS_ASYNC) @Param(description="true if api is asynchronous") + private Boolean isAsync; + + @SerializedName(ApiConstants.PARAMS) @Param(description="the list params the api accepts", responseObject = ApiParameterResponse.class) + private Set params; + + public ApiDiscoveryResponse(){ + params = new HashSet(); + isAsync = false; + } + + public void setName(String name) { + this.name = name; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setSince(String since) { + this.since = since; + } + + public void setAsync(Boolean isAsync) { + this.isAsync = isAsync; + } + + public void setParams(Set params) { + this.params = params; + } + + public void addParam(ApiParameterResponse param) { + this.params.add(param); + } +} \ No newline at end of file diff --git a/plugins/api/discovery/src/org/apache/cloudstack/api/response/ApiParameterResponse.java b/plugins/api/discovery/src/org/apache/cloudstack/api/response/ApiParameterResponse.java new file mode 100644 index 00000000000..9a73bec3dee --- /dev/null +++ b/plugins/api/discovery/src/org/apache/cloudstack/api/response/ApiParameterResponse.java @@ -0,0 +1,70 @@ +// 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.cloudstack.api.response; + +import org.apache.cloudstack.api.ApiConstants; +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.BaseResponse; + +public class ApiParameterResponse extends BaseResponse { + @SerializedName(ApiConstants.NAME) @Param(description="the name of the api parameter") + private String name; + + @SerializedName(ApiConstants.DESCRIPTION) @Param(description="description of the api parameter") + private String description; + + @SerializedName(ApiConstants.TYPE) @Param(description="parameter type") + private String type; + + @SerializedName(ApiConstants.LENGTH) @Param(description="length of the parameter") + private int length; + + @SerializedName(ApiConstants.REQUIRED) @Param(description="version of CloudStack the api was introduced in") + private Boolean required; + + @SerializedName(ApiConstants.SINCE) @Param(description="version of CloudStack the api was introduced in") + private String since; + + public ApiParameterResponse(){ + } + + public void setName(String name) { + this.name = name; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setType(String type) { + this.type = type; + } + + public void setLength(int length) { + this.length = length; + } + + public void setRequired(Boolean required) { + this.required = required; + } + + public void setSince(String since) { + this.since = since; + } + + } \ No newline at end of file diff --git a/plugins/api/discovery/src/org/apache/cloudstack/discovery/ApiDiscoveryServiceImpl.java b/plugins/api/discovery/src/org/apache/cloudstack/discovery/ApiDiscoveryServiceImpl.java new file mode 100644 index 00000000000..ca57df5d314 --- /dev/null +++ b/plugins/api/discovery/src/org/apache/cloudstack/discovery/ApiDiscoveryServiceImpl.java @@ -0,0 +1,144 @@ +// 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.cloudstack.discovery; + +import com.cloud.utils.ReflectUtil; +import com.cloud.utils.component.AdapterBase; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.BaseAsyncCreateCmd; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.ApiDiscoveryResponse; +import org.apache.cloudstack.api.response.ApiParameterResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.log4j.Logger; + +import javax.ejb.Local; +import javax.naming.ConfigurationException; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Local(value = ApiDiscoveryService.class) +public class ApiDiscoveryServiceImpl extends AdapterBase implements ApiDiscoveryService { + + private static final Logger s_logger = Logger.getLogger(ApiDiscoveryServiceImpl.class); + private Map> _apiNameCmdClassMap; + private ListResponse _discoveryResponse; + + protected ApiDiscoveryServiceImpl() { + super(); + } + + private void generateApiNameCmdClassMapping() { + _apiNameCmdClassMap = new HashMap>(); + Set> cmdClasses = ReflectUtil.getClassesWithAnnotation(APICommand.class, new String[]{"org.apache.cloudstack.api", "com.cloud.api"}); + + for(Class cmdClass: cmdClasses) { + String apiName = cmdClass.getAnnotation(APICommand.class).name(); + if (_apiNameCmdClassMap.containsKey(apiName)) { + s_logger.error("API Cmd class " + cmdClass.getName() + " has non-unique apiname" + apiName); + continue; + } + _apiNameCmdClassMap.put(apiName, cmdClass); + } + } + + private void precacheListApiResponse() { + + if(_apiNameCmdClassMap == null) + return; + + _discoveryResponse = new ListResponse(); + + List apiDiscoveryResponses = new ArrayList(); + + for(String key: _apiNameCmdClassMap.keySet()) { + Class cmdClass = _apiNameCmdClassMap.get(key); + APICommand apiCmdAnnotation = cmdClass.getAnnotation(APICommand.class); + if (apiCmdAnnotation == null) + apiCmdAnnotation = cmdClass.getSuperclass().getAnnotation(APICommand.class); + if (apiCmdAnnotation == null + || !apiCmdAnnotation.includeInApiDoc() + || apiCmdAnnotation.name().isEmpty()) + continue; + + ApiDiscoveryResponse response = new ApiDiscoveryResponse(); + response.setName(apiCmdAnnotation.name()); + response.setDescription(apiCmdAnnotation.description()); + response.setSince(apiCmdAnnotation.since()); + + Field[] fields = ReflectUtil.getAllFieldsForClass(cmdClass, + new Class[] {BaseCmd.class, BaseAsyncCmd.class, BaseAsyncCreateCmd.class}); + + boolean isAsync = ReflectUtil.isCmdClassAsync(cmdClass, + new Class[] {BaseAsyncCmd.class, BaseAsyncCreateCmd.class}); + + response.setAsync(isAsync); + + for(Field field: fields) { + Parameter parameterAnnotation = field.getAnnotation(Parameter.class); + if (parameterAnnotation != null + && parameterAnnotation.expose() + && parameterAnnotation.includeInApiDoc()) { + + ApiParameterResponse paramResponse = new ApiParameterResponse(); + paramResponse.setName(parameterAnnotation.name()); + paramResponse.setDescription(parameterAnnotation.description()); + paramResponse.setType(parameterAnnotation.type().toString()); + paramResponse.setLength(parameterAnnotation.length()); + paramResponse.setRequired(parameterAnnotation.required()); + paramResponse.setSince(parameterAnnotation.since()); + response.addParam(paramResponse); + } + } + response.setObjectName("apis"); + apiDiscoveryResponses.add(response); + } + _discoveryResponse.setResponses(apiDiscoveryResponses); + } + + @Override + public boolean configure(String name, Map params) + throws ConfigurationException { + super.configure(name, params); + + generateApiNameCmdClassMapping(); + precacheListApiResponse(); + + return true; + } + + public Map> getApiNameCmdClassMapping() { + return _apiNameCmdClassMap; + } + + @Override + public ListResponse listApis() { + return _discoveryResponse; + } + + @Override + public String[] getPropertiesFiles() { + return new String[]{"api-discovery_commands.properties"}; + } +} \ No newline at end of file diff --git a/plugins/pom.xml b/plugins/pom.xml index f0589a1caaf..a42ae2967b1 100644 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -32,6 +32,7 @@ test + api/discovery acl/static-role-based deployment-planners/user-concentrated-pod deployment-planners/user-dispersing diff --git a/server/src/com/cloud/api/ApiServer.java b/server/src/com/cloud/api/ApiServer.java index e2dc9ce2bbf..8d304e0c9b3 100755 --- a/server/src/com/cloud/api/ApiServer.java +++ b/server/src/com/cloud/api/ApiServer.java @@ -50,6 +50,7 @@ import javax.crypto.spec.SecretKeySpec; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import com.cloud.utils.ReflectUtil; import org.apache.cloudstack.acl.APIAccessChecker; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.api.*; @@ -59,6 +60,7 @@ import org.apache.cloudstack.api.command.user.event.ListEventsCmd; import org.apache.cloudstack.api.command.user.vm.ListVMsCmd; import org.apache.cloudstack.api.command.user.vmgroup.ListVMGroupsCmd; import org.apache.cloudstack.api.command.user.volume.ListVolumesCmd; +import org.apache.cloudstack.discovery.ApiDiscoveryService; import org.apache.commons.codec.binary.Base64; import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.ConnectionClosedException; @@ -145,8 +147,11 @@ public class ApiServer implements HttpRequestHandler { @Inject private AccountManager _accountMgr = null; @Inject private DomainManager _domainMgr = null; @Inject private AsyncJobManager _asyncMgr = null; + @Inject(adapter = APIAccessChecker.class) protected Adapters _apiAccessCheckers; + @Inject(adapter = ApiDiscoveryService.class) + protected Adapters _apiDiscoveryServices; private Account _systemAccount = null; private User _systemUser = null; @@ -201,17 +206,8 @@ public class ApiServer implements HttpRequestHandler { } } - // Populate api name and cmd class mappings - Reflections reflections = new Reflections("org.apache.cloudstack.api"); - Set> cmdClasses = reflections.getTypesAnnotatedWith(APICommand.class); - reflections = new Reflections("com.cloud.api"); - cmdClasses.addAll(reflections.getTypesAnnotatedWith(APICommand.class)); - for(Class cmdClass: cmdClasses) { - String apiName = cmdClass.getAnnotation(APICommand.class).name(); - if (_apiNameCmdClassMap.containsKey(apiName)) { - s_logger.error("API Cmd class " + cmdClass.getName() + " has non-unique apiname" + apiName); - } - _apiNameCmdClassMap.put(apiName, cmdClass); + for (ApiDiscoveryService discoveryService: _apiDiscoveryServices) { + _apiNameCmdClassMap.putAll(discoveryService.getApiNameCmdClassMapping()); } encodeApiResponse = Boolean.valueOf(configDao.getValue(Config.EncodeApiResponse.key()));