/** * 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.falcon.resource; import org.apache.commons.beanutils.PropertyUtils; import org.apache.commons.lang.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.falcon.FalconException; import org.apache.falcon.FalconRuntimException; import org.apache.falcon.FalconWebException; import org.apache.falcon.Pair; import org.apache.falcon.entity.EntityNotRegisteredException; import org.apache.falcon.entity.EntityUtil; import org.apache.falcon.entity.lock.MemoryLocks; import org.apache.falcon.entity.parser.EntityParser; import org.apache.falcon.entity.parser.EntityParserFactory; import org.apache.falcon.entity.parser.ValidationException; import org.apache.falcon.entity.store.ConfigurationStore; import org.apache.falcon.entity.store.EntityAlreadyExistsException; import org.apache.falcon.entity.store.FeedLocationStore; import org.apache.falcon.entity.v0.Entity; import org.apache.falcon.entity.v0.EntityGraph; import org.apache.falcon.entity.v0.EntityIntegrityChecker; import org.apache.falcon.entity.v0.EntityType; import org.apache.falcon.entity.v0.cluster.Cluster; import org.apache.falcon.entity.v0.datasource.Datasource; import org.apache.falcon.entity.v0.feed.Clusters; import org.apache.falcon.entity.v0.feed.ClusterType; import org.apache.falcon.entity.v0.feed.Feed; import org.apache.falcon.entity.v0.process.Process; import org.apache.falcon.resource.APIResult.Status; import org.apache.falcon.resource.EntityList.EntityElement; import org.apache.falcon.resource.metadata.AbstractMetadataResource; import org.apache.falcon.security.CurrentUser; import org.apache.falcon.security.DefaultAuthorizationProvider; import org.apache.falcon.security.SecurityUtil; import org.apache.falcon.util.DeploymentUtil; import org.apache.falcon.util.RuntimeProperties; import org.apache.falcon.util.StartupProperties; import org.apache.falcon.workflow.WorkflowEngineFactory; import org.apache.falcon.workflow.engine.AbstractWorkflowEngine; import org.apache.hadoop.io.IOUtils; import org.apache.hadoop.security.UserGroupInformation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.core.Response; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; /** * A base class for managing Entity operations. */ public abstract class AbstractEntityManager extends AbstractMetadataResource { private static final Logger LOG = LoggerFactory.getLogger(AbstractEntityManager.class); private static MemoryLocks memoryLocks = MemoryLocks.getInstance(); protected static final String DO_AS_PARAM = "doAs"; protected static final int XML_DEBUG_LEN = 10 * 1024; protected ConfigurationStore configStore = ConfigurationStore.get(); public AbstractEntityManager() { } protected static Integer getDefaultResultsPerPage() { Integer result = 10; final String key = "webservices.default.results.per.page"; String value = RuntimeProperties.get().getProperty(key, result.toString()); try { result = Integer.valueOf(value); } catch (NumberFormatException e) { LOG.warn("Invalid value:{} for key:{} in runtime.properties", value, key); } return result; } protected static void checkColo(String colo) { if (DeploymentUtil.isEmbeddedMode()) { return; } if (StringUtils.isNotEmpty(colo) && !colo.equals("*")) { if (!DeploymentUtil.getCurrentColo().equals(colo)) { throw FalconWebException.newAPIException("Current colo (" + DeploymentUtil.getCurrentColo() + ") is not " + colo); } } } public static Set<String> getAllColos() { if (DeploymentUtil.isEmbeddedMode()) { return DeploymentUtil.getDefaultColos(); } String[] colos = RuntimeProperties.get().getProperty("all.colos", DeploymentUtil.getDefaultColo()).split(","); for (int i = 0; i < colos.length; i++) { colos[i] = colos[i].trim(); } return new HashSet<String>(Arrays.asList(colos)); } protected Set<String> getColosFromExpression(String coloExpr, String type, String entity) { final Set<String> applicableColos = getApplicableColos(type, entity); return getColosFromExpression(coloExpr, applicableColos); } protected Set<String> getColosFromExpression(String coloExpr, String type, Entity entity) { final Set<String> applicableColos = getApplicableColos(type, entity); return getColosFromExpression(coloExpr, applicableColos); } private Set<String> getColosFromExpression(String coloExpr, Set<String> applicableColos) { Set<String> colos; if (coloExpr == null || coloExpr.equals("*") || coloExpr.isEmpty()) { colos = applicableColos; } else { colos = new HashSet<>(Arrays.asList(coloExpr.split(","))); if (!applicableColos.containsAll(colos)) { throw FalconWebException.newAPIException("Given colos not applicable for entity operation"); } } return colos; } public static Set<String> getApplicableColos(String type, String name) { try { if (DeploymentUtil.isEmbeddedMode()) { return DeploymentUtil.getDefaultColos(); } if (EntityType.getEnum(type) == EntityType.CLUSTER || name == null) { return getAllColos(); } return getApplicableColos(type, EntityUtil.getEntity(type, name)); } catch (FalconException e) { throw FalconWebException.newAPIException(e); } } public static Set<String> getApplicableColos(String type, Entity entity) { try { if (DeploymentUtil.isEmbeddedMode()) { return DeploymentUtil.getDefaultColos(); } if (EntityType.getEnum(type) == EntityType.CLUSTER) { return getAllColos(); } Set<String> clusters = EntityUtil.getClustersDefined(entity); Set<String> colos = new HashSet<String>(); for (String cluster : clusters) { Cluster clusterEntity = EntityUtil.getEntity(EntityType.CLUSTER, cluster); colos.add(clusterEntity.getColo()); } return colos; } catch (FalconException e) { throw FalconWebException.newAPIException(e); } } /** * Submit a new entity. Entities can be of type feed, process or data end * points. Entity definitions are validated structurally against schema and * subsequently for other rules before they are admitted into the system * <p/> * Entity name acts as the key and an entity once added, can't be added * again unless deleted. * * @param request - Servlet Request * @param type - entity type - feed, process or data end point * @param colo - applicable colo * @return result of the operation */ public APIResult submit(HttpServletRequest request, String type, String colo) { checkColo(colo); try { String doAsUser = request.getParameter(DO_AS_PARAM); Entity entity = submitInternal(request.getInputStream(), type, doAsUser); return new APIResult(APIResult.Status.SUCCEEDED, "Submit successful (" + type + ") " + entity.getName()); } catch (Throwable e) { LOG.error("Unable to persist entity object", e); throw FalconWebException.newAPIException(e); } } /** * Post an entity XML with entity type. Validates the XML which can be * Process, Feed or Data endpoint * * @param type entity type * @return APIResult -Succeeded or Failed */ public APIResult validate(HttpServletRequest request, String type, Boolean skipDryRun) { try { return validate(request.getInputStream(), type, skipDryRun); } catch (IOException e) { LOG.error("Unable to get InputStream from Request", request, e); throw FalconWebException.newAPIException(e); } } protected APIResult validate(InputStream inputStream, String type, Boolean skipDryRun) { try { EntityType entityType = EntityType.getEnum(type); Entity entity = deserializeEntity(inputStream, entityType); validate(entity); // Validate that the entity can be scheduled in the cluster. // Perform dryrun only if falcon is not in safemode. if (entity.getEntityType().isSchedulable() && !StartupProperties.isServerInSafeMode()) { Set<String> clusters = EntityUtil.getClustersDefinedInColos(entity); for (String cluster : clusters) { try { getWorkflowEngine(entity).dryRun(entity, cluster, skipDryRun); } catch (FalconException e) { throw new FalconException("dryRun failed on cluster " + cluster, e); } } } return new APIResult(APIResult.Status.SUCCEEDED, "Validated successfully (" + entityType + ") " + entity.getName()); } catch (Throwable e) { LOG.error("Validation failed for entity ({})", type, e); throw FalconWebException.newAPIException(e); } } /** * Deletes a scheduled entity, a deleted entity is removed completely from * execution pool. * * @param type entity type * @param entity entity name * @return APIResult */ public APIResult delete(HttpServletRequest request, String type, String entity, String colo) { return delete(type, entity, colo); } protected APIResult delete(String type, String entity, String colo) { checkColo(colo); List<Entity> tokenList = new ArrayList<>(); try { EntityType entityType = EntityType.getEnum(type); String removedFromEngine = ""; try { Entity entityObj = EntityUtil.getEntity(type, entity); verifySafemodeOperation(entityObj, EntityUtil.ENTITY_OPERATION.DELETE); canRemove(entityObj); obtainEntityLocks(entityObj, "delete", tokenList); if (entityType.isSchedulable() && !DeploymentUtil.isPrism()) { getWorkflowEngine(entityObj).delete(entityObj); removedFromEngine = "(KILLED in WF_ENGINE)"; } configStore.remove(entityType, entity); } catch (EntityNotRegisteredException e) { // already deleted return new APIResult(APIResult.Status.SUCCEEDED, entity + "(" + type + ") doesn't exist. Nothing to do"); } return new APIResult(APIResult.Status.SUCCEEDED, entity + "(" + type + ") removed successfully " + removedFromEngine); } catch (Throwable e) { LOG.error("Unable to reach workflow engine for deletion or deletion failed", e); throw FalconWebException.newAPIException(e); } finally { releaseEntityLocks(entity, tokenList); } } public APIResult update(HttpServletRequest request, String type, String entityName, String colo, Boolean skipDryRun) { try { return update(request.getInputStream(), type, entityName, colo, skipDryRun); } catch (IOException e) { LOG.error("Unable to get InputStream from Request", request, e); throw FalconWebException.newAPIException(e); } } protected APIResult update(InputStream inputStream, String type, String entityName, String colo, Boolean skipDryRun) { checkColo(colo); try { EntityType entityType = EntityType.getEnum(type); Entity entity = deserializeEntity(inputStream, entityType); verifySafemodeOperation(entity, EntityUtil.ENTITY_OPERATION.UPDATE); return update(entity, type, entityName, skipDryRun); } catch (FalconException e) { LOG.error("Update failed", e); throw FalconWebException.newAPIException(e, Response.Status.INTERNAL_SERVER_ERROR); } } protected APIResult update(Entity newEntity, String type, String entityName, Boolean skipDryRun) { List<Entity> tokenList = new ArrayList<>(); try { EntityType entityType = EntityType.getEnum(type); Entity oldEntity = EntityUtil.getEntity(type, entityName); // KLUDGE - Until ACL is mandated entity passed should be decorated for equals check to pass decorateEntityWithACL(newEntity); validate(newEntity); validateUpdate(oldEntity, newEntity); configStore.initiateUpdate(newEntity); obtainEntityLocks(oldEntity, "update", tokenList); StringBuilder result = new StringBuilder("Updated successfully"); switch(entityType) { case CLUSTER: configStore.update(entityType, newEntity); break; case DATASOURCE: configStore.update(entityType, newEntity); // check always if dependant feeds are already upgraded and upgrade accordingly if (entityType.equals(EntityType.DATASOURCE)) { ConfigurationStore.get().cleanupUpdateInit(); releaseEntityLocks(entityName, tokenList); updateDatasourceDependents(entityName, skipDryRun); } break; case FEED: case PROCESS: if (!DeploymentUtil.isPrism()) { Set<String> oldClusters = EntityUtil.getClustersDefinedInColos(oldEntity); Set<String> newClusters = EntityUtil.getClustersDefinedInColos(newEntity); newClusters.retainAll(oldClusters); //common clusters for update oldClusters.removeAll(newClusters); //deleted clusters for (String cluster : newClusters) { result.append(getWorkflowEngine(oldEntity).update(oldEntity, newEntity, cluster, skipDryRun)); } for (String cluster : oldClusters) { getWorkflowEngine(oldEntity).delete(oldEntity, cluster); } } configStore.update(entityType, newEntity); break; default: throw FalconWebException.newAPIException("Unknown entity type in update : " + entityType); } return new APIResult(APIResult.Status.SUCCEEDED, result.toString()); } catch (Throwable e) { LOG.error("Update failed", e); throw FalconWebException.newAPIException(e); } finally { ConfigurationStore.get().cleanupUpdateInit(); releaseEntityLocks(entityName, tokenList); } } /** * check if the data source entity dependent feeds are upgraded or not by checking against the data source entity * version and upgrade feeds accordingly. * * @param datasourceName Name of the data source entity * @param skipDryRun Skip dry run during update if set to true * @return APIResult * */ public APIResult updateDatasourceDependents(String datasourceName, Boolean skipDryRun) { try { Datasource datasource = EntityUtil.getEntity(EntityType.DATASOURCE, datasourceName); StringBuilder result = new StringBuilder(String.format("Updating feed entities " + "dependent on datasource : %s ", datasource.getName())); // get data source dependent entities and check the version referenced is same Pair<String, EntityType>[] dependentEntities = EntityIntegrityChecker.referencedBy(datasource); if (dependentEntities == null) { return new APIResult(APIResult.Status.SUCCEEDED, String.format("Datasource %s has " + "no dependent entities", datasourceName)); } for (Pair<String, EntityType> depEntity : dependentEntities) { Entity entity = EntityUtil.getEntity(depEntity.second, depEntity.first); if (entity.getEntityType() != EntityType.FEED) { throw FalconWebException.newAPIException("Datasource dependents should be FEEDS, but" + "encountered type : " + entity.getEntityType()); } Feed newFeed = (Feed) entity.copy(); for (org.apache.falcon.entity.v0.feed.Cluster feedCluster : newFeed.getClusters().getClusters()) { if (feedCluster.getType() == ClusterType.SOURCE) { boolean updatedFeed = isUpdateFeedDatasourceVersion(feedCluster, datasource, newFeed); if (updatedFeed) { // rewrite the dependent feed and update it on the store result.append(getWorkflowEngine(entity).update(entity, newFeed, feedCluster.getName(), skipDryRun)); updateEntityInConfigStore(entity, newFeed); } } } } return new APIResult(APIResult.Status.SUCCEEDED, result.toString()); } catch (FalconException e) { LOG.error("Update failed", e); throw FalconWebException.newAPIException(e, Response.Status.INTERNAL_SERVER_ERROR); } } private boolean isUpdateFeedDatasourceVersion(org.apache.falcon.entity.v0.feed.Cluster feedCluster, Datasource datasource, Feed feed) throws FalconException { org.apache.falcon.entity.v0.feed.Datasource updateFeedImp = incFeedDatasourceVersion(datasource, feed, feedCluster.getImport() != null ? feedCluster.getImport().getSource() : null); org.apache.falcon.entity.v0.feed.Datasource updateFeedExp = incFeedDatasourceVersion(datasource, feed, feedCluster.getExport() != null ? feedCluster.getExport().getTarget() : null); return ((updateFeedImp != null) || (updateFeedExp != null)); } private org.apache.falcon.entity.v0.feed.Datasource incFeedDatasourceVersion(Datasource datasource, Feed feed, org.apache.falcon.entity.v0.feed.Datasource depDatasource) throws FalconException { if ((depDatasource != null) && (datasource.getName().equals(depDatasource.getName()))) { if (depDatasource.getVersion() < datasource.getVersion()) { LOG.info(String.format("Updating since Feed '%s' referenced datasource '%s' " + "version '%d' < datasource entity version in store '%d'", feed.getName(), depDatasource.getName(), depDatasource.getVersion(), datasource.getVersion())); depDatasource.setVersion(depDatasource.getVersion()+1); return depDatasource; } else if (depDatasource.getVersion() > datasource.getVersion()) { throw new FalconException(String.format("Feed '%s' datasource '%s' version '%d' > datasource " + "entity version in store '%d'", feed.getName(), depDatasource.getName(), depDatasource.getVersion(), datasource.getVersion())); } } return null; } /** * Updates scheduled dependent entities of a cluster. * * @param clusterName Name of cluster * @param colo colo * @param skipDryRun Skip dry run during update if set to true * @return APIResult */ public APIResult updateClusterDependents(String clusterName, String colo, Boolean skipDryRun) { checkColo(colo); try { verifySuperUser(); Cluster cluster = EntityUtil.getEntity(EntityType.CLUSTER, clusterName); verifySafemodeOperation(cluster, EntityUtil.ENTITY_OPERATION.UPDATE_CLUSTER_DEPENDENTS); int clusterVersion = cluster.getVersion(); StringBuilder result = new StringBuilder("Updating entities dependent on cluster \n"); // get dependent entities. check if cluster version changed. if yes, update dependent entities Pair<String, EntityType>[] dependentEntities = EntityIntegrityChecker.referencedBy(cluster); if (dependentEntities == null) { // nothing to update return new APIResult(APIResult.Status.SUCCEEDED, "Cluster " + clusterName + " has no dependent entities"); } for (Pair<String, EntityType> depEntity : dependentEntities) { Entity entity = EntityUtil.getEntity(depEntity.second, depEntity.first); switch (entity.getEntityType()) { case FEED: Feed newFeedEntity = (Feed) entity.copy(); Clusters feedClusters = newFeedEntity.getClusters(); if (feedClusters != null) { boolean requireUpdate = false; for(org.apache.falcon.entity.v0.feed.Cluster feedCluster : feedClusters.getClusters()) { if (feedCluster.getName().equals(clusterName) && feedCluster.getVersion() != clusterVersion) { // update feed cluster entity feedCluster.setVersion(clusterVersion); requireUpdate = true; } } if (requireUpdate) { result.append(getWorkflowEngine(entity).update(entity, newFeedEntity, cluster.getName(), skipDryRun)); updateEntityInConfigStore(entity, newFeedEntity); } } break; case PROCESS: Process newProcessEntity = (Process) entity.copy(); org.apache.falcon.entity.v0.process.Clusters processClusters = newProcessEntity.getClusters(); if (processClusters != null) { boolean requireUpdate = false; for(org.apache.falcon.entity.v0.process.Cluster procCluster : processClusters.getClusters()) { if (procCluster.getName().equals(clusterName) && procCluster.getVersion() != clusterVersion) { // update feed cluster entity procCluster.setVersion(clusterVersion); requireUpdate = true; } } if (requireUpdate) { result.append(getWorkflowEngine(entity).update(entity, newProcessEntity, cluster.getName(), skipDryRun)); updateEntityInConfigStore(entity, newProcessEntity); } } break; default: break; } } return new APIResult(APIResult.Status.SUCCEEDED, result.toString()); } catch (Exception e) { LOG.error("Update failed", e); throw FalconWebException.newAPIException(e, Response.Status.INTERNAL_SERVER_ERROR); } } private void updateEntityInConfigStore(Entity oldEntity, Entity newEntity) { List<Entity> tokenList = new ArrayList<>(); try { configStore.initiateUpdate(newEntity); obtainEntityLocks(oldEntity, "update", tokenList); configStore.update(newEntity.getEntityType(), newEntity); } catch (Throwable e) { LOG.error("Update failed", e); throw FalconWebException.newAPIException(e); } finally { ConfigurationStore.get().cleanupUpdateInit(); releaseEntityLocks(oldEntity.getName(), tokenList); } } private void obtainEntityLocks(Entity entity, String command, List<Entity> tokenList) throws FalconException { //first obtain lock for the entity for which update is issued. if (memoryLocks.acquireLock(entity, command)) { tokenList.add(entity); } else { throw new FalconException(command + " command is already issued for " + entity.toShortString()); } //now obtain locks for all dependent entities if any. Set<Entity> affectedEntities = EntityGraph.get().getDependents(entity); for (Entity e : affectedEntities) { if (memoryLocks.acquireLock(e, command)) { tokenList.add(e); LOG.debug("{} on entity {} has acquired lock on {}", command, entity, e); } else { LOG.error("Error while trying to acquire lock on {}. Releasing already obtained locks", e.toShortString()); throw new FalconException("There are multiple update commands running for dependent entity " + e.toShortString()); } } } private void releaseEntityLocks(String entityName, List<Entity> tokenList) { if (tokenList != null && !tokenList.isEmpty()) { for (Entity entity : tokenList) { memoryLocks.releaseLock(entity); } LOG.info("All locks released on {}", entityName); } else { LOG.info("No locks to release on " + entityName); } } private void validateUpdate(Entity oldEntity, Entity newEntity) throws FalconException, IOException { if (oldEntity.getEntityType() != newEntity.getEntityType() || !oldEntity.equals(newEntity)) { throw new FalconException( oldEntity.toShortString() + " can't be updated with " + newEntity.toShortString()); } if (oldEntity.getEntityType() == EntityType.CLUSTER) { verifySuperUser(); } String[] props = oldEntity.getEntityType().getImmutableProperties(); for (String prop : props) { Object oldProp, newProp; try { oldProp = PropertyUtils.getProperty(oldEntity, prop); newProp = PropertyUtils.getProperty(newEntity, prop); } catch (Exception e) { throw new FalconException(e); } if (!ObjectUtils.equals(oldProp, newProp)) { throw new ValidationException(oldEntity.toShortString() + ": " + prop + " can't be changed"); } } } protected void canRemove(Entity entity) throws FalconException { Pair<String, EntityType>[] referencedBy = EntityIntegrityChecker.referencedBy(entity); if (referencedBy != null && referencedBy.length > 0) { StringBuilder messages = new StringBuilder(); for (Pair<String, EntityType> ref : referencedBy) { messages.append(ref).append("\n"); } throw new FalconException( entity.getName() + "(" + entity.getEntityType() + ") cant " + "be removed as it is referred by " + messages); } } protected Entity submitInternal(InputStream inputStream, String type, String doAsUser) throws IOException, FalconException { EntityType entityType = EntityType.getEnum(type); Entity entity = deserializeEntity(inputStream, entityType); verifySafemodeOperation(entity, EntityUtil.ENTITY_OPERATION.SUBMIT); submitInternal(entity, doAsUser); return entity; } protected void verifySafemodeOperation(Entity entity, EntityUtil.ENTITY_OPERATION operation) { // if Falcon not in safemode, allow everything except cluster update if (!StartupProperties.isServerInSafeMode()) { if (operation.equals(EntityUtil.ENTITY_OPERATION.UPDATE) && entity.getEntityType().equals(EntityType.CLUSTER)) { LOG.error("Entity operation {} is only allowed on cluster entities during safemode", operation.name()); throw FalconWebException.newAPIException("Entity operation " + operation.name() + " is only allowed on cluster entities during safemode"); } return; } switch (operation) { case UPDATE: if (entity.getEntityType().equals(EntityType.CLUSTER)) { return; } else { LOG.error("Entity operation {} is only allowed on cluster entities during safemode", operation.name()); throw FalconWebException.newAPIException("Entity operation " + operation.name() + " is only allowed on cluster entities during safemode"); } case SUSPEND: if (entity.getEntityType().equals(EntityType.CLUSTER)) { LOG.error("Entity operation {} is not allowed on cluster entity", operation.name()); throw FalconWebException.newAPIException("Entity operation " + operation.name() + " is not allowed on cluster entity"); } else { return; } case SCHEDULE: case UPDATE_CLUSTER_DEPENDENTS: case SUBMIT_AND_SCHEDULE: case DELETE: case RESUME: case TOUCH: case SUBMIT: default: LOG.error("Entity operation {} is not allowed during safemode", operation.name()); throw FalconWebException.newAPIException("Entity operation " + operation.name() + " not allowed during safemode"); } } protected synchronized void submitInternal(Entity entity, String doAsUser) throws IOException, FalconException { EntityType entityType = entity.getEntityType(); List<Entity> tokenList = new ArrayList<>(); // KLUDGE - Until ACL is mandated entity passed should be decorated for equals check to pass decorateEntityWithACL(entity); try { obtainEntityLocks(entity, "submit", tokenList); }finally { ConfigurationStore.get().cleanupUpdateInit(); releaseEntityLocks(entity.getName(), tokenList); } Entity existingEntity = configStore.get(entityType, entity.getName()); if (existingEntity != null) { if (EntityUtil.equals(existingEntity, entity)) { return; } throw new EntityAlreadyExistsException( entity.toShortString() + " already registered with configuration store. " + "Can't be submitted again. Try removing before submitting."); } SecurityUtil.tryProxy(entity, doAsUser); // proxy before validating since FS/Oozie needs to be proxied validate(entity); configStore.publish(entityType, entity); LOG.info("Submit successful: ({}): {}", entityType, entity.getName()); } /** * KLUDGE - Until ACL is mandated entity passed should be decorated for equals check to pass. * existingEntity in config store will have teh decoration and equals check fails * if entity passed is not decorated for checking if entity already exists. * * @param entity entity */ protected void decorateEntityWithACL(Entity entity) { if (SecurityUtil.isAuthorizationEnabled() || entity.getACL() != null) { return; // not necessary to decorate } final String proxyUser = CurrentUser.getUser(); final String defaultGroupName = CurrentUser.getPrimaryGroupName(); switch (entity.getEntityType()) { case CLUSTER: org.apache.falcon.entity.v0.cluster.ACL clusterACL = new org.apache.falcon.entity.v0.cluster.ACL(); clusterACL.setOwner(proxyUser); clusterACL.setGroup(defaultGroupName); ((org.apache.falcon.entity.v0.cluster.Cluster) entity).setACL(clusterACL); break; case FEED: org.apache.falcon.entity.v0.feed.ACL feedACL = new org.apache.falcon.entity.v0.feed.ACL(); feedACL.setOwner(proxyUser); feedACL.setGroup(defaultGroupName); ((org.apache.falcon.entity.v0.feed.Feed) entity).setACL(feedACL); break; case PROCESS: org.apache.falcon.entity.v0.process.ACL processACL = new org.apache.falcon.entity.v0.process.ACL(); processACL.setOwner(proxyUser); processACL.setGroup(defaultGroupName); ((org.apache.falcon.entity.v0.process.Process) entity).setACL(processACL); break; default: break; } } protected Entity deserializeEntity(InputStream xmlStream, EntityType entityType) throws FalconException { EntityParser<?> entityParser = EntityParserFactory.getParser(entityType); if (xmlStream.markSupported()) { xmlStream.mark(XML_DEBUG_LEN); // mark up to debug len } try { return entityParser.parse(xmlStream); } catch (FalconException e) { if (LOG.isDebugEnabled() && xmlStream.markSupported()) { try { xmlStream.reset(); String xmlData = getAsString(xmlStream); LOG.debug("XML DUMP for ({}): {}", entityType, xmlData, e); } catch (IOException ignore) { // ignore } } throw e; } } @SuppressWarnings({"unchecked", "rawtypes"}) protected void validate(Entity entity) throws FalconException { EntityParser entityParser = EntityParserFactory.getParser(entity.getEntityType()); entityParser.validate(entity); } private String getAsString(InputStream xmlStream) throws IOException { byte[] data = new byte[XML_DEBUG_LEN]; IOUtils.readFully(xmlStream, data, 0, XML_DEBUG_LEN); return new String(data); } /** * Enumeration of all possible status of an entity. */ public enum EntityStatus { SUBMITTED, SUSPENDED, RUNNING, COMPLETED } /** * Returns the status of requested entity. * * @param type entity type * @param entity entity name * @param showScheduler whether to return the scheduler on which the entity is scheduled. * @return String */ public APIResult getStatus(String type, String entity, String colo, Boolean showScheduler) { checkColo(colo); Entity entityObj; try { entityObj = EntityUtil.getEntity(type, entity); EntityType entityType = EntityType.getEnum(type); Pair<EntityStatus, String> status = getStatus(entityObj, entityType); String statusString = status.first.name(); return new APIResult(Status.SUCCEEDED, (status.first != EntityStatus.SUBMITTED && showScheduler != null && showScheduler) ? statusString + " (scheduled on " + status.second + ")" : statusString); } catch (FalconWebException e) { throw e; } catch (Exception e) { LOG.error("Unable to get status for entity {} ({})", entity, type, e); throw FalconWebException.newAPIException(e); } } protected Pair<EntityStatus, String> getStatus(Entity entity, EntityType type) throws FalconException { EntityStatus status = EntityStatus.SUBMITTED; AbstractWorkflowEngine workflowEngine = getWorkflowEngine(entity); if (type.isSchedulable()) { if (workflowEngine.isActive(entity)) { if (workflowEngine.isSuspended(entity)) { status = EntityStatus.SUSPENDED; } else { status = EntityStatus.RUNNING; } } else if (workflowEngine.isCompleted(entity)) { status = EntityStatus.COMPLETED; } } return new Pair<>(status, workflowEngine.getName()); } /** * Returns dependencies. * * @param type entity type * @param entityName entity name * @return EntityList */ public EntityList getDependencies(String type, String entityName) { try { Entity entityObj = EntityUtil.getEntity(type, entityName); return EntityUtil.getEntityDependencies(entityObj); } catch (Exception e) { LOG.error("Unable to get dependencies for entityName {} ({})", entityName, type, e); throw FalconWebException.newAPIException(e); } } //SUSPEND CHECKSTYLE CHECK ParameterNumberCheck /** * Returns the list of filtered entities as well as the total number of results. * * @param fieldStr Fields that the query is interested in, separated by comma * @param nameSubsequence Name subsequence to match * @param tagKeywords Tag keywords to match, separated by commma * @param filterType Only return entities of this type * @param filterTags Full tag matching, separated by comma * @param filterBy Specific fields to match (i.e. TYPE, NAME, STATUS, PIPELINES, CLUSTER) * @param orderBy Order result by these fields. * @param sortOrder Valid options are "asc" and “desc” * @param offset Pagination offset. * @param resultsPerPage Number of results that should be returned starting at the offset. * @return EntityList */ public EntityList getEntityList(String fieldStr, String nameSubsequence, String tagKeywords, String filterType, String filterTags, String filterBy, String orderBy, String sortOrder, Integer offset, Integer resultsPerPage, final String doAsUser) { return getEntityList(fieldStr, nameSubsequence, tagKeywords, filterType, filterTags, filterBy, orderBy, sortOrder, offset, resultsPerPage, doAsUser, false); } public EntityList getEntityList(String fieldStr, String nameSubsequence, String tagKeywords, String filterType, String filterTags, String filterBy, String orderBy, String sortOrder, Integer offset, Integer resultsPerPage, final String doAsUser, boolean isReturnAll) { HashSet<String> fields = new HashSet<String>(Arrays.asList(fieldStr.toUpperCase().split(","))); Map<String, List<String>> filterByFieldsValues = getFilterByFieldsValues(filterBy); for (String key : filterByFieldsValues.keySet()) { if (!key.toUpperCase().equals("NAME") && !key.toUpperCase().equals("CLUSTER")) { fields.add(key.toUpperCase()); } } try { // get filtered entities List<Entity> entities = getEntityList( nameSubsequence, tagKeywords, filterType, filterTags, filterBy, doAsUser); // sort entities and pagination List<Entity> entitiesReturn = sortEntitiesPagination( entities, orderBy, sortOrder, offset, resultsPerPage, isReturnAll); // add total number of results EntityList entityList = entitiesReturn.size() == 0 ? new EntityList(new Entity[]{}, 0) : new EntityList(buildEntityElements(new HashSet<String>(fields), entitiesReturn), entities.size()); return entityList; } catch (Exception e) { LOG.error("Failed to get entity list", e); throw FalconWebException.newAPIException(e); } } public List<Entity> getEntityList(String nameSubsequence, String tagKeywords, String filterType, String filterTags, String filterBy, final String doAsUser) throws FalconException, IOException { Map<String, List<String>> filterByFieldsValues = getFilterByFieldsValues(filterBy); validateEntityFilterByClause(filterByFieldsValues); if (StringUtils.isNotEmpty(filterTags)) { filterByFieldsValues.put(EntityList.EntityFilterByFields.TAGS.name(), Arrays.asList(filterTags)); } // get filtered entities List<Entity> entities = new ArrayList<Entity>(); if (StringUtils.isEmpty(filterType)) { // return entities of all types if no entity type specified for (EntityType entityType : EntityType.values()) { entities.addAll(getFilteredEntities( entityType, nameSubsequence, tagKeywords, filterByFieldsValues, "", "", "", doAsUser)); } } else { String[] types = filterType.split(","); for (String type : types) { EntityType entityType = EntityType.getEnum(type); entities.addAll(getFilteredEntities( entityType, nameSubsequence, tagKeywords, filterByFieldsValues, "", "", "", doAsUser)); } } return entities; } protected List<Entity> sortEntitiesPagination(List<Entity> entities, String orderBy, String sortOrder, Integer offset, Integer resultsPerPage) { return sortEntitiesPagination(entities, orderBy, sortOrder, offset, resultsPerPage, false); } protected List<Entity> sortEntitiesPagination(List<Entity> entities, String orderBy, String sortOrder, Integer offset, Integer resultsPerPage, boolean isReturnAll) { // sort entities entities = sortEntities(entities, orderBy, sortOrder); // pagination int pageCount = getRequiredNumberOfResults(entities.size(), offset, resultsPerPage, isReturnAll); List<Entity> entitiesReturn = new ArrayList<Entity>(); if (pageCount > 0) { entitiesReturn.addAll(entities.subList(offset, (offset + pageCount))); } return entitiesReturn; } protected Map<String, List<String>> validateEntityFilterByClause(Map<String, List<String>> filterByFieldsValues) { for (Map.Entry<String, List<String>> entry : filterByFieldsValues.entrySet()) { try { EntityList.EntityFilterByFields.valueOf(entry.getKey().toUpperCase()); } catch (IllegalArgumentException e) { throw FalconWebException.newAPIException("Invalid filter key: " + entry.getKey()); } } return filterByFieldsValues; } protected Map<String, List<String>> validateEntityFilterByClause(String entityFilterByClause) { Map<String, List<String>> filterByFieldsValues = getFilterByFieldsValues(entityFilterByClause); return validateEntityFilterByClause(filterByFieldsValues); } protected List<Entity> getFilteredEntities( EntityType entityType, String nameSubsequence, String tagKeywords, Map<String, List<String>> filterByFieldsValues, String startDate, String endDate, String cluster, final String doAsUser) throws FalconException, IOException { Collection<String> entityNames = configStore.getEntities(entityType); if (entityNames.isEmpty()) { return Collections.emptyList(); } List<Entity> entities = new ArrayList<Entity>(); char[] subsequence = nameSubsequence.toLowerCase().toCharArray(); List<String> tagKeywordsList; if (StringUtils.isEmpty(tagKeywords)) { tagKeywordsList = new ArrayList<>(); } else { tagKeywordsList = getFilterByTags(Arrays.asList(tagKeywords.toLowerCase())); } for (String entityName : entityNames) { Entity entity; try { entity = configStore.get(entityType, entityName); if (entity == null) { continue; } } catch (FalconException e1) { LOG.error("Unable to get list for entities for ({})", entityType.getEntityClass().getSimpleName(), e1); throw FalconWebException.newAPIException(e1); } if (SecurityUtil.isAuthorizationEnabled() && !isEntityAuthorized(entity)) { // the user who requested list query has no permission to access this entity. Skip this entity continue; } if (isFilteredByDatesAndCluster(entity, startDate, endDate, cluster)) { // this is for entity summary continue; } SecurityUtil.tryProxy(entity, doAsUser); // filter by fields if (isFilteredByFields(entity, filterByFieldsValues)) { continue; } // filter by subsequence of name if (subsequence.length > 0 && !matchesNameSubsequence(subsequence, entityName.toLowerCase())) { continue; } // filter by tag keywords if (!matchTagKeywords(tagKeywordsList, entity.getTags())) { continue; } entities.add(entity); } return entities; } //RESUME CHECKSTYLE CHECK ParameterNumberCheck private boolean matchesNameSubsequence(char[] subsequence, String name) { int currentIndex = 0; // current index in pattern which is to be matched for (Character c : name.toCharArray()) { if (currentIndex < subsequence.length && c == subsequence[currentIndex]) { currentIndex++; } if (currentIndex == subsequence.length) { return true; } } return false; } private boolean matchTagKeywords(List<String> tagKeywords, String tags) { if (tagKeywords.isEmpty()) { return true; } if (StringUtils.isEmpty(tags)) { return false; } tags = tags.toLowerCase(); for (String keyword : tagKeywords) { if (tags.indexOf(keyword) == -1) { return false; } } return true; } private boolean isFilteredByDatesAndCluster(Entity entity, String startDate, String endDate, String cluster) throws FalconException { if (StringUtils.isEmpty(cluster)) { return false; // no filtering necessary on cluster } Set<String> clusters = EntityUtil.getClustersDefined(entity); if (!clusters.contains(cluster)) { return true; // entity does not have this cluster } if (StringUtils.isNotEmpty(startDate)) { Date parsedDate = EntityUtil.parseDateUTC(startDate); if (parsedDate.after(EntityUtil.getEndTime(entity, cluster))) { return true; } } if (StringUtils.isNotEmpty(endDate)) { Date parseDate = EntityUtil.parseDateUTC(endDate); if (parseDate.before(EntityUtil.getStartTime(entity, cluster))) { return true; } } return false; } protected static Map<String, List<String>> getFilterByFieldsValues(String filterBy) { // Filter the results by specific field:value, eliminate empty values Map<String, List<String>> filterByFieldValues = new HashMap<String, List<String>>(); if (StringUtils.isNotEmpty(filterBy)) { String[] fieldValueArray = filterBy.split(","); for (String fieldValue : fieldValueArray) { String[] splits = fieldValue.split(":", 2); String filterByField = splits[0]; if (splits.length == 2 && !splits[1].equals("")) { List<String> currentValue = filterByFieldValues.get(filterByField); if (currentValue == null) { currentValue = new ArrayList<String>(); filterByFieldValues.put(filterByField, currentValue); } currentValue.add(splits[1]); } } } return filterByFieldValues; } private static List<String> getFilterByTags(List<String> filterTags) { ArrayList<String> filterTagsList = new ArrayList<String>(); if (filterTags!= null && !filterTags.isEmpty()) { for (String filterTag: filterTags) { String[] splits = filterTag.split(","); for (String tag : splits) { filterTagsList.add(tag.trim()); } } } return filterTagsList; } protected String getStatusString(Entity entity) { String statusString; try { statusString = getStatus(entity, entity.getEntityType()).first.name(); } catch (Throwable throwable) { // Unable to fetch statusString, setting it to unknown for backwards compatibility statusString = "UNKNOWN"; } return statusString; } protected boolean isEntityAuthorized(Entity entity) { try { SecurityUtil.getAuthorizationProvider().authorizeEntity(entity.getName(), entity.getEntityType().toString(), entity.getACL(), "list", CurrentUser.getAuthenticatedUGI()); } catch (Exception e) { LOG.info("Authorization failed for entity=" + entity.getName() + " for user=" + CurrentUser.getUser(), e); return false; } return true; } private boolean isFilteredByTags(List<String> filterTagsList, List<String> tags) { if (filterTagsList.isEmpty()) { return false; } else if (tags.isEmpty()) { return true; } for (String tag : filterTagsList) { if (!tags.contains(tag)) { return true; } } return false; } private boolean isFilteredByPipelines(List<String> filterPipelinesList, List<String> pipelines) { if (filterPipelinesList.isEmpty()) { return false; } else if (pipelines.isEmpty()) { return true; } for (String pipeline : filterPipelinesList) { if (pipelines.contains(pipeline)) { return false; } } return true; } private boolean isFilteredByClusters(List<String> filterClustersList, Set<String> clusters) { if (filterClustersList.isEmpty()) { return false; } else if (clusters.isEmpty()) { return true; } for (String cluster : filterClustersList) { if (clusters.contains(cluster)) { return false; } } return true; } private boolean isFilteredByFields(Entity entity, Map<String, List<String>> filterKeyVals) { if (filterKeyVals.isEmpty()) { return false; } for (Map.Entry<String, List<String>> pair : filterKeyVals.entrySet()) { EntityList.EntityFilterByFields filter = EntityList.EntityFilterByFields.valueOf(pair.getKey().toUpperCase()); if (isEntityFiltered(entity, filter, pair)) { return true; } } return false; } private boolean isEntityFiltered(Entity entity, EntityList.EntityFilterByFields filter, Map.Entry<String, List<String>> pair) { switch (filter) { case TYPE: return !containsIgnoreCase(pair.getValue(), entity.getEntityType().toString()); case NAME: return !containsIgnoreCase(pair.getValue(), entity.getName()); case STATUS: return !containsIgnoreCase(pair.getValue(), getStatusString(entity)); case PIPELINES: if (!entity.getEntityType().equals(EntityType.PROCESS)) { throw FalconWebException.newAPIException("Invalid filterBy key for non" + " process entities " + pair.getKey()); } return isFilteredByPipelines(pair.getValue(), EntityUtil.getPipelines(entity)); case CLUSTER: return isFilteredByClusters(pair.getValue(), EntityUtil.getClustersDefined(entity)); case TAGS: return isFilteredByTags(getFilterByTags(pair.getValue()), EntityUtil.getTags(entity)); default: return false; } } private List<Entity> sortEntities(List<Entity> entities, String orderBy, String sortOrder) { // Sort the ArrayList using orderBy param if (!entities.isEmpty() && StringUtils.isNotEmpty(orderBy)) { EntityList.EntityFieldList orderByField = EntityList.EntityFieldList.valueOf(orderBy.toUpperCase()); final String order = getValidSortOrder(sortOrder, orderBy); switch (orderByField) { case NAME: Collections.sort(entities, new Comparator<Entity>() { @Override public int compare(Entity e1, Entity e2) { return (order.equalsIgnoreCase("asc")) ? e1.getName().compareTo(e2.getName()) : e2.getName().compareTo(e1.getName()); } }); break; default: break; } } // else no sort return entities; } protected String getValidSortOrder(String sortOrder, String orderBy) { if (StringUtils.isEmpty(sortOrder)) { return (orderBy.equalsIgnoreCase("starttime") || orderBy.equalsIgnoreCase("endtime")) ? "desc" : "asc"; } if (sortOrder.equalsIgnoreCase("asc") || sortOrder.equalsIgnoreCase("desc")) { return sortOrder; } String err = "Value for param sortOrder should be \"asc\" or \"desc\". It is : " + sortOrder; LOG.error(err); throw FalconWebException.newAPIException(err); } protected int getRequiredNumberOfResults(int arraySize, int offset, int numresults) { return getRequiredNumberOfResults(arraySize, offset, numresults, false); } protected int getRequiredNumberOfResults(int arraySize, int offset, int numresults, boolean isReturnAll) { /* Get a subset of elements based on offset and count. When returning subset of elements, elements[offset] is included. Size 10, offset 10, return empty list. Size 10, offset 5, count 3, return elements[5,6,7]. Size 10, offset 5, count >= 5, return elements[5,6,7,8,9] return elements starting from elements[offset] until the end OR offset+numResults*/ if (!isReturnAll && numresults < 1) { LOG.error("Value for param numResults should be > than 0 : {}", numresults); throw FalconWebException.newAPIException("Value for param numResults should be > than 0 : " + numresults); } if (offset < 0) { offset = 0; } if (offset >= arraySize || arraySize == 0) { // No elements to return return 0; } int retLen = arraySize - offset; if (!isReturnAll && retLen > numresults) { retLen = numresults; } return retLen; } protected EntityElement[] buildEntityElements(HashSet<String> fields, List<Entity> entities) { EntityElement[] elements = new EntityElement[entities.size()]; int elementIndex = 0; for (Entity entity : entities) { elements[elementIndex++] = getEntityElement(entity, fields); } return elements; } protected EntityElement getEntityElement(Entity entity, HashSet<String> fields) { EntityElement elem = new EntityElement(); elem.type = entity.getEntityType().toString(); elem.name = entity.getName(); if (fields.contains(EntityList.EntityFieldList.STATUS.name())) { elem.status = getStatusString(entity); } if (fields.contains(EntityList.EntityFieldList.PIPELINES.name())) { elem.pipeline = EntityUtil.getPipelines(entity); } if (fields.contains(EntityList.EntityFieldList.TAGS.name())) { elem.tag = EntityUtil.getTags(entity); } if (fields.contains(EntityList.EntityFieldList.CLUSTERS.name())) { elem.cluster = new ArrayList<String>(EntityUtil.getClustersDefined(entity)); } return elem; } /** * Returns the entity definition as an XML based on name. * * @param type entity type * @param entityName entity name * @return String */ public String getEntityDefinition(String type, String entityName) { try { EntityType entityType = EntityType.getEnum(type); Entity entity = configStore.get(entityType, entityName); if (entity == null) { throw new NoSuchElementException(entityName + " (" + type + ") not found"); } return entity.toString(); } catch (Throwable e) { LOG.error("Unable to get entity definition from config store for ({}): {}", type, entityName, e); throw FalconWebException.newAPIException(e); } } /** * Given the location of data, returns the feed. * @param type type of the entity, is valid only for feeds. * @param instancePath location of the data * @return Feed Name, type of the data and cluster name. */ public FeedLookupResult reverseLookup(String type, String instancePath) { try { EntityType entityType = EntityType.getEnum(type); if (entityType != EntityType.FEED) { LOG.error("Reverse Lookup is not supported for entitytype: {}", type); throw new IllegalArgumentException("Reverse lookup is not supported for " + type); } instancePath = StringUtils.trim(instancePath); String instancePathWithoutSlash = instancePath.endsWith("/") ? StringUtils.removeEnd(instancePath, "/") : instancePath; // treat strings with and without trailing slash as same for purpose of searching e.g. // /data/cas and /data/cas/ should be treated as same. String instancePathWithSlash = instancePathWithoutSlash + "/"; FeedLocationStore store = FeedLocationStore.get(); Collection<FeedLookupResult.FeedProperties> feeds = new ArrayList<>(); Collection<FeedLookupResult.FeedProperties> res = store.reverseLookup(instancePathWithoutSlash); if (res != null) { feeds.addAll(res); } res = store.reverseLookup(instancePathWithSlash); if (res != null) { feeds.addAll(res); } FeedLookupResult result = new FeedLookupResult(APIResult.Status.SUCCEEDED, "SUCCESS"); FeedLookupResult.FeedProperties[] props = feeds.toArray(new FeedLookupResult.FeedProperties[0]); result.setElements(props); return result; } catch (IllegalArgumentException e) { throw FalconWebException.newAPIException(e); } catch (Throwable throwable) { LOG.error("reverse look up failed", throwable); throw FalconWebException.newAPIException(throwable, Response.Status.INTERNAL_SERVER_ERROR); } } protected AbstractWorkflowEngine getWorkflowEngine(Entity entity) throws FalconException { return WorkflowEngineFactory.getWorkflowEngine(entity); } protected <T extends APIResult> T consolidateResult(Map<String, T> results, Class<T> clazz) { if (results == null || results.isEmpty()) { return null; } StringBuilder message = new StringBuilder(); StringBuilder requestIds = new StringBuilder(); List instances = new ArrayList(); int statusCount = 0; for (Map.Entry<String, T> entry : results.entrySet()) { String colo = entry.getKey(); T result = results.get(colo); message.append(colo).append('/').append(result.getMessage()).append('\n'); requestIds.append(colo).append('/').append(result.getRequestId()).append('\n'); statusCount += result.getStatus().ordinal(); if (result.getCollection() == null) { continue; } Collections.addAll(instances, result.getCollection()); } Object[] arrInstances = instances.toArray(); APIResult.Status status = (statusCount == 0) ? APIResult.Status.SUCCEEDED : ((statusCount == results.size() * 2) ? APIResult.Status.FAILED : APIResult.Status.PARTIAL); try { Constructor<T> constructor = clazz.getConstructor(Status.class, String.class); T result = constructor.newInstance(status, message.toString()); result.setCollection(arrInstances); result.setRequestId(requestIds.toString()); return result; } catch (Exception e) { throw new FalconRuntimException("Unable to consolidate result.", e); } } private boolean containsIgnoreCase(List<String> strList, String str) { for (String s : strList) { if (s.equalsIgnoreCase(str)) { return true; } } return false; } private void verifySuperUser() throws FalconException, IOException { final UserGroupInformation authenticatedUGI = CurrentUser.getAuthenticatedUGI(); DefaultAuthorizationProvider authorizationProvider = new DefaultAuthorizationProvider(); if (!authorizationProvider.isSuperUser(authenticatedUGI)) { throw new FalconException("Permission denied : " + "Cluster entity update can only be performed by superuser."); } } }