/* * 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.brooklyn.rest.resources; import static com.google.common.base.Preconditions.checkNotNull; import static javax.ws.rs.core.Response.created; import static javax.ws.rs.core.Response.status; import static javax.ws.rs.core.Response.Status.ACCEPTED; import java.net.URI; import java.net.URISyntaxException; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.ResponseBuilder; import javax.ws.rs.core.UriInfo; import org.apache.brooklyn.api.entity.Application; import org.apache.brooklyn.api.entity.Entity; import org.apache.brooklyn.api.entity.EntitySpec; import org.apache.brooklyn.api.entity.Group; import org.apache.brooklyn.api.location.Location; import org.apache.brooklyn.api.mgmt.Task; import org.apache.brooklyn.api.objs.BrooklynObject; import org.apache.brooklyn.api.sensor.AttributeSensor; import org.apache.brooklyn.api.sensor.Sensor; import org.apache.brooklyn.api.typereg.RegisteredType; import org.apache.brooklyn.core.config.ConstraintViolationException; import org.apache.brooklyn.core.entity.Attributes; import org.apache.brooklyn.core.entity.EntityPredicates; import org.apache.brooklyn.core.entity.lifecycle.Lifecycle; import org.apache.brooklyn.core.entity.trait.Startable; import org.apache.brooklyn.core.mgmt.EntityManagementUtils; import org.apache.brooklyn.core.mgmt.EntityManagementUtils.CreationResult; import org.apache.brooklyn.core.mgmt.entitlement.EntitlementPredicates; import org.apache.brooklyn.core.mgmt.entitlement.Entitlements; import org.apache.brooklyn.core.mgmt.entitlement.Entitlements.EntityAndItem; import org.apache.brooklyn.core.mgmt.entitlement.Entitlements.StringAndArgument; import org.apache.brooklyn.core.sensor.Sensors; import org.apache.brooklyn.core.typereg.RegisteredTypeLoadingContexts; import org.apache.brooklyn.core.typereg.RegisteredTypes; import org.apache.brooklyn.entity.group.AbstractGroup; import org.apache.brooklyn.rest.api.ApplicationApi; import org.apache.brooklyn.rest.domain.ApplicationSpec; import org.apache.brooklyn.rest.domain.ApplicationSummary; import org.apache.brooklyn.rest.domain.EntitySummary; import org.apache.brooklyn.rest.domain.TaskSummary; import org.apache.brooklyn.rest.filter.HaHotStateRequired; import org.apache.brooklyn.rest.transform.ApplicationTransformer; import org.apache.brooklyn.rest.transform.EntityTransformer; import org.apache.brooklyn.rest.transform.TaskTransformer; import org.apache.brooklyn.rest.util.BrooklynRestResourceUtils; import org.apache.brooklyn.rest.util.WebResourceUtils; import org.apache.brooklyn.util.collections.MutableMap; import org.apache.brooklyn.util.core.ResourceUtils; import org.apache.brooklyn.util.exceptions.Exceptions; import org.apache.brooklyn.util.exceptions.UserFacingException; import org.apache.brooklyn.util.guava.Maybe; import org.apache.brooklyn.util.javalang.JavaClassNames; import org.apache.brooklyn.util.text.Strings; import org.codehaus.jackson.JsonNode; import org.codehaus.jackson.node.ArrayNode; import org.codehaus.jackson.node.ObjectNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Throwables; import com.google.common.collect.FluentIterable; import com.google.common.collect.Iterables; @HaHotStateRequired public class ApplicationResource extends AbstractBrooklynRestResource implements ApplicationApi { private static final Logger log = LoggerFactory.getLogger(ApplicationResource.class); @Context private UriInfo uriInfo; /** @deprecated since 0.6.0 use {@link #fetch(String)} (with slightly different, but better semantics) */ @Deprecated @Override public JsonNode applicationTree() { ArrayNode apps = mapper().createArrayNode(); for (Application application : mgmt().getApplications()) apps.add(recursiveTreeFromEntity(application)); return apps; } private ObjectNode entityBase(Entity entity) { ObjectNode aRoot = mapper().createObjectNode(); aRoot.put("name", entity.getDisplayName()); aRoot.put("id", entity.getId()); aRoot.put("type", entity.getEntityType().getName()); Boolean serviceUp = entity.getAttribute(Attributes.SERVICE_UP); if (serviceUp!=null) aRoot.put("serviceUp", serviceUp); Lifecycle serviceState = entity.getAttribute(Attributes.SERVICE_STATE_ACTUAL); if (serviceState!=null) aRoot.put("serviceState", serviceState.toString()); String iconUrl = entity.getIconUrl(); if (iconUrl!=null) { if (brooklyn().isUrlServerSideAndSafe(iconUrl)) // route to server if it is a server-side url iconUrl = EntityTransformer.entityUri(entity)+"/icon"; aRoot.put("iconUrl", iconUrl); } return aRoot; } private JsonNode recursiveTreeFromEntity(Entity entity) { ObjectNode aRoot = entityBase(entity); if (!entity.getChildren().isEmpty()) aRoot.put("children", childEntitiesRecursiveAsArray(entity)); return aRoot; } // TODO when applicationTree can be removed, replace this with an extension to EntitySummary (without links) private JsonNode fromEntity(Entity entity) { ObjectNode aRoot = entityBase(entity); aRoot.put("applicationId", entity.getApplicationId()); if (entity.getParent()!=null) { aRoot.put("parentId", entity.getParent().getId()); } if (!entity.groups().isEmpty()) aRoot.put("groupIds", entitiesIdAsArray(entity.groups())); if (!entity.getChildren().isEmpty()) aRoot.put("children", entitiesIdAndNameAsArray(entity.getChildren())); if (entity instanceof Group) { // use attribute instead of method in case it is read-only Collection<Entity> members = entity.getAttribute(AbstractGroup.GROUP_MEMBERS); if (members!=null && !members.isEmpty()) aRoot.put("members", entitiesIdAndNameAsArray(members)); } return aRoot; } private ArrayNode childEntitiesRecursiveAsArray(Entity entity) { ArrayNode node = mapper().createArrayNode(); for (Entity e : entity.getChildren()) { if (Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY, entity)) { node.add(recursiveTreeFromEntity(e)); } } return node; } private ArrayNode entitiesIdAndNameAsArray(Collection<? extends Entity> entities) { ArrayNode node = mapper().createArrayNode(); for (Entity entity : entities) { if (Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY, entity)) { ObjectNode holder = mapper().createObjectNode(); holder.put("id", entity.getId()); holder.put("name", entity.getDisplayName()); node.add(holder); } } return node; } private ArrayNode entitiesIdAsArray(Iterable<? extends Entity> entities) { ArrayNode node = mapper().createArrayNode(); for (Entity entity : entities) { if (Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY, entity)) { node.add(entity.getId()); } } return node; } @Override public JsonNode fetch(String entityIds) { Map<String, JsonNode> jsonEntitiesById = MutableMap.of(); for (Application application : mgmt().getApplications()) jsonEntitiesById.put(application.getId(), fromEntity(application)); if (entityIds != null) { for (String entityId: entityIds.split(",")) { Entity entity = mgmt().getEntityManager().getEntity(entityId.trim()); while (entity != null && entity.getParent() != null) { if (Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY, entity)) { jsonEntitiesById.put(entity.getId(), fromEntity(entity)); } entity = entity.getParent(); } } } ArrayNode result = mapper().createArrayNode(); for (JsonNode n: jsonEntitiesById.values()) result.add(n); return result; } @Override public List<ApplicationSummary> list(String typeRegex) { if (Strings.isBlank(typeRegex)) { typeRegex = ".*"; } return FluentIterable .from(mgmt().getApplications()) .filter(EntitlementPredicates.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY)) .filter(EntityPredicates.hasInterfaceMatching(typeRegex)) .transform(ApplicationTransformer.FROM_APPLICATION) .toList(); } @Override public ApplicationSummary get(String application) { return ApplicationTransformer.summaryFromApplication(brooklyn().getApplication(application)); } public Response create(ApplicationSpec applicationSpec) { return createFromAppSpec(applicationSpec); } /** @deprecated since 0.7.0 see #create */ @Deprecated protected Response createFromAppSpec(ApplicationSpec applicationSpec) { if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.DEPLOY_APPLICATION, applicationSpec)) { throw WebResourceUtils.unauthorized("User '%s' is not authorized to start application %s", Entitlements.getEntitlementContext().user(), applicationSpec); } checkApplicationTypesAreValid(applicationSpec); checkLocationsAreValid(applicationSpec); // TODO duplicate prevention List<Location> locations = brooklyn().getLocations(applicationSpec); Application app = brooklyn().create(applicationSpec); Task<?> t = brooklyn().start(app, locations); TaskSummary ts = TaskTransformer.FROM_TASK.apply(t); URI ref = uriInfo.getBaseUriBuilder() .path(ApplicationApi.class) .path(ApplicationApi.class, "get") .build(app.getApplicationId()); return created(ref).entity(ts).build(); } @Override public Response createFromYaml(String yaml) { // First of all, see if it's a URL URI uri; try { uri = new URI(yaml); } catch (URISyntaxException e) { // It's not a URI then... uri = null; } if (uri != null) { log.debug("Create app called with URI; retrieving contents: {}", uri); yaml = ResourceUtils.create(mgmt()).getResourceAsString(uri.toString()); } log.debug("Creating app from yaml:\n{}", yaml); EntitySpec<? extends Application> spec = createEntitySpecForApplication(yaml); if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.DEPLOY_APPLICATION, spec)) { throw WebResourceUtils.unauthorized("User '%s' is not authorized to start application %s", Entitlements.getEntitlementContext().user(), yaml); } return launch(yaml, spec); } private Response launch(String yaml, EntitySpec<? extends Application> spec) { try { Application app = EntityManagementUtils.createUnstarted(mgmt(), spec); CreationResult<Application,Void> result = EntityManagementUtils.start(app); boolean isEntitled = Entitlements.isEntitled( mgmt().getEntitlementManager(), Entitlements.INVOKE_EFFECTOR, EntityAndItem.of(app, StringAndArgument.of(Startable.START.getName(), null))); if (!isEntitled) { throw WebResourceUtils.unauthorized("User '%s' is not authorized to start application %s", Entitlements.getEntitlementContext().user(), spec.getType()); } log.info("Launched from YAML: " + yaml + " -> " + app + " (" + result.task() + ")"); URI ref = URI.create(app.getApplicationId()); ResponseBuilder response = created(ref); if (result.task() != null) response.entity(TaskTransformer.FROM_TASK.apply(result.task())); return response.build(); } catch (ConstraintViolationException e) { throw new UserFacingException(e); } catch (Exception e) { throw Exceptions.propagate(e); } } @Override public Response createPoly(byte[] inputToAutodetectType) { log.debug("Creating app from autodetecting input"); boolean looksLikeLegacy = false; Exception legacyFormatException = null; // attempt legacy format try { ApplicationSpec appSpec = mapper().readValue(inputToAutodetectType, ApplicationSpec.class); if (appSpec.getType() != null || appSpec.getEntities() != null) { looksLikeLegacy = true; } return createFromAppSpec(appSpec); } catch (Exception e) { Exceptions.propagateIfFatal(e); legacyFormatException = e; log.debug("Input is not legacy ApplicationSpec JSON (will try others): "+e, e); } //TODO infer encoding from request String potentialYaml = new String(inputToAutodetectType); EntitySpec<? extends Application> spec = createEntitySpecForApplication(potentialYaml); // TODO not json - try ZIP, etc if (spec != null) { return launch(potentialYaml, spec); } else if (looksLikeLegacy) { throw Throwables.propagate(legacyFormatException); } else { return Response.serverError().entity("Unsupported format; not able to autodetect.").build(); } } @Override public Response createFromForm(String contents) { log.debug("Creating app from form"); return createPoly(contents.getBytes()); } @Override public Response delete(String application) { Application app = brooklyn().getApplication(application); if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.INVOKE_EFFECTOR, Entitlements.EntityAndItem.of(app, StringAndArgument.of(Entitlements.LifecycleEffectors.DELETE, null)))) { throw WebResourceUtils.unauthorized("User '%s' is not authorized to delete application %s", Entitlements.getEntitlementContext().user(), app); } Task<?> t = brooklyn().destroy(app); TaskSummary ts = TaskTransformer.FROM_TASK.apply(t); return status(ACCEPTED).entity(ts).build(); } private EntitySpec<? extends Application> createEntitySpecForApplication(String potentialYaml) { try { return EntityManagementUtils.createEntitySpecForApplication(mgmt(), potentialYaml); } catch (Exception e) { // An IllegalArgumentException for creating the entity spec gets wrapped in a ISE, and possibly a Compound. // But we want to return a 400 rather than 500, so ensure we throw IAE. IllegalArgumentException iae = (IllegalArgumentException) Exceptions.getFirstThrowableOfType(e, IllegalArgumentException.class); if (iae != null) { throw new IllegalArgumentException("Cannot create spec for app: "+iae.getMessage(), e); } else { throw Exceptions.propagate(e); } } } private void checkApplicationTypesAreValid(ApplicationSpec applicationSpec) { String appType = applicationSpec.getType(); if (appType != null) { checkEntityTypeIsValid(appType); if (applicationSpec.getEntities() != null) { throw WebResourceUtils.preconditionFailed("Application given explicit type '%s' must not define entities", appType); } return; } for (org.apache.brooklyn.rest.domain.EntitySpec entitySpec : applicationSpec.getEntities()) { String entityType = entitySpec.getType(); checkEntityTypeIsValid(checkNotNull(entityType, "entityType")); } } private void checkSpecTypeIsValid(String type, Class<? extends BrooklynObject> subType) { Maybe<RegisteredType> typeV = RegisteredTypes.tryValidate(mgmt().getTypeRegistry().get(type), RegisteredTypeLoadingContexts.spec(subType)); if (!typeV.isNull()) { // found, throw if any problem typeV.get(); return; } // not found, try classloading try { brooklyn().getCatalogClassLoader().loadClass(type); } catch (ClassNotFoundException e) { log.debug("Class not found for type '" + type + "'; reporting 404", e); throw WebResourceUtils.notFound("Undefined type '%s'", type); } log.info(JavaClassNames.simpleClassName(subType)+" type '{}' not defined in catalog but is on classpath; continuing", type); } private void checkEntityTypeIsValid(String type) { checkSpecTypeIsValid(type, Entity.class); } @SuppressWarnings("deprecation") private void checkLocationsAreValid(ApplicationSpec applicationSpec) { for (String locationId : applicationSpec.getLocations()) { locationId = BrooklynRestResourceUtils.fixLocation(locationId); if (!brooklyn().getLocationRegistry().canMaybeResolve(locationId) && brooklyn().getLocationRegistry().getDefinedLocationById(locationId)==null) { throw WebResourceUtils.notFound("Undefined location '%s'", locationId); } } } @Override public List<EntitySummary> getDescendants(String application, String typeRegex) { return EntityTransformer.entitySummaries(brooklyn().descendantsOfType(application, application, typeRegex)); } @Override public Map<String, Object> getDescendantsSensor(String application, String sensor, String typeRegex) { Iterable<Entity> descs = brooklyn().descendantsOfType(application, application, typeRegex); return getSensorMap(sensor, descs); } public static Map<String, Object> getSensorMap(String sensor, Iterable<Entity> descs) { if (Iterables.isEmpty(descs)) return Collections.emptyMap(); Map<String, Object> result = MutableMap.of(); Iterator<Entity> di = descs.iterator(); Sensor<?> s = null; while (di.hasNext()) { Entity potentialSource = di.next(); s = potentialSource.getEntityType().getSensor(sensor); if (s!=null) break; } if (s==null) s = Sensors.newSensor(Object.class, sensor); if (!(s instanceof AttributeSensor<?>)) { log.warn("Cannot retrieve non-attribute sensor "+s+" for entities; returning empty map"); return result; } for (Entity e: descs) { Object v = null; try { v = e.getAttribute((AttributeSensor<?>)s); } catch (Exception exc) { Exceptions.propagateIfFatal(exc); log.warn("Error retrieving sensor "+s+" for "+e+" (ignoring): "+exc); } if (v!=null) result.put(e.getId(), v); } return result; } }