This is an automated email from the ASF dual-hosted git repository. ofuks pushed a commit to branch audit in repository https://gitbox.apache.org/repos/asf/incubator-dlab.git
commit 6560b6c2adf1e43e0e3d598426aaa261a96c44fb Author: Oleh Fuks <olegfuk...@gmail.com> AuthorDate: Wed Jun 10 12:52:19 2020 +0300 Added audit for computational resources --- .../epam/dlab/rest/contracts/ComputationalAPI.java | 1 + .../azure/ComputationalResourceAzure.java | 2 +- .../dlab/backendapi/domain/AuditActionEnum.java | 1 + .../resources/aws/ComputationalResourceAws.java | 10 ++-- .../azure/ComputationalResourceAzure.java | 10 ++-- .../resources/gcp/ComputationalResourceGcp.java | 40 +++++++------- .../backendapi/service/ComputationalService.java | 19 ++++--- .../service/impl/ComputationalServiceImpl.java | 45 ++++++++-------- .../service/impl/EnvironmentServiceImpl.java | 8 +-- .../service/impl/SchedulerJobServiceImpl.java | 14 ++--- .../epam/dlab/backendapi/util/RequestBuilder.java | 60 ++++++++++----------- .../resources/ExploratoryResourceTest.java | 16 ------ .../service/impl/ComputationalServiceImplTest.java | 63 ++++++++++++---------- .../service/impl/EnvironmentServiceImplTest.java | 7 +-- .../service/impl/SchedulerJobServiceImplTest.java | 43 +++++++-------- 15 files changed, 172 insertions(+), 167 deletions(-) diff --git a/services/dlab-webapp-common/src/main/java/com/epam/dlab/rest/contracts/ComputationalAPI.java b/services/dlab-webapp-common/src/main/java/com/epam/dlab/rest/contracts/ComputationalAPI.java index a909635..1586c0b 100644 --- a/services/dlab-webapp-common/src/main/java/com/epam/dlab/rest/contracts/ComputationalAPI.java +++ b/services/dlab-webapp-common/src/main/java/com/epam/dlab/rest/contracts/ComputationalAPI.java @@ -20,6 +20,7 @@ package com.epam.dlab.rest.contracts; public interface ComputationalAPI { + String AUDIT_MESSAGE = "Notebook name: %s"; String LIBRARY = "library/"; String COMPUTATIONAL = "computational"; String COMPUTATIONAL_CREATE = COMPUTATIONAL + "/create"; diff --git a/services/provisioning-service/src/main/java/com/epam/dlab/backendapi/resources/azure/ComputationalResourceAzure.java b/services/provisioning-service/src/main/java/com/epam/dlab/backendapi/resources/azure/ComputationalResourceAzure.java index 59b5f27..2ca5c42 100644 --- a/services/provisioning-service/src/main/java/com/epam/dlab/backendapi/resources/azure/ComputationalResourceAzure.java +++ b/services/provisioning-service/src/main/java/com/epam/dlab/backendapi/resources/azure/ComputationalResourceAzure.java @@ -41,7 +41,7 @@ import javax.ws.rs.core.MediaType; @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @Slf4j -public class ComputationalResourceAzure { +public class ComputationalResourceAzure implements ComputationalAPI { @Inject private SparkClusterService sparkClusterService; diff --git a/services/self-service/src/main/java/com/epam/dlab/backendapi/domain/AuditActionEnum.java b/services/self-service/src/main/java/com/epam/dlab/backendapi/domain/AuditActionEnum.java index eb49aa1..315925c 100644 --- a/services/self-service/src/main/java/com/epam/dlab/backendapi/domain/AuditActionEnum.java +++ b/services/self-service/src/main/java/com/epam/dlab/backendapi/domain/AuditActionEnum.java @@ -22,5 +22,6 @@ package com.epam.dlab.backendapi.domain; public enum AuditActionEnum { CREATE_PROJECT, START_PROJECT, STOP_PROJECT, TERMINATE_PROJECT, UPDATE_PROJECT, CREATE_NOTEBOOK, START_NOTEBOOK, STOP_NOTEBOOK, TERMINATE_NOTEBOOK, UPDATE_CLUSTER_CONFIG, + CREATE_DATA_ENGINE, CREATE_DATA_ENGINE_SERVICE, TERMINATE_COMPUTATIONAL, FOLLOW_NOTEBOOK_LINK } diff --git a/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/aws/ComputationalResourceAws.java b/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/aws/ComputationalResourceAws.java index 87f99bd..d6ab4f4 100644 --- a/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/aws/ComputationalResourceAws.java +++ b/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/aws/ComputationalResourceAws.java @@ -48,6 +48,7 @@ import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import java.util.Collections; import java.util.List; import static com.epam.dlab.dto.UserInstanceStatus.CREATING; @@ -106,8 +107,8 @@ public class ComputationalResourceAws implements ComputationalAPI { .config(form.getConfig()) .version(form.getVersion()) .build(); - boolean resourceAdded = computationalService.createDataEngineService(userInfo, form, - awsComputationalResource, form.getProject()); + boolean resourceAdded = computationalService.createDataEngineService(userInfo, form.getName(), form, awsComputationalResource, + form.getProject(), Collections.singletonList(String.format(AUDIT_MESSAGE, form.getNotebookName()))); return resourceAdded ? Response.ok().build() : Response.status(Response.Status.FOUND).build(); } @@ -129,7 +130,7 @@ public class ComputationalResourceAws implements ComputationalAPI { log.debug("Create computational resources for {} | form is {}", userInfo.getName(), form); validate(form); - return computationalService.createSparkCluster(userInfo, form, form.getProject()) + return computationalService.createSparkCluster(userInfo, form.getName(), form, form.getProject(), Collections.singletonList(String.format(AUDIT_MESSAGE, form.getNotebookName()))) ? Response.ok().build() : Response.status(Response.Status.FOUND).build(); } @@ -150,7 +151,8 @@ public class ComputationalResourceAws implements ComputationalAPI { @PathParam("computationalName") String computationalName) { log.debug("Terminating computational resource {} for user {}", computationalName, userInfo.getName()); - computationalService.terminateComputational(userInfo, projectName, exploratoryName, computationalName); + computationalService.terminateComputational(userInfo, userInfo.getName(), projectName, exploratoryName, + computationalName, Collections.singletonList(String.format(AUDIT_MESSAGE, exploratoryName))); return Response.ok().build(); } diff --git a/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/azure/ComputationalResourceAzure.java b/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/azure/ComputationalResourceAzure.java index ca18d14..922260b 100644 --- a/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/azure/ComputationalResourceAzure.java +++ b/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/azure/ComputationalResourceAzure.java @@ -42,8 +42,11 @@ import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import java.util.Collections; import java.util.List; +import static com.epam.dlab.rest.contracts.ComputationalAPI.AUDIT_MESSAGE; + /** * Provides the REST API for the computational resource on Azure. */ @@ -79,16 +82,14 @@ public class ComputationalResourceAzure { public Response createDataEngine(@Auth UserInfo userInfo, @Valid @NotNull SparkStandaloneClusterCreateForm form) { log.debug("Create computational resources for {} | form is {}", userInfo.getName(), form); - if (!UserRoles.checkAccess(userInfo, RoleType.COMPUTATIONAL, form.getImage(), userInfo.getRoles())) { log.warn("Unauthorized attempt to create a {} by user {}", form.getImage(), userInfo.getName()); throw new DlabException("You do not have the privileges to create a " + form.getTemplateName()); } - return computationalService.createSparkCluster(userInfo, form, form.getProject()) + return computationalService.createSparkCluster(userInfo, form.getName(), form, form.getProject(), Collections.singletonList(String.format(AUDIT_MESSAGE, form.getNotebookName()))) ? Response.ok().build() : Response.status(Response.Status.FOUND).build(); - } /** @@ -108,7 +109,8 @@ public class ComputationalResourceAzure { log.debug("Terminating computational resource {} for user {}", computationalName, userInfo.getName()); - computationalService.terminateComputational(userInfo, projectName, exploratoryName, computationalName); + computationalService.terminateComputational(userInfo, userInfo.getName(), projectName, exploratoryName, + computationalName, Collections.singletonList(String.format(AUDIT_MESSAGE, exploratoryName))); return Response.ok().build(); } diff --git a/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/gcp/ComputationalResourceGcp.java b/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/gcp/ComputationalResourceGcp.java index 087330a..71eaf35 100644 --- a/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/gcp/ComputationalResourceGcp.java +++ b/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/gcp/ComputationalResourceGcp.java @@ -48,6 +48,7 @@ import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import java.util.Collections; import java.util.List; import static com.epam.dlab.dto.UserInstanceStatus.CREATING; @@ -82,7 +83,7 @@ public class ComputationalResourceGcp implements ComputationalAPI { * Asynchronously creates Dataproc cluster * * @param userInfo user info. - * @param formDTO DTO info about creation of the computational resource. + * @param form DTO info about creation of the computational resource. * @return 200 OK - if request success, 302 Found - for duplicates. * @throws IllegalArgumentException if docker image name is malformed */ @@ -90,30 +91,30 @@ public class ComputationalResourceGcp implements ComputationalAPI { @Path("dataengine-service") @Operation(tags = "computational", summary = "Create dataproc cluster") public Response createDataEngineService(@Auth @Parameter(hidden = true) UserInfo userInfo, - @Valid @NotNull @Parameter GcpComputationalCreateForm formDTO) { + @Valid @NotNull @Parameter GcpComputationalCreateForm form) { - log.debug("Create computational resources for {} | form is {}", userInfo.getName(), formDTO); + log.debug("Create computational resources for {} | form is {}", userInfo.getName(), form); - if (DataEngineType.CLOUD_SERVICE == DataEngineType.fromDockerImageName(formDTO.getImage())) { - validate(userInfo, formDTO); + if (DataEngineType.CLOUD_SERVICE == DataEngineType.fromDockerImageName(form.getImage())) { + validate(userInfo, form); GcpComputationalResource gcpComputationalResource = GcpComputationalResource.builder().computationalName - (formDTO.getName()) - .imageName(formDTO.getImage()) - .templateName(formDTO.getTemplateName()) + (form.getName()) + .imageName(form.getImage()) + .templateName(form.getTemplateName()) .status(CREATING.toString()) - .masterShape(formDTO.getMasterInstanceType()) - .slaveShape(formDTO.getSlaveInstanceType()) - .slaveNumber(formDTO.getSlaveInstanceCount()) - .masterNumber(formDTO.getMasterInstanceCount()) - .preemptibleNumber(formDTO.getPreemptibleCount()) - .version(formDTO.getVersion()) + .masterShape(form.getMasterInstanceType()) + .slaveShape(form.getSlaveInstanceType()) + .slaveNumber(form.getSlaveInstanceCount()) + .masterNumber(form.getMasterInstanceCount()) + .preemptibleNumber(form.getPreemptibleCount()) + .version(form.getVersion()) .build(); - boolean resourceAdded = computationalService.createDataEngineService(userInfo, formDTO, - gcpComputationalResource, formDTO.getProject()); + boolean resourceAdded = computationalService.createDataEngineService(userInfo, form.getName(), form, gcpComputationalResource, + form.getProject(), Collections.singletonList(String.format(AUDIT_MESSAGE, form.getNotebookName()))); return resourceAdded ? Response.ok().build() : Response.status(Response.Status.FOUND).build(); } - throw new IllegalArgumentException("Malformed image " + formDTO.getImage()); + throw new IllegalArgumentException("Malformed image " + form.getImage()); } @@ -135,7 +136,7 @@ public class ComputationalResourceGcp implements ComputationalAPI { throw new DlabException("You do not have the privileges to create a " + form.getTemplateName()); } - return computationalService.createSparkCluster(userInfo, form, form.getProject()) + return computationalService.createSparkCluster(userInfo, form.getName(), form, form.getProject(), Collections.singletonList(String.format(AUDIT_MESSAGE, form.getNotebookName()))) ? Response.ok().build() : Response.status(Response.Status.FOUND).build(); } @@ -157,7 +158,8 @@ public class ComputationalResourceGcp implements ComputationalAPI { @PathParam("computationalName") String computationalName) { log.debug("Terminating computational resource {} for user {}", computationalName, userInfo.getName()); - computationalService.terminateComputational(userInfo, projectName, exploratoryName, computationalName); + computationalService.terminateComputational(userInfo, userInfo.getName(), projectName, exploratoryName, + computationalName, Collections.singletonList(String.format(AUDIT_MESSAGE, exploratoryName))); return Response.ok().build(); } diff --git a/services/self-service/src/main/java/com/epam/dlab/backendapi/service/ComputationalService.java b/services/self-service/src/main/java/com/epam/dlab/backendapi/service/ComputationalService.java index 4a6f392..135657f 100644 --- a/services/self-service/src/main/java/com/epam/dlab/backendapi/service/ComputationalService.java +++ b/services/self-service/src/main/java/com/epam/dlab/backendapi/service/ComputationalService.java @@ -35,27 +35,30 @@ public interface ComputationalService { /** * Asynchronously triggers creation of Spark cluster * - * @param userInfo user authentication info - * @param form input cluster parameters + * @param userInfo user authentication info + * @param resourceName name of computational resource + * @param form input cluster parameters + * @param auditInfo additional info for audit * @return <code>true</code> if action is successfully triggered, <code>false</code>false if cluster with the same * name already exists * @throws IllegalArgumentException if input parameters exceed limits or docker image name is malformed */ - boolean createSparkCluster(UserInfo userInfo, SparkStandaloneClusterCreateForm form, String project); + boolean createSparkCluster(UserInfo userInfo, String resourceName, SparkStandaloneClusterCreateForm form, String project, List<String> auditInfo); /** * Asynchronously triggers termination of computational resources * * @param userInfo user info of authenticated user + * @param resourceCreator username of resource creator * @param project project name - * @param exploratoryName name of exploratory where to terminate computational resources with - * <code>computationalName</code> + * @param exploratoryName name of exploratory where to terminate computational resources with <code>computationalName</code> * @param computationalName computational name + * @param auditInfo additional info for audit */ - void terminateComputational(UserInfo userInfo, String project, String exploratoryName, String computationalName); + void terminateComputational(UserInfo userInfo, String resourceCreator, String project, String exploratoryName, String computationalName, List<String> auditInfo); - boolean createDataEngineService(UserInfo userInfo, ComputationalCreateFormDTO formDTO, UserComputationalResource - computationalResource, String project); + boolean createDataEngineService(UserInfo userInfo, String resourceName, ComputationalCreateFormDTO formDTO, UserComputationalResource + computationalResource, String project, List<String> auditInfo); void stopSparkCluster(UserInfo userInfo, String project, String exploratoryName, String computationalName); diff --git a/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/ComputationalServiceImpl.java b/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/ComputationalServiceImpl.java index 722ee4d..d13fd6a 100644 --- a/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/ComputationalServiceImpl.java +++ b/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/ComputationalServiceImpl.java @@ -21,8 +21,12 @@ package com.epam.dlab.backendapi.service.impl; import com.epam.dlab.auth.UserInfo; +import com.epam.dlab.backendapi.annotation.Audit; import com.epam.dlab.backendapi.annotation.BudgetLimited; +import com.epam.dlab.backendapi.annotation.Info; import com.epam.dlab.backendapi.annotation.Project; +import com.epam.dlab.backendapi.annotation.ResourceName; +import com.epam.dlab.backendapi.annotation.User; import com.epam.dlab.backendapi.dao.ComputationalDAO; import com.epam.dlab.backendapi.dao.ExploratoryDAO; import com.epam.dlab.backendapi.domain.EndpointDTO; @@ -65,6 +69,9 @@ import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; +import static com.epam.dlab.backendapi.domain.AuditActionEnum.CREATE_DATA_ENGINE; +import static com.epam.dlab.backendapi.domain.AuditActionEnum.CREATE_DATA_ENGINE_SERVICE; +import static com.epam.dlab.backendapi.domain.AuditActionEnum.TERMINATE_COMPUTATIONAL; import static com.epam.dlab.dto.UserInstanceStatus.CREATING; import static com.epam.dlab.dto.UserInstanceStatus.FAILED; import static com.epam.dlab.dto.UserInstanceStatus.RECONFIGURING; @@ -142,9 +149,10 @@ public class ComputationalServiceImpl implements ComputationalService { } @BudgetLimited + @Audit(action = CREATE_DATA_ENGINE) @Override - public boolean createSparkCluster(UserInfo userInfo, SparkStandaloneClusterCreateForm form, @Project String project) { - + public boolean createSparkCluster(@User UserInfo userInfo, @ResourceName String resourceName, SparkStandaloneClusterCreateForm form, @Project String project, + @Info List<String> auditInfo) { final ProjectDTO projectDTO = projectService.get(project); final UserInstanceDTO instance = exploratoryDAO.fetchExploratoryFields(userInfo.getName(), project, form.getNotebookName()); @@ -176,44 +184,39 @@ public class ComputationalServiceImpl implements ComputationalService { } } + @Audit(action = TERMINATE_COMPUTATIONAL) @Override - public void terminateComputational(UserInfo userInfo, String project, String exploratoryName, String computationalName) { + public void terminateComputational(@User UserInfo userInfo, String resourceCreator, String project, String exploratoryName, @ResourceName String computationalName, + @Info List<String> auditInfo) { try { + updateComputationalStatus(resourceCreator, project, exploratoryName, computationalName, TERMINATING); - updateComputationalStatus(userInfo.getName(), project, exploratoryName, computationalName, TERMINATING); - - final UserInstanceDTO userInstanceDTO = exploratoryDAO.fetchExploratoryFields(userInfo.getName(), project, - exploratoryName); - UserComputationalResource compResource = computationalDAO.fetchComputationalFields(userInfo.getName(), project, - exploratoryName, computationalName); + final UserInstanceDTO userInstanceDTO = exploratoryDAO.fetchExploratoryFields(resourceCreator, project, exploratoryName); + UserComputationalResource compResource = computationalDAO.fetchComputationalFields(resourceCreator, project, exploratoryName, computationalName); final DataEngineType dataEngineType = compResource.getDataEngineType(); EndpointDTO endpointDTO = endpointService.get(userInstanceDTO.getEndpoint()); - ComputationalTerminateDTO dto = requestBuilder.newComputationalTerminate(userInfo, userInstanceDTO, compResource, endpointDTO); + ComputationalTerminateDTO dto = requestBuilder.newComputationalTerminate(resourceCreator, userInstanceDTO, compResource, endpointDTO); final String provisioningUrl = Optional.ofNullable(DATA_ENGINE_TYPE_TERMINATE_URLS.get(dataEngineType)) .orElseThrow(UnsupportedOperationException::new); - String uuid = - provisioningService.post(endpointDTO.getUrl() + provisioningUrl, - userInfo.getAccessToken(), dto, String.class); - requestId.put(userInfo.getName(), uuid); + final String uuid = provisioningService.post(endpointDTO.getUrl() + provisioningUrl, userInfo.getAccessToken(), dto, String.class); + requestId.put(resourceCreator, uuid); } catch (RuntimeException re) { - try { - updateComputationalStatus(userInfo.getName(), project, exploratoryName, computationalName, FAILED); + updateComputationalStatus(resourceCreator, project, exploratoryName, computationalName, FAILED); } catch (DlabException e) { - log.error(COULD_NOT_UPDATE_THE_STATUS_MSG_FORMAT, computationalName, userInfo.getName(), e); + log.error(COULD_NOT_UPDATE_THE_STATUS_MSG_FORMAT, computationalName, resourceCreator, e); } - throw re; } } @BudgetLimited + @Audit(action = CREATE_DATA_ENGINE_SERVICE) @Override - public boolean createDataEngineService(UserInfo userInfo, ComputationalCreateFormDTO formDTO, - UserComputationalResource computationalResource, @Project String project) { - + public boolean createDataEngineService(@User UserInfo userInfo, @ResourceName String resourceName, ComputationalCreateFormDTO formDTO, + UserComputationalResource computationalResource, @Project String project, @Info List<String> auditInfo) { final ProjectDTO projectDTO = projectService.get(project); final UserInstanceDTO instance = exploratoryDAO.fetchExploratoryFields(userInfo.getName(), project, formDTO .getNotebookName()); diff --git a/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/EnvironmentServiceImpl.java b/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/EnvironmentServiceImpl.java index eb04e55..a396859 100644 --- a/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/EnvironmentServiceImpl.java +++ b/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/EnvironmentServiceImpl.java @@ -51,6 +51,7 @@ import java.util.stream.Stream; import static com.epam.dlab.backendapi.resources.dto.UserDTO.Status.ACTIVE; import static com.epam.dlab.backendapi.resources.dto.UserDTO.Status.NOT_ACTIVE; +import static com.epam.dlab.rest.contracts.ComputationalAPI.AUDIT_MESSAGE; import static java.util.stream.Collectors.toList; @Singleton @@ -161,10 +162,9 @@ public class EnvironmentServiceImpl implements EnvironmentService { @ProjectAdmin @Override - public void terminateComputational(@User UserInfo userInfo, String user, @Project String project, - String exploratoryName, String computationalName) { - computationalService.terminateComputational(new UserInfo(user, userInfo.getAccessToken()), project, exploratoryName, - computationalName); + public void terminateComputational(@User UserInfo userInfo, String user, @Project String project, String exploratoryName, String computationalName) { + computationalService.terminateComputational(userInfo, user, project, exploratoryName, computationalName, + Collections.singletonList(String.format(AUDIT_MESSAGE, exploratoryName))); } private UserDTO toUserDTO(String u, UserDTO.Status status) { diff --git a/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/SchedulerJobServiceImpl.java b/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/SchedulerJobServiceImpl.java index e00a288..37deea6 100644 --- a/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/SchedulerJobServiceImpl.java +++ b/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/SchedulerJobServiceImpl.java @@ -223,13 +223,13 @@ public class SchedulerJobServiceImpl implements SchedulerJobService { } private void terminateComputational(SchedulerJobData job) { - final String user = job.getUser(); - final String expName = job.getExploratoryName(); - final String compName = job.getComputationalName(); - final UserInfo userInfo = securityService.getServiceAccountInfo(user); - log.debug("Terminating exploratory {} computational {} for user {} by scheduler", expName, compName, user); - computationalService.terminateComputational(userInfo, job.getProject(), expName, compName); - } + final String user = job.getUser(); + final String expName = job.getExploratoryName(); + final String compName = job.getComputationalName(); + final UserInfo userInfo = securityService.getServiceAccountInfo(user); + log.debug("Terminating exploratory {} computational {} for user {} by scheduler", expName, compName, user); + computationalService.terminateComputational(userInfo, user, job.getProject(), expName, compName, Collections.singletonList(AUDIT_MESSAGE)); + } private void stopExploratory(SchedulerJobData job) { final String expName = job.getExploratoryName(); diff --git a/services/self-service/src/main/java/com/epam/dlab/backendapi/util/RequestBuilder.java b/services/self-service/src/main/java/com/epam/dlab/backendapi/util/RequestBuilder.java index 32bea28..a72db8d 100644 --- a/services/self-service/src/main/java/com/epam/dlab/backendapi/util/RequestBuilder.java +++ b/services/self-service/src/main/java/com/epam/dlab/backendapi/util/RequestBuilder.java @@ -444,36 +444,36 @@ public class RequestBuilder { } @SuppressWarnings("unchecked") - public <T extends ComputationalBase<T>> T newComputationalTerminate(UserInfo userInfo, - UserInstanceDTO userInstanceDTO, - UserComputationalResource computationalResource, - EndpointDTO endpointDTO) { - T computationalTerminate; - CloudProvider cloudProvider = endpointDTO.getCloudProvider(); - switch (cloudProvider) { - case AWS: - AwsComputationalTerminateDTO terminateDTO = newResourceSysBaseDTO(userInfo.getName(), cloudProvider, - AwsComputationalTerminateDTO.class); - if (computationalResource.getDataEngineType() == DataEngineType.CLOUD_SERVICE) { - terminateDTO.setClusterName(computationalResource.getComputationalId()); - } - computationalTerminate = (T) terminateDTO; - break; - case AZURE: - computationalTerminate = (T) newResourceSysBaseDTO(userInfo.getName(), cloudProvider, ComputationalTerminateDTO.class); - break; - case GCP: - GcpComputationalTerminateDTO gcpTerminateDTO = newResourceSysBaseDTO(userInfo.getName(), cloudProvider, - GcpComputationalTerminateDTO.class); - if (computationalResource.getDataEngineType() == DataEngineType.CLOUD_SERVICE) { - gcpTerminateDTO.setClusterName(computationalResource.getComputationalId()); - } - computationalTerminate = (T) gcpTerminateDTO; - break; - - default: - throw new IllegalArgumentException(UNSUPPORTED_CLOUD_PROVIDER_MESSAGE + cloudProvider); - } + public <T extends ComputationalBase<T>> T newComputationalTerminate(String resourceCreator, + UserInstanceDTO userInstanceDTO, + UserComputationalResource computationalResource, + EndpointDTO endpointDTO) { + T computationalTerminate; + CloudProvider cloudProvider = endpointDTO.getCloudProvider(); + switch (cloudProvider) { + case AWS: + AwsComputationalTerminateDTO terminateDTO = newResourceSysBaseDTO(resourceCreator, cloudProvider, + AwsComputationalTerminateDTO.class); + if (computationalResource.getDataEngineType() == DataEngineType.CLOUD_SERVICE) { + terminateDTO.setClusterName(computationalResource.getComputationalId()); + } + computationalTerminate = (T) terminateDTO; + break; + case AZURE: + computationalTerminate = (T) newResourceSysBaseDTO(resourceCreator, cloudProvider, ComputationalTerminateDTO.class); + break; + case GCP: + GcpComputationalTerminateDTO gcpTerminateDTO = newResourceSysBaseDTO(resourceCreator, cloudProvider, + GcpComputationalTerminateDTO.class); + if (computationalResource.getDataEngineType() == DataEngineType.CLOUD_SERVICE) { + gcpTerminateDTO.setClusterName(computationalResource.getComputationalId()); + } + computationalTerminate = (T) gcpTerminateDTO; + break; + + default: + throw new IllegalArgumentException(UNSUPPORTED_CLOUD_PROVIDER_MESSAGE + cloudProvider); + } return computationalTerminate .withExploratoryName(userInstanceDTO.getExploratoryName()) diff --git a/services/self-service/src/test/java/com/epam/dlab/backendapi/resources/ExploratoryResourceTest.java b/services/self-service/src/test/java/com/epam/dlab/backendapi/resources/ExploratoryResourceTest.java index a3e1dbd..a9ab5f2 100644 --- a/services/self-service/src/test/java/com/epam/dlab/backendapi/resources/ExploratoryResourceTest.java +++ b/services/self-service/src/test/java/com/epam/dlab/backendapi/resources/ExploratoryResourceTest.java @@ -144,22 +144,6 @@ public class ExploratoryResourceTest extends TestBase { } @Test - public void startWithFailedAuth() throws AuthenticationException { - authFailSetup(); - when(exploratoryService.start(any(UserInfo.class), anyString(), anyString(), anyList())).thenReturn("someUuid"); - final Response response = resources.getJerseyTest() - .target("/infrastructure_provision/exploratory_environment") - .request() - .header("Authorization", "Bearer " + TOKEN) - .post(Entity.json(getEmptyExploratoryActionFormDTO())); - - assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatus()); - assertEquals(MediaType.APPLICATION_JSON, response.getHeaderString(HttpHeaders.CONTENT_TYPE)); - - verifyZeroInteractions(exploratoryService); - } - - @Test public void stop() { when(exploratoryService.stop(any(UserInfo.class), anyString(), anyString(), anyString(), anyList())).thenReturn("someUuid"); final Response response = resources.getJerseyTest() diff --git a/services/self-service/src/test/java/com/epam/dlab/backendapi/service/impl/ComputationalServiceImplTest.java b/services/self-service/src/test/java/com/epam/dlab/backendapi/service/impl/ComputationalServiceImplTest.java index 74fc7f0..1238e51 100644 --- a/services/self-service/src/test/java/com/epam/dlab/backendapi/service/impl/ComputationalServiceImplTest.java +++ b/services/self-service/src/test/java/com/epam/dlab/backendapi/service/impl/ComputationalServiceImplTest.java @@ -70,6 +70,7 @@ import java.util.Optional; import static com.epam.dlab.dto.UserInstanceStatus.CREATING; import static com.epam.dlab.dto.UserInstanceStatus.RUNNING; import static com.epam.dlab.dto.UserInstanceStatus.STOPPED; +import static com.epam.dlab.rest.contracts.ComputationalAPI.AUDIT_MESSAGE; import static java.util.Collections.singletonList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -169,9 +170,9 @@ public class ComputationalServiceImplTest { when(provisioningService.post(anyString(), anyString(), any(ComputationalBase.class), any())).thenReturn(UUID); when(requestId.put(anyString(), anyString())).thenReturn(UUID); - SparkStandaloneClusterCreateForm sparkClusterCreateForm = (SparkStandaloneClusterCreateForm) formList.get(0); - boolean creationResult = - computationalService.createSparkCluster(userInfo, sparkClusterCreateForm, PROJECT); + SparkStandaloneClusterCreateForm form = (SparkStandaloneClusterCreateForm) formList.get(0); + boolean creationResult = computationalService.createSparkCluster(userInfo, form.getName(), form, PROJECT, + Collections.singletonList(String.format(AUDIT_MESSAGE, form.getNotebookName()))); assertTrue(creationResult); verify(projectService).get(PROJECT); @@ -179,7 +180,7 @@ public class ComputationalServiceImplTest { verify(exploratoryDAO).fetchExploratoryFields(USER, PROJECT, EXPLORATORY_NAME); verify(requestBuilder).newComputationalCreate( - refEq(userInfo), refEq(projectDTO), refEq(userInstance), refEq(sparkClusterCreateForm), refEq(endpointDTO())); + refEq(userInfo), refEq(projectDTO), refEq(userInstance), refEq(form), refEq(endpointDTO())); verify(provisioningService) .post(endpointDTO().getUrl() + ComputationalAPI.COMPUTATIONAL_CREATE_SPARK, TOKEN, compBaseMocked, @@ -195,11 +196,10 @@ public class ComputationalServiceImplTest { any(SparkStandaloneClusterResource.class))).thenReturn(false); when(exploratoryDAO.fetchExploratoryFields(anyString(), anyString(), anyString())).thenReturn(userInstance); - - boolean creationResult = computationalService.createSparkCluster(userInfo, (SparkStandaloneClusterCreateForm) formList.get(0), - PROJECT); + SparkStandaloneClusterCreateForm form = (SparkStandaloneClusterCreateForm) formList.get(0); + boolean creationResult = computationalService.createSparkCluster(userInfo, form.getName(), form, PROJECT, + Collections.singletonList(String.format(AUDIT_MESSAGE, form.getNotebookName()))); assertFalse(creationResult); - verify(computationalDAO).addComputational(eq(USER), eq(EXPLORATORY_NAME), eq(PROJECT), refEq(sparkClusterResource)); verifyNoMoreInteractions(configuration, computationalDAO); } @@ -214,9 +214,9 @@ public class ComputationalServiceImplTest { when(computationalDAO.updateComputationalStatus(any(ComputationalStatusDTO.class))) .thenReturn(mock(UpdateResult.class)); - SparkStandaloneClusterCreateForm sparkClusterCreateForm = (SparkStandaloneClusterCreateForm) formList.get(0); + SparkStandaloneClusterCreateForm form = (SparkStandaloneClusterCreateForm) formList.get(0); try { - computationalService.createSparkCluster(userInfo, sparkClusterCreateForm, PROJECT); + computationalService.createSparkCluster(userInfo, form.getName(), form, PROJECT, Collections.singletonList(String.format(AUDIT_MESSAGE, form.getNotebookName()))); } catch (ResourceNotFoundException e) { assertEquals("Exploratory for user with name not found", e.getMessage()); } @@ -244,9 +244,9 @@ public class ComputationalServiceImplTest { when(computationalDAO.updateComputationalStatus(any(ComputationalStatusDTO.class))) .thenReturn(mock(UpdateResult.class)); - SparkStandaloneClusterCreateForm sparkClusterCreateForm = (SparkStandaloneClusterCreateForm) formList.get(0); + SparkStandaloneClusterCreateForm form = (SparkStandaloneClusterCreateForm) formList.get(0); try { - computationalService.createSparkCluster(userInfo, sparkClusterCreateForm, PROJECT); + computationalService.createSparkCluster(userInfo, form.getName(), form, PROJECT, Collections.singletonList(String.format(AUDIT_MESSAGE, form.getNotebookName()))); } catch (DlabException e) { assertEquals("Cannot create instance of resource class ", e.getMessage()); } @@ -254,7 +254,7 @@ public class ComputationalServiceImplTest { verify(computationalDAO).addComputational(USER, EXPLORATORY_NAME, PROJECT, sparkClusterResource); verify(computationalDAO).updateComputationalStatus(refEq(computationalStatusDTOWithStatusFailed, "self")); verify(exploratoryDAO).fetchExploratoryFields(USER, PROJECT, EXPLORATORY_NAME); - verify(requestBuilder).newComputationalCreate(userInfo, projectDTO, userInstance, sparkClusterCreateForm, endpointDTO()); + verify(requestBuilder).newComputationalCreate(userInfo, projectDTO, userInstance, form, endpointDTO()); verifyNoMoreInteractions(projectService, configuration, computationalDAO, exploratoryDAO, requestBuilder); } @@ -276,19 +276,19 @@ public class ComputationalServiceImplTest { ComputationalTerminateDTO ctDto = new ComputationalTerminateDTO(); ctDto.setComputationalName(COMP_NAME); ctDto.setExploratoryName(EXPLORATORY_NAME); - when(requestBuilder.newComputationalTerminate(any(UserInfo.class), any(UserInstanceDTO.class), + when(requestBuilder.newComputationalTerminate(anyString(), any(UserInstanceDTO.class), any(UserComputationalResource.class), any(EndpointDTO.class))).thenReturn(ctDto); when(provisioningService.post(anyString(), anyString(), any(ComputationalTerminateDTO.class), any())) .thenReturn(UUID); when(requestId.put(anyString(), anyString())).thenReturn(UUID); - computationalService.terminateComputational(userInfo, PROJECT, EXPLORATORY_NAME, COMP_NAME); + computationalService.terminateComputational(userInfo, userInfo.getName(), PROJECT, EXPLORATORY_NAME, COMP_NAME, Collections.singletonList(AUDIT_MESSAGE)); verify(computationalDAO).updateComputationalStatus(refEq(computationalStatusDTOWithStatusTerminating, "self")); verify(computationalDAO).fetchComputationalFields(USER, PROJECT, EXPLORATORY_NAME, COMP_NAME); - verify(requestBuilder).newComputationalTerminate(userInfo, userInstance, ucResource, endpointDTO()); + verify(requestBuilder).newComputationalTerminate(userInfo.getName(), userInstance, ucResource, endpointDTO()); verify(provisioningService).post(endpointDTO().getUrl() + ComputationalAPI.COMPUTATIONAL_TERMINATE_CLOUD_SPECIFIC, TOKEN, ctDto, String.class); @@ -308,7 +308,7 @@ public class ComputationalServiceImplTest { .thenReturn(mock(UpdateResult.class)); try { - computationalService.terminateComputational(userInfo, PROJECT, EXPLORATORY_NAME, COMP_NAME); + computationalService.terminateComputational(userInfo, userInfo.getName(), PROJECT, EXPLORATORY_NAME, COMP_NAME, Collections.singletonList(AUDIT_MESSAGE)); } catch (DlabException e) { assertEquals("Could not update computational resource status", e.getMessage()); } @@ -329,7 +329,7 @@ public class ComputationalServiceImplTest { .thenReturn(mock(UpdateResult.class)); try { - computationalService.terminateComputational(userInfo, PROJECT, EXPLORATORY_NAME, COMP_NAME); + computationalService.terminateComputational(userInfo, userInfo.getName(), PROJECT, EXPLORATORY_NAME, COMP_NAME, Collections.singletonList(AUDIT_MESSAGE)); } catch (DlabException e) { assertEquals("Computational resource for user with exploratory name not found.", e.getMessage()); } @@ -356,14 +356,14 @@ public class ComputationalServiceImplTest { when(computationalDAO.fetchComputationalFields(anyString(), anyString(), anyString(), anyString())).thenReturn(ucResource); doThrow(new DlabException("Cannot create instance of resource class ")) - .when(requestBuilder).newComputationalTerminate(any(UserInfo.class), any(UserInstanceDTO.class), + .when(requestBuilder).newComputationalTerminate(anyString(), any(UserInstanceDTO.class), any(UserComputationalResource.class), any(EndpointDTO.class)); when(computationalDAO.updateComputationalStatus(any(ComputationalStatusDTO.class))) .thenReturn(mock(UpdateResult.class)); try { - computationalService.terminateComputational(userInfo, PROJECT, EXPLORATORY_NAME, COMP_NAME); + computationalService.terminateComputational(userInfo, userInfo.getName(), PROJECT, EXPLORATORY_NAME, COMP_NAME, Collections.singletonList(AUDIT_MESSAGE)); } catch (DlabException e) { assertEquals("Cannot create instance of resource class ", e.getMessage()); } @@ -373,7 +373,7 @@ public class ComputationalServiceImplTest { verify(exploratoryDAO).fetchExploratoryFields(USER, PROJECT, EXPLORATORY_NAME); - verify(requestBuilder).newComputationalTerminate(userInfo, userInstance, ucResource, endpointDTO()); + verify(requestBuilder).newComputationalTerminate(userInfo.getName(), userInstance, ucResource, endpointDTO()); verify(computationalDAO).updateComputationalStatus(refEq(computationalStatusDTOWithStatusFailed, "self")); verifyNoMoreInteractions(computationalDAO, exploratoryDAO, requestBuilder); } @@ -395,8 +395,9 @@ public class ComputationalServiceImplTest { when(provisioningService.post(anyString(), anyString(), any(ComputationalBase.class), any())).thenReturn(UUID); when(requestId.put(anyString(), anyString())).thenReturn(UUID); - boolean creationResult = - computationalService.createDataEngineService(userInfo, formList.get(1), ucResource, PROJECT); + ComputationalCreateFormDTO form = formList.get(1); + boolean creationResult = computationalService.createDataEngineService(userInfo, form.getName(), form, ucResource, PROJECT, + Collections.singletonList(String.format(AUDIT_MESSAGE, form.getNotebookName()))); assertTrue(creationResult); verify(projectService).get(PROJECT); @@ -422,8 +423,9 @@ public class ComputationalServiceImplTest { when(computationalDAO.addComputational(anyString(), anyString(), any(), any(UserComputationalResource.class))) .thenReturn(false); - boolean creationResult = computationalService.createDataEngineService(userInfo, formList.get(1), ucResource, - PROJECT); + ComputationalCreateFormDTO form = formList.get(1); + boolean creationResult = computationalService.createDataEngineService(userInfo, form.getName(), form, ucResource, PROJECT, + Collections.singletonList(String.format(AUDIT_MESSAGE, form.getNotebookName()))); assertFalse(creationResult); verify(computationalDAO).addComputational(eq(USER), eq(EXPLORATORY_NAME), eq(PROJECT), refEq(ucResource)); @@ -440,8 +442,10 @@ public class ComputationalServiceImplTest { when(computationalDAO.updateComputationalStatus(any(ComputationalStatusDTO.class))) .thenReturn(mock(UpdateResult.class)); + ComputationalCreateFormDTO form = formList.get(1); try { - computationalService.createDataEngineService(userInfo, formList.get(1), ucResource, PROJECT); + computationalService.createDataEngineService(userInfo, form.getName(), form, ucResource, PROJECT, + Collections.singletonList(String.format(AUDIT_MESSAGE, form.getNotebookName()))); } catch (DlabException e) { assertEquals("Exploratory for user with name not found", e.getMessage()); } @@ -472,9 +476,10 @@ public class ComputationalServiceImplTest { when(computationalDAO.updateComputationalStatus(any(ComputationalStatusDTO.class))) .thenReturn(mock(UpdateResult.class)); - ComputationalCreateFormDTO computationalCreateFormDTO = formList.get(1); + ComputationalCreateFormDTO form = formList.get(1); try { - computationalService.createDataEngineService(userInfo, computationalCreateFormDTO, ucResource, PROJECT); + computationalService.createDataEngineService(userInfo, form.getName(), form, ucResource, PROJECT, + Collections.singletonList(String.format(AUDIT_MESSAGE, form.getNotebookName()))); } catch (DlabException e) { assertEquals("Could not send request for creation the computational resource compName: " + "Cannot create instance of resource class ", e.getMessage()); @@ -484,7 +489,7 @@ public class ComputationalServiceImplTest { verify(computationalDAO).addComputational(eq(USER), eq(EXPLORATORY_NAME), eq(PROJECT), refEq(ucResource)); verify(exploratoryDAO).fetchExploratoryFields(USER, PROJECT, EXPLORATORY_NAME); verify(requestBuilder).newComputationalCreate( - refEq(userInfo), refEq(projectDTO), refEq(userInstance), refEq(computationalCreateFormDTO), refEq(endpointDTO())); + refEq(userInfo), refEq(projectDTO), refEq(userInstance), refEq(form), refEq(endpointDTO())); verify(computationalDAO).updateComputationalStatus(refEq(computationalStatusDTOWithStatusFailed, "self")); verifyNoMoreInteractions(projectService, computationalDAO, exploratoryDAO, requestBuilder); diff --git a/services/self-service/src/test/java/com/epam/dlab/backendapi/service/impl/EnvironmentServiceImplTest.java b/services/self-service/src/test/java/com/epam/dlab/backendapi/service/impl/EnvironmentServiceImplTest.java index 7bde7a6..ce80c08 100644 --- a/services/self-service/src/test/java/com/epam/dlab/backendapi/service/impl/EnvironmentServiceImplTest.java +++ b/services/self-service/src/test/java/com/epam/dlab/backendapi/service/impl/EnvironmentServiceImplTest.java @@ -65,6 +65,7 @@ import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class EnvironmentServiceImplTest { private static final String AUDIT_QUOTA_MESSAGE = "Billing quota reached"; + private static final String AUDIT_MESSAGE = "Notebook name: %s"; private static final String DLAB_SYSTEM_USER = "DLab system user"; private static final String USER = "test"; private static final String EXPLORATORY_NAME_1 = "expName1"; @@ -187,12 +188,12 @@ public class EnvironmentServiceImplTest { public void terminateComputational() { final UserInfo userInfo = getUserInfo(); doNothing().when(computationalService) - .terminateComputational(any(UserInfo.class), anyString(), anyString(), anyString()); + .terminateComputational(any(UserInfo.class), anyString(), anyString(), anyString(), anyString(), anyList()); environmentService.terminateComputational(userInfo, USER, PROJECT_NAME, EXPLORATORY_NAME_1, "compName"); - verify(computationalService) - .terminateComputational(refEq(userInfo), eq(PROJECT_NAME), eq(EXPLORATORY_NAME_1), eq("compName")); + verify(computationalService).terminateComputational(refEq(userInfo), eq(userInfo.getName()), eq(PROJECT_NAME), eq(EXPLORATORY_NAME_1), eq("compName"), + eq(Collections.singletonList(String.format(AUDIT_MESSAGE, EXPLORATORY_NAME_1)))); verifyNoMoreInteractions(securityService, computationalService); } diff --git a/services/self-service/src/test/java/com/epam/dlab/backendapi/service/impl/SchedulerJobServiceImplTest.java b/services/self-service/src/test/java/com/epam/dlab/backendapi/service/impl/SchedulerJobServiceImplTest.java index 2b535fe..5b9b738 100644 --- a/services/self-service/src/test/java/com/epam/dlab/backendapi/service/impl/SchedulerJobServiceImplTest.java +++ b/services/self-service/src/test/java/com/epam/dlab/backendapi/service/impl/SchedulerJobServiceImplTest.java @@ -866,27 +866,28 @@ public class SchedulerJobServiceImplTest { @Test public void testTerminateComputationalByScheduler() { - final List<DayOfWeek> stopDays = Arrays.asList(DayOfWeek.values()); - final List<DayOfWeek> startDays = Arrays.asList(DayOfWeek.values()); - final LocalDateTime terminateDateTime = LocalDateTime.of(LocalDate.now(), - LocalTime.now().truncatedTo(ChronoUnit.MINUTES)); - final LocalDate finishDate = LocalDate.now().plusDays(1); - final SchedulerJobData schedulerJobData = getSchedulerJobData(LocalDate.now(), finishDate, startDays, stopDays - , terminateDateTime, false, USER, LocalTime.now().truncatedTo(ChronoUnit.MINUTES) - ); - when(schedulerJobDAO.getComputationalSchedulerDataWithOneOfStatus(any(UserInstanceStatus.class), - anyVararg())).thenReturn(singletonList(schedulerJobData)); - when(securityService.getServiceAccountInfo(anyString())).thenReturn(getUserInfo()); - - schedulerJobService.terminateComputationalByScheduler(); - - verify(securityService).getServiceAccountInfo(USER); - verify(schedulerJobDAO) - .getComputationalSchedulerDataWithOneOfStatus(RUNNING, STOPPED, RUNNING); - verify(computationalService).terminateComputational(refEq(getUserInfo()), eq(PROJECT), - eq(EXPLORATORY_NAME), eq(COMPUTATIONAL_NAME)); - verifyNoMoreInteractions(securityService, schedulerJobDAO, computationalService); - } + final List<DayOfWeek> stopDays = Arrays.asList(DayOfWeek.values()); + final List<DayOfWeek> startDays = Arrays.asList(DayOfWeek.values()); + final LocalDateTime terminateDateTime = LocalDateTime.of(LocalDate.now(), + LocalTime.now().truncatedTo(ChronoUnit.MINUTES)); + final LocalDate finishDate = LocalDate.now().plusDays(1); + final SchedulerJobData schedulerJobData = getSchedulerJobData(LocalDate.now(), finishDate, startDays, stopDays + , terminateDateTime, false, USER, LocalTime.now().truncatedTo(ChronoUnit.MINUTES) + ); + when(schedulerJobDAO.getComputationalSchedulerDataWithOneOfStatus(any(UserInstanceStatus.class), + anyVararg())).thenReturn(singletonList(schedulerJobData)); + UserInfo userInfo = getUserInfo(); + when(securityService.getServiceAccountInfo(anyString())).thenReturn(userInfo); + + schedulerJobService.terminateComputationalByScheduler(); + + verify(securityService).getServiceAccountInfo(USER); + verify(schedulerJobDAO) + .getComputationalSchedulerDataWithOneOfStatus(RUNNING, STOPPED, RUNNING); + verify(computationalService).terminateComputational(refEq(userInfo), eq(userInfo.getName()), eq(PROJECT), eq(EXPLORATORY_NAME), eq(COMPUTATIONAL_NAME), + eq(Collections.singletonList(AUDIT_MESSAGE))); + verifyNoMoreInteractions(securityService, schedulerJobDAO, computationalService); + } @Test public void testTerminateComputationalBySchedulerWhenSchedulerIsNotConfigured() { --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@dlab.apache.org For additional commands, e-mail: commits-h...@dlab.apache.org