ambari-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From alejan...@apache.org
Subject [5/6] ambari git commit: AMBARI-12885. Dynamic stack extensions - install and upgrade support for custom services (Tim Thorpe via alejandro)
Date Mon, 20 Jun 2016 21:42:45 GMT
http://git-wip-us.apache.org/repos/asf/ambari/blob/9ce79716/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ExtensionLinkResourceProvider.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ExtensionLinkResourceProvider.java b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ExtensionLinkResourceProvider.java
new file mode 100644
index 0000000..67cc972
--- /dev/null
+++ b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ExtensionLinkResourceProvider.java
@@ -0,0 +1,241 @@
+/**
+ * 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.ambari.server.controller.internal;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.ambari.server.AmbariException;
+import org.apache.ambari.server.StaticallyInject;
+import org.apache.ambari.server.controller.AmbariManagementController;
+import org.apache.ambari.server.controller.ExtensionLinkRequest;
+import org.apache.ambari.server.controller.RequestStatusResponse;
+import org.apache.ambari.server.controller.spi.*;
+import org.apache.ambari.server.controller.spi.Resource.Type;
+import org.apache.ambari.server.controller.utilities.PropertyHelper;
+import org.apache.ambari.server.orm.dao.ExtensionLinkDAO;
+import org.apache.ambari.server.orm.entities.ExtensionLinkEntity;
+
+import com.google.inject.Inject;
+
+/**
+ * An extension version is like a stack version but it contains custom services.  Linking an extension
+ * version to the current stack version allows the cluster to install the custom services contained in
+ * the extension version.
+ */
+@StaticallyInject
+public class ExtensionLinkResourceProvider extends AbstractControllerResourceProvider {
+
+  public static final String LINK_ID_PROPERTY_ID = PropertyHelper
+      .getPropertyId("ExtensionLink", "link_id");
+
+  public static final String STACK_NAME_PROPERTY_ID = PropertyHelper
+	      .getPropertyId("ExtensionLink", "stack_name");
+
+  public static final String STACK_VERSION_PROPERTY_ID = PropertyHelper
+      .getPropertyId("ExtensionLink", "stack_version");
+
+  public static final String EXTENSION_NAME_PROPERTY_ID = PropertyHelper
+      .getPropertyId("ExtensionLink", "extension_name");
+
+  public static final String EXTENSION_VERSION_PROPERTY_ID = PropertyHelper
+      .getPropertyId("ExtensionLink", "extension_version");
+
+  private static Set<String> pkPropertyIds = new HashSet<String>(
+      Arrays.asList(new String[] { LINK_ID_PROPERTY_ID, STACK_NAME_PROPERTY_ID, STACK_VERSION_PROPERTY_ID, EXTENSION_NAME_PROPERTY_ID, EXTENSION_VERSION_PROPERTY_ID }));
+
+  @Inject
+  private static ExtensionLinkDAO dao;
+
+  protected ExtensionLinkResourceProvider(Set<String> propertyIds,
+      Map<Type, String> keyPropertyIds,
+      AmbariManagementController managementController) {
+    super(propertyIds, keyPropertyIds, managementController);
+  }
+
+  @Override
+  public RequestStatus createResources(Request request)
+	        throws SystemException, UnsupportedPropertyException,
+	        NoSuchParentResourceException, ResourceAlreadyExistsException {
+
+    final Set<ExtensionLinkRequest> requests = new HashSet<ExtensionLinkRequest>();
+    for (Map<String, Object> propertyMap : request.getProperties()) {
+      requests.add(getRequest(propertyMap));
+    }
+
+    createResources(new Command<Void>() {
+      @Override
+      public Void invoke() throws AmbariException {
+        for (ExtensionLinkRequest extensionLinkRequest : requests) {
+          getManagementController().createExtensionLink(extensionLinkRequest);
+        }
+        return null;
+      }
+    });
+
+    if (requests.size() > 0) {
+      //Need to reread the stacks/extensions directories so the latest information is available
+      try {
+        getManagementController().updateStacks();
+      } catch (AmbariException e) {
+        throw new SystemException(e.getMessage(), e);
+      }
+
+      notifyCreate(Resource.Type.ExtensionLink, request);
+    }
+
+    return getRequestStatus(null);
+  }
+
+  protected RequestStatus deleteResourcesAuthorized(Request request, Predicate predicate)
+        throws SystemException, UnsupportedPropertyException,
+        NoSuchResourceException, NoSuchParentResourceException {
+
+    final Set<ExtensionLinkRequest> requests = new HashSet<ExtensionLinkRequest>();
+    if (predicate == null) {
+      requests.add(getRequest(Collections.<String, Object>emptyMap()));
+    } else {
+      for (Map<String, Object> propertyMap : getPropertyMaps(predicate)) {
+        requests.add(getRequest(propertyMap));
+      }
+    }
+
+    RequestStatusResponse response = modifyResources(new Command<RequestStatusResponse>() {
+      @Override
+      public RequestStatusResponse invoke() throws AmbariException {
+        for (ExtensionLinkRequest extensionLinkRequest : requests) {
+          getManagementController().deleteExtensionLink(extensionLinkRequest);
+        }
+        return null;
+      }
+    });
+
+    //Need to reread the stacks/extensions directories so the latest information is available
+    try {
+      getManagementController().updateStacks();
+    } catch (AmbariException e) {
+      throw new SystemException(e.getMessage(), e);
+    }
+
+    notifyDelete(Resource.Type.ExtensionLink, predicate);
+
+    return getRequestStatus(response);
+  }
+
+  @Override
+  public Set<Resource> getResources(Request request, Predicate predicate)
+        throws SystemException, UnsupportedPropertyException,
+        NoSuchResourceException, NoSuchParentResourceException {
+
+    final Set<Resource> resources = new HashSet<Resource>();
+    final Set<String> requestedIds = getRequestPropertyIds(request, predicate);
+
+    final Set<ExtensionLinkRequest> requests = new HashSet<ExtensionLinkRequest>();
+    if (predicate == null) {
+      requests.add(getRequest(Collections.<String, Object>emptyMap()));
+    } else {
+      for (Map<String, Object> propertyMap : getPropertyMaps(predicate)) {
+        requests.add(getRequest(propertyMap));
+      }
+    }
+
+    Set<ExtensionLinkEntity> entities = new HashSet<ExtensionLinkEntity>();
+
+    for (ExtensionLinkRequest extensionLinkRequest : requests) {
+      verifyStackAndExtensionExist(extensionLinkRequest);
+      entities.addAll(dao.find(extensionLinkRequest));
+    }
+
+    for (ExtensionLinkEntity entity : entities) {
+      Resource resource = new ResourceImpl(Resource.Type.ExtensionLink);
+      setResourceProperty(resource, LINK_ID_PROPERTY_ID,
+		  entity.getLinkId(), requestedIds);
+      setResourceProperty(resource, STACK_NAME_PROPERTY_ID,
+	  entity.getStack().getStackName(), requestedIds);
+      setResourceProperty(resource, STACK_VERSION_PROPERTY_ID,
+          entity.getStack().getStackVersion(), requestedIds);
+      setResourceProperty(resource, EXTENSION_NAME_PROPERTY_ID,
+          entity.getExtension().getExtensionName(), requestedIds);
+      setResourceProperty(resource, EXTENSION_VERSION_PROPERTY_ID,
+          entity.getExtension().getExtensionVersion(), requestedIds);
+
+      resources.add(resource);
+    }
+    return resources;
+  }
+
+  @Override
+  public RequestStatus updateResources(Request request, Predicate predicate)
+        throws SystemException, UnsupportedPropertyException,
+        NoSuchResourceException, NoSuchParentResourceException {
+
+    //Need to reread the stacks/extensions directories so the latest information is available
+    try {
+      getManagementController().updateStacks();
+    } catch (AmbariException e) {
+      throw new SystemException(e.getMessage(), e);
+    }
+
+    notifyUpdate(Resource.Type.ExtensionLink, request, predicate);
+    return getRequestStatus(null);
+  }
+
+  private void verifyStackAndExtensionExist(ExtensionLinkRequest request) throws NoSuchParentResourceException {
+    try {
+      if (request.getStackName() != null && request.getStackVersion() != null) {
+        getManagementController().getAmbariMetaInfo().getStack(request.getStackName(), request.getStackVersion());
+      }
+      if (request.getExtensionName() != null && request.getExtensionVersion() != null) {
+        getManagementController().getAmbariMetaInfo().getExtension(request.getExtensionName(), request.getExtensionVersion());
+      }
+    }
+    catch (AmbariException ambariException) {
+      throw new NoSuchParentResourceException(ambariException.getMessage());
+    }
+  }
+
+  private ExtensionLinkRequest getRequest(Map<String, Object> properties) {
+    return new ExtensionLinkRequest(
+        (String) properties.get(LINK_ID_PROPERTY_ID),
+        (String) properties.get(STACK_NAME_PROPERTY_ID),
+        (String) properties.get(STACK_VERSION_PROPERTY_ID),
+        (String) properties.get(EXTENSION_NAME_PROPERTY_ID),
+        (String) properties.get(EXTENSION_VERSION_PROPERTY_ID));
+  }
+
+  private ExtensionLinkRequest createExtensionLinkRequest(ExtensionLinkEntity entity) {
+    if (entity == null) {
+      return null;
+    }
+
+    return new ExtensionLinkRequest(String.valueOf(entity.getLinkId()),
+        entity.getStack().getStackName(),
+        entity.getStack().getStackVersion(),
+        entity.getExtension().getExtensionName(),
+        entity.getExtension().getExtensionVersion());
+  }
+
+  @Override
+  protected Set<String> getPKPropertyIds() {
+    return pkPropertyIds;
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/9ce79716/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ExtensionResourceProvider.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ExtensionResourceProvider.java b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ExtensionResourceProvider.java
new file mode 100644
index 0000000..8baf202
--- /dev/null
+++ b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ExtensionResourceProvider.java
@@ -0,0 +1,121 @@
+/**
+ * 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.ambari.server.controller.internal;
+
+import java.util.*;
+
+import org.apache.ambari.server.AmbariException;
+import org.apache.ambari.server.controller.AmbariManagementController;
+import org.apache.ambari.server.controller.RequestStatusResponse;
+import org.apache.ambari.server.controller.ExtensionRequest;
+import org.apache.ambari.server.controller.ExtensionResponse;
+import org.apache.ambari.server.controller.spi.*;
+import org.apache.ambari.server.controller.spi.Resource.Type;
+import org.apache.ambari.server.controller.utilities.PropertyHelper;
+
+/**
+ * An extension version is like a stack version but it contains custom services.  Linking an extension
+ * version to the current stack version allows the cluster to install the custom services contained in
+ * the extension version.
+ */
+public class ExtensionResourceProvider extends ReadOnlyResourceProvider {
+
+  public static final String EXTENSION_NAME_PROPERTY_ID = PropertyHelper
+      .getPropertyId("Extensions", "extension_name");
+
+  private static Set<String> pkPropertyIds = new HashSet<String>(
+      Arrays.asList(new String[] { EXTENSION_NAME_PROPERTY_ID }));
+
+  protected ExtensionResourceProvider(Set<String> propertyIds,
+      Map<Type, String> keyPropertyIds,
+      AmbariManagementController managementController) {
+    super(propertyIds, keyPropertyIds, managementController);
+  }
+
+
+  @Override
+  public Set<Resource> getResources(Request request, Predicate predicate)
+      throws SystemException, UnsupportedPropertyException,
+      NoSuchResourceException, NoSuchParentResourceException {
+
+    final Set<ExtensionRequest> requests = new HashSet<ExtensionRequest>();
+
+    if (predicate == null) {
+      requests.add(getRequest(Collections.<String, Object>emptyMap()));
+    } else {
+      for (Map<String, Object> propertyMap : getPropertyMaps(predicate)) {
+        requests.add(getRequest(propertyMap));
+      }
+    }
+
+    Set<String> requestedIds = getRequestPropertyIds(request, predicate);
+
+    Set<ExtensionResponse> responses = getResources(new Command<Set<ExtensionResponse>>() {
+      @Override
+      public Set<ExtensionResponse> invoke() throws AmbariException {
+        return getManagementController().getExtensions(requests);
+      }
+    });
+
+    Set<Resource> resources = new HashSet<Resource>();
+
+    for (ExtensionResponse response : responses) {
+      Resource resource = new ResourceImpl(Resource.Type.Extension);
+
+      setResourceProperty(resource, EXTENSION_NAME_PROPERTY_ID,
+          response.getExtensionName(), requestedIds);
+
+      resource.setProperty(EXTENSION_NAME_PROPERTY_ID, response.getExtensionName());
+
+      resources.add(resource);
+    }
+
+    return resources;
+  }
+
+  @Override
+  public RequestStatus updateResources(Request request, Predicate predicate)
+    throws SystemException, UnsupportedPropertyException,
+    NoSuchResourceException, NoSuchParentResourceException {
+
+    RequestStatusResponse response = modifyResources(
+      new Command<RequestStatusResponse>() {
+
+      @Override
+      public RequestStatusResponse invoke() throws AmbariException {
+        //return getManagementController().updateExtensions();
+	    //TODO - do we need a separate method
+        return getManagementController().updateStacks();
+      }
+    });
+
+    notifyUpdate(Type.Extension, request, predicate);
+
+    return getRequestStatus(response);
+  }
+
+  private ExtensionRequest getRequest(Map<String, Object> properties) {
+    return new ExtensionRequest((String) properties.get(EXTENSION_NAME_PROPERTY_ID));
+  }
+
+  @Override
+  protected Set<String> getPKPropertyIds() {
+    return pkPropertyIds;
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/9ce79716/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ExtensionVersionResourceProvider.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ExtensionVersionResourceProvider.java b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ExtensionVersionResourceProvider.java
new file mode 100644
index 0000000..57ebabd
--- /dev/null
+++ b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ExtensionVersionResourceProvider.java
@@ -0,0 +1,131 @@
+/**
+ * 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.ambari.server.controller.internal;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import com.google.inject.Inject;
+import org.apache.ambari.server.AmbariException;
+import org.apache.ambari.server.StaticallyInject;
+import org.apache.ambari.server.controller.AmbariManagementController;
+import org.apache.ambari.server.controller.ExtensionVersionRequest;
+import org.apache.ambari.server.controller.ExtensionVersionResponse;
+import org.apache.ambari.server.controller.spi.NoSuchParentResourceException;
+import org.apache.ambari.server.controller.spi.NoSuchResourceException;
+import org.apache.ambari.server.controller.spi.Predicate;
+import org.apache.ambari.server.controller.spi.Request;
+import org.apache.ambari.server.controller.spi.Resource;
+import org.apache.ambari.server.controller.spi.Resource.Type;
+import org.apache.ambari.server.controller.spi.SystemException;
+import org.apache.ambari.server.controller.spi.UnsupportedPropertyException;
+import org.apache.ambari.server.controller.utilities.PropertyHelper;
+
+/**
+ * An extension version is like a stack version but it contains custom services.  Linking an extension
+ * version to the current stack version allows the cluster to install the custom services contained in
+ * the extension version.
+ */
+@StaticallyInject
+public class ExtensionVersionResourceProvider extends ReadOnlyResourceProvider {
+
+  public static final String EXTENSION_VERSION_PROPERTY_ID     = PropertyHelper.getPropertyId("Versions", "extension_version");
+  public static final String EXTENSION_NAME_PROPERTY_ID        = PropertyHelper.getPropertyId("Versions", "extension_name");
+  public static final String EXTENSION_VALID_PROPERTY_ID      = PropertyHelper.getPropertyId("Versions", "valid");
+  public static final String EXTENSION_ERROR_SET      = PropertyHelper.getPropertyId("Versions", "extension-errors");
+  public static final String EXTENSION_PARENT_PROPERTY_ID      = PropertyHelper.getPropertyId("Versions", "parent_extension_version");
+
+  private static Set<String> pkPropertyIds = new HashSet<String>(
+      Arrays.asList(new String[] { EXTENSION_NAME_PROPERTY_ID, EXTENSION_VERSION_PROPERTY_ID }));
+
+  protected ExtensionVersionResourceProvider(Set<String> propertyIds,
+      Map<Type, String> keyPropertyIds,
+      AmbariManagementController managementController) {
+    super(propertyIds, keyPropertyIds, managementController);
+  }
+
+  @Override
+  public Set<Resource> getResources(Request request, Predicate predicate)
+      throws SystemException, UnsupportedPropertyException,
+      NoSuchResourceException, NoSuchParentResourceException {
+
+    final Set<ExtensionVersionRequest> requests = new HashSet<ExtensionVersionRequest>();
+
+    if (predicate == null) {
+      requests.add(getRequest(Collections.<String, Object>emptyMap()));
+    } else {
+      for (Map<String, Object> propertyMap : getPropertyMaps(predicate)) {
+        requests.add(getRequest(propertyMap));
+      }
+    }
+
+    Set<String> requestedIds = getRequestPropertyIds(request, predicate);
+
+    Set<ExtensionVersionResponse> responses = getResources(new Command<Set<ExtensionVersionResponse>>() {
+      @Override
+      public Set<ExtensionVersionResponse> invoke() throws AmbariException {
+        return getManagementController().getExtensionVersions(requests);
+      }
+    });
+
+    Set<Resource> resources = new HashSet<Resource>();
+
+    for (ExtensionVersionResponse response : responses) {
+      Resource resource = new ResourceImpl(Resource.Type.ExtensionVersion);
+
+      setResourceProperty(resource, EXTENSION_NAME_PROPERTY_ID,
+          response.getExtensionName(), requestedIds);
+
+      setResourceProperty(resource, EXTENSION_VERSION_PROPERTY_ID,
+          response.getExtensionVersion(), requestedIds);
+
+      setResourceProperty(resource, EXTENSION_VALID_PROPERTY_ID,
+          response.isValid(), requestedIds);
+
+      setResourceProperty(resource, EXTENSION_ERROR_SET,
+          response.getErrors(), requestedIds);
+
+      setResourceProperty(resource, EXTENSION_PARENT_PROPERTY_ID,
+        response.getParentVersion(), requestedIds);
+
+      resources.add(resource);
+    }
+
+    return resources;
+  }
+
+  private ExtensionVersionRequest getRequest(Map<String, Object> properties) {
+    return new ExtensionVersionRequest(
+        (String) properties.get(EXTENSION_NAME_PROPERTY_ID),
+        (String) properties.get(EXTENSION_VERSION_PROPERTY_ID));
+  }
+
+  @Override
+  protected Set<String> getPKPropertyIds() {
+    return pkPropertyIds;
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/9ce79716/ambari-server/src/main/java/org/apache/ambari/server/controller/spi/Resource.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/controller/spi/Resource.java b/ambari-server/src/main/java/org/apache/ambari/server/controller/spi/Resource.java
index 99e4ccd..6853266 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/controller/spi/Resource.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/controller/spi/Resource.java
@@ -92,6 +92,9 @@ public interface Resource {
     Member,
     Stack,
     StackVersion,
+    ExtensionLink,
+    Extension,
+    ExtensionVersion,
     OperatingSystem,
     Repository,
     StackService,
@@ -210,6 +213,9 @@ public interface Resource {
     public static final Type Member = InternalType.Member.getType();
     public static final Type Stack = InternalType.Stack.getType();
     public static final Type StackVersion = InternalType.StackVersion.getType();
+    public static final Type ExtensionLink = InternalType.ExtensionLink.getType();
+    public static final Type Extension = InternalType.Extension.getType();
+    public static final Type ExtensionVersion = InternalType.ExtensionVersion.getType();
     public static final Type OperatingSystem = InternalType.OperatingSystem.getType();
     public static final Type Repository = InternalType.Repository.getType();
     public static final Type StackService = InternalType.StackService.getType();

http://git-wip-us.apache.org/repos/asf/ambari/blob/9ce79716/ambari-server/src/main/java/org/apache/ambari/server/orm/dao/ExtensionDAO.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/orm/dao/ExtensionDAO.java b/ambari-server/src/main/java/org/apache/ambari/server/orm/dao/ExtensionDAO.java
new file mode 100644
index 0000000..6c6c3ae
--- /dev/null
+++ b/ambari-server/src/main/java/org/apache/ambari/server/orm/dao/ExtensionDAO.java
@@ -0,0 +1,168 @@
+/**
+ * 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.ambari.server.orm.dao;
+
+import java.util.List;
+
+import javax.persistence.EntityManager;
+import javax.persistence.TypedQuery;
+
+import org.apache.ambari.server.AmbariException;
+import org.apache.ambari.server.orm.RequiresSession;
+import org.apache.ambari.server.orm.entities.ExtensionEntity;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.persist.Transactional;
+
+/**
+ * The {@link ExtensionDAO} class is used to manage the persistence and retrieval of
+ * {@link ExtensionEntity} instances.
+ *
+ * An extension version is like a stack version but it contains custom services.  Linking an extension
+ * version to the current stack version allows the cluster to install the custom services contained in
+ * the extension version.
+ */
+@Singleton
+public class ExtensionDAO {
+
+  /**
+   * JPA entity manager
+   */
+  @Inject
+  private Provider<EntityManager> entityManagerProvider;
+
+  /**
+   * DAO utilities for dealing mostly with {@link TypedQuery} results.
+   */
+  @Inject
+  private DaoUtils daoUtils;
+
+  /**
+   * Gets a extension with the specified ID.
+   *
+   * @param extensionId
+   *          the ID of the extension to retrieve.
+   * @return the extension or {@code null} if none exists.
+   */
+  @RequiresSession
+  public ExtensionEntity findById(long extensionId) {
+    return entityManagerProvider.get().find(ExtensionEntity.class, extensionId);
+  }
+
+  /**
+   * Gets all of the defined extensions.
+   *
+   * @return all of the extensions loaded from resources or an empty list (never
+   *         {@code null}).
+   */
+  @RequiresSession
+  public List<ExtensionEntity> findAll() {
+    TypedQuery<ExtensionEntity> query = entityManagerProvider.get().createNamedQuery(
+        "ExtensionEntity.findAll", ExtensionEntity.class);
+
+    return daoUtils.selectList(query);
+  }
+
+  /**
+   * Gets the extension that matches the specified name and version.
+   *
+   * @return the extension matching the specified name and version or {@code null}
+   *         if none.
+   */
+  @RequiresSession
+  public ExtensionEntity find(String extensionName, String extensionVersion) {
+    TypedQuery<ExtensionEntity> query = entityManagerProvider.get().createNamedQuery(
+        "ExtensionEntity.findByNameAndVersion", ExtensionEntity.class);
+
+    query.setParameter("extensionName", extensionName);
+    query.setParameter("extensionVersion", extensionVersion);
+
+    return daoUtils.selectOne(query);
+  }
+
+  /**
+   * Persists a new extension instance.
+   *
+   * @param extension
+   *          the extension to persist (not {@code null}).
+   */
+  @Transactional
+  public void create(ExtensionEntity extension)
+      throws AmbariException {
+    EntityManager entityManager = entityManagerProvider.get();
+    entityManager.persist(extension);
+  }
+
+  /**
+   * Refresh the state of the extension instance from the database.
+   *
+   * @param extension
+   *          the extension to refresh (not {@code null}).
+   */
+  @Transactional
+  public void refresh(ExtensionEntity extension) {
+    entityManagerProvider.get().refresh(extension);
+  }
+
+  /**
+   * Merge the specified extension with the existing extension in the database.
+   *
+   * @param extension
+   *          the extension to merge (not {@code null}).
+   * @return the updated extension with merged content (never {@code null}).
+   */
+  @Transactional
+  public ExtensionEntity merge(ExtensionEntity extension) {
+    return entityManagerProvider.get().merge(extension);
+  }
+
+  /**
+   * Creates or updates the specified entity. This method will check
+   * {@link ExtensionEntity#getStackId()} in order to determine whether the entity
+   * should be created or merged.
+   *
+   * @param extension
+   *          the extension to create or update (not {@code null}).
+   */
+  public void createOrUpdate(ExtensionEntity extension)
+      throws AmbariException {
+    if (null == extension.getExtensionId()) {
+      create(extension);
+    } else {
+      merge(extension);
+    }
+  }
+
+  /**
+   * Removes the specified extension and all related clusters, services and
+   * components.
+   *
+   * @param extension
+   *          the extension to remove.
+   */
+  @Transactional
+  public void remove(ExtensionEntity extension) {
+    EntityManager entityManager = entityManagerProvider.get();
+    extension = findById(extension.getExtensionId());
+    if (null != extension) {
+      entityManager.remove(extension);
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/9ce79716/ambari-server/src/main/java/org/apache/ambari/server/orm/dao/ExtensionLinkDAO.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/orm/dao/ExtensionLinkDAO.java b/ambari-server/src/main/java/org/apache/ambari/server/orm/dao/ExtensionLinkDAO.java
new file mode 100644
index 0000000..d90480b
--- /dev/null
+++ b/ambari-server/src/main/java/org/apache/ambari/server/orm/dao/ExtensionLinkDAO.java
@@ -0,0 +1,240 @@
+/**
+ * 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.ambari.server.orm.dao;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.persistence.EntityManager;
+import javax.persistence.TypedQuery;
+
+import org.apache.ambari.server.AmbariException;
+import org.apache.ambari.server.controller.ExtensionLinkRequest;
+import org.apache.ambari.server.orm.RequiresSession;
+import org.apache.ambari.server.orm.entities.ExtensionLinkEntity;
+import org.apache.ambari.server.orm.entities.ExtensionLinkEntity;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.persist.Transactional;
+
+/**
+ * The {@link ExtensionLinkDAO} class is used to manage the persistence and retrieval of
+ * {@link ExtensionLinkEntity} instances.
+ *
+ * An extension version is like a stack version but it contains custom services.  Linking an extension
+ * version to the current stack version allows the cluster to install the custom services contained in
+ * the extension version.
+ */
+@Singleton
+public class ExtensionLinkDAO {
+
+  /**
+   * JPA entity manager
+   */
+  @Inject
+  private Provider<EntityManager> entityManagerProvider;
+
+  /**
+   * DAO utilities for dealing mostly with {@link TypedQuery} results.
+   */
+  @Inject
+  private DaoUtils daoUtils;
+
+
+  /**
+   * Gets the extension links that match the specified stack name and version.
+   *
+   * @return the extension links  matching the specified stack name and version if any.
+   */
+  @RequiresSession
+  public List<ExtensionLinkEntity> find(ExtensionLinkRequest request) {
+    if (request.getLinkId() != null) {
+      ExtensionLinkEntity entity = findById(new Long(request.getLinkId()));
+      List<ExtensionLinkEntity> list = new ArrayList<ExtensionLinkEntity>();
+      list.add(entity);
+      return list;
+    }
+
+    String stackName = request.getStackName();
+    String stackVersion = request.getStackName();
+    String extensionName = request.getStackName();
+    String extensionVersion = request.getStackName();
+
+    if (stackName != null && stackVersion != null) {
+      if (extensionName != null && extensionVersion != null) {
+        ExtensionLinkEntity entity = findByStackAndExtension(stackName, stackVersion, extensionName, extensionVersion);
+        List<ExtensionLinkEntity> list = new ArrayList<ExtensionLinkEntity>();
+        list.add(entity);
+        return list;
+      }
+      return findByStack(stackName, stackVersion);
+    }
+    if (extensionName != null && extensionVersion != null) {
+      return findByExtension(extensionName, extensionVersion);
+    }
+
+    return findAll();
+  }
+
+  /**
+   * Gets an extension link with the specified ID.
+   *
+   * @param linkId
+   *          the ID of the extension link to retrieve.
+   * @return the extension or {@code null} if none exists.
+   */
+  @RequiresSession
+  public ExtensionLinkEntity findById(long linkId) {
+    return entityManagerProvider.get().find(ExtensionLinkEntity.class, linkId);
+  }
+
+  /**
+   * Gets all of the defined extension links.
+   *
+   * @return all of the extension links loaded from resources or an empty list (never
+   *         {@code null}).
+   */
+  @RequiresSession
+  public List<ExtensionLinkEntity> findAll() {
+    TypedQuery<ExtensionLinkEntity> query = entityManagerProvider.get().createNamedQuery(
+        "ExtensionLinkEntity.findAll", ExtensionLinkEntity.class);
+
+    return daoUtils.selectList(query);
+  }
+
+  /**
+   * Gets the extension links that match the specified extension name and version.
+   *
+   * @return the extension links matching the specified extension name and version if any.
+   */
+  @RequiresSession
+  public List<ExtensionLinkEntity> findByExtension(String extensionName, String extensionVersion) {
+    TypedQuery<ExtensionLinkEntity> query = entityManagerProvider.get().createNamedQuery(
+        "ExtensionLinkEntity.findByExtension", ExtensionLinkEntity.class);
+
+    query.setParameter("extensionName", extensionName);
+    query.setParameter("extensionVersion", extensionVersion);
+
+    return daoUtils.selectList(query);
+  }
+
+  /**
+   * Gets the extension links that match the specified stack name and version.
+   *
+   * @return the extension links  matching the specified stack name and version if any.
+   */
+  @RequiresSession
+  public List<ExtensionLinkEntity> findByStack(String stackName, String stackVersion) {
+    TypedQuery<ExtensionLinkEntity> query = entityManagerProvider.get().createNamedQuery(
+        "ExtensionLinkEntity.findByStack", ExtensionLinkEntity.class);
+
+    query.setParameter("stackName", stackName);
+    query.setParameter("stackVersion", stackVersion);
+
+    return daoUtils.selectList(query);
+  }
+
+  /**
+   * Gets the extension link that match the specified stack name, stack version, extension name and extension version.
+   *
+   * @return the extension link matching the specified stack name, stack version, extension name and extension version if any.
+   */
+  @RequiresSession
+  public ExtensionLinkEntity findByStackAndExtension(String stackName, String stackVersion, String extensionName, String extensionVersion) {
+    TypedQuery<ExtensionLinkEntity> query = entityManagerProvider.get().createNamedQuery(
+        "ExtensionLinkEntity.findByStackAndExtension", ExtensionLinkEntity.class);
+
+    query.setParameter("stackName", stackName);
+    query.setParameter("stackVersion", stackVersion);
+    query.setParameter("extensionName", extensionName);
+    query.setParameter("extensionVersion", extensionVersion);
+
+    return daoUtils.selectOne(query);
+  }
+
+  /**
+   * Persists a new extension link instance.
+   *
+   * @param link
+   *          the extension link to persist (not {@code null}).
+   */
+  @Transactional
+  public void create(ExtensionLinkEntity link)
+      throws AmbariException {
+    EntityManager entityManager = entityManagerProvider.get();
+    entityManager.persist(link);
+  }
+
+  /**
+   * Refresh the state of the extension instance from the database.
+   *
+   * @param link
+   *          the extension link to refresh (not {@code null}).
+   */
+  @Transactional
+  public void refresh(ExtensionLinkEntity link) {
+    entityManagerProvider.get().refresh(link);
+  }
+
+  /**
+   * Merge the specified extension link with the existing extension link in the database.
+   *
+   * @param link
+   *          the extension link to merge (not {@code null}).
+   * @return the updated extension link with merged content (never {@code null}).
+   */
+  @Transactional
+  public ExtensionLinkEntity merge(ExtensionLinkEntity link) {
+    return entityManagerProvider.get().merge(link);
+  }
+
+  /**
+   * Creates or updates the specified entity. This method will check
+   * {@link ExtensionLinkEntity#getLinkId()} in order to determine whether the entity
+   * should be created or merged.
+   *
+   * @param extension
+   *          the link to create or update (not {@code null}).
+   */
+  public void createOrUpdate(ExtensionLinkEntity link)
+      throws AmbariException {
+    if (null == link.getLinkId()) {
+      create(link);
+    } else {
+      merge(link);
+    }
+  }
+
+  /**
+   * Removes the specified extension link
+   *
+   * @param link
+   *          the extension link to remove.
+   */
+  @Transactional
+  public void remove(ExtensionLinkEntity link) {
+    EntityManager entityManager = entityManagerProvider.get();
+    link = findById(link.getLinkId());
+    if (null != link) {
+      entityManager.remove(link);
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/9ce79716/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ExtensionEntity.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ExtensionEntity.java b/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ExtensionEntity.java
new file mode 100644
index 0000000..870d941
--- /dev/null
+++ b/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ExtensionEntity.java
@@ -0,0 +1,156 @@
+/**
+ * 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.ambari.server.orm.entities;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.Table;
+import javax.persistence.TableGenerator;
+import javax.persistence.UniqueConstraint;
+
+/**
+ * The {@link ExtensionEntity} class is used to model an extension to the stack.
+ *
+ * An extension version is like a stack version but it contains custom services.  Linking an extension
+ * version to the current stack version allows the cluster to install the custom services contained in
+ * the extension version.
+ */
+@Entity
+@Table(name = "extension", uniqueConstraints = @UniqueConstraint(columnNames = {
+    "extension_name", "extension_version" }))
+@TableGenerator(name = "extension_id_generator", table = "ambari_sequences", pkColumnName = "sequence_name", valueColumnName = "sequence_value", pkColumnValue = "extension_id_seq", initialValue = 0)
+@NamedQueries({
+    @NamedQuery(name = "ExtensionEntity.findAll", query = "SELECT extension FROM ExtensionEntity extension"),
+    @NamedQuery(name = "ExtensionEntity.findByNameAndVersion", query = "SELECT extension FROM ExtensionEntity extension WHERE extension.extensionName = :extensionName AND extension.extensionVersion = :extensionVersion") })
+public class ExtensionEntity {
+
+  @Id
+  @GeneratedValue(strategy = GenerationType.TABLE, generator = "extension_id_generator")
+  @Column(name = "extension_id", nullable = false, updatable = false)
+  private Long extensionId;
+
+  @Column(name = "extension_name", length = 255, nullable = false)
+  private String extensionName;
+
+  @Column(name = "extension_version", length = 255, nullable = false)
+  private String extensionVersion;
+
+  /**
+   * Constructor.
+   */
+  public ExtensionEntity() {
+  }
+
+  /**
+   * Gets the unique identifier for this extension.
+   *
+   * @return the ID.
+   */
+  public Long getExtensionId() {
+    return extensionId;
+  }
+
+  /**
+   * Gets the name of the extension.
+   *
+   * @return the name of the extension (never {@code null}).
+   */
+  public String getExtensionName() {
+    return extensionName;
+  }
+
+  /**
+   * Sets the name of the extension.
+   *
+   * @param extensionName
+   *          the extension name (not {@code null}).
+   */
+  public void setExtensionName(String extensionName) {
+    this.extensionName = extensionName;
+  }
+
+  /**
+   * Gets the version of the extension.
+   *
+   * @return the extension version (never {@code null}).
+   */
+  public String getExtensionVersion() {
+    return extensionVersion;
+  }
+
+  /**
+   * Sets the version of the extension.
+   *
+   * @param extensionVersion
+   *          the extension version (not {@code null}).
+   */
+  public void setExtensionVersion(String extensionVersion) {
+    this.extensionVersion = extensionVersion;
+  }
+
+  /**
+   *
+   */
+  @Override
+  public boolean equals(Object object) {
+    if (this == object) {
+      return true;
+    }
+
+    if (object == null || getClass() != object.getClass()) {
+      return false;
+    }
+
+    ExtensionEntity that = (ExtensionEntity) object;
+
+    if (extensionId != null ? !extensionId.equals(that.extensionId) : that.extensionId != null) {
+      return false;
+    }
+
+    return true;
+  }
+
+  /**
+   *
+   */
+  @Override
+  public int hashCode() {
+    int result = null != extensionId ? extensionId.hashCode() : 0;
+    return result;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public String toString() {
+    StringBuilder buffer = new StringBuilder();
+    buffer.append(getClass().getSimpleName());
+    buffer.append("{");
+    buffer.append("id=").append(extensionId);
+    buffer.append(", name=").append(extensionName);
+    buffer.append(", version=").append(extensionVersion);
+    buffer.append("}");
+    return buffer.toString();
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/9ce79716/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ExtensionLinkEntity.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ExtensionLinkEntity.java b/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ExtensionLinkEntity.java
new file mode 100644
index 0000000..12b3ce0
--- /dev/null
+++ b/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ExtensionLinkEntity.java
@@ -0,0 +1,139 @@
+/**
+ * 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.ambari.server.orm.entities;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.OneToOne;
+import javax.persistence.Table;
+import javax.persistence.TableGenerator;
+import javax.persistence.UniqueConstraint;
+
+/**
+ * The {@link ExtensionLinkEntity} class is used to model the extensions linked to the stack.
+ *
+ * An extension version is like a stack version but it contains custom services.  Linking an extension
+ * version to the current stack version allows the cluster to install the custom services contained in
+ * the extension version.
+ */
+@Table(name = "extensionlink", uniqueConstraints = @UniqueConstraint(columnNames = {
+		"stack_id", "extension_id" }))
+@TableGenerator(name = "link_id_generator", table = "ambari_sequences", pkColumnName = "sequence_name", valueColumnName = "sequence_value", pkColumnValue = "link_id_seq", initialValue = 0)
+@NamedQueries({
+    @NamedQuery(name = "ExtensionLinkEntity.findAll", query = "SELECT link FROM ExtensionLinkEntity link"),
+    @NamedQuery(name = "ExtensionLinkEntity.findByStackAndExtension", query = "SELECT link FROM ExtensionLinkEntity link WHERE link.stack.stackName = :stackName AND link.stack.stackVersion = :stackVersion AND link.extension.extensionName = :extensionName AND link.extension.extensionVersion = :extensionVersion"),
+    @NamedQuery(name = "ExtensionLinkEntity.findByStack", query = "SELECT link FROM ExtensionLinkEntity link WHERE link.stack.stackName = :stackName AND link.stack.stackVersion = :stackVersion"),
+    @NamedQuery(name = "ExtensionLinkEntity.findByExtension", query = "SELECT link FROM ExtensionLinkEntity link WHERE link.extension.extensionName = :extensionName AND link.extension.extensionVersion = :extensionVersion") })
+@Entity
+public class ExtensionLinkEntity {
+
+  @Id
+  @GeneratedValue(strategy = GenerationType.TABLE, generator = "link_id_generator")
+  @Column(name = "link_id", nullable = false, updatable = false)
+  private Long linkId;
+
+  @OneToOne
+  @JoinColumn(name = "stack_id", unique = false, nullable = false, insertable = true, updatable = false)
+  private StackEntity stack;
+
+  @OneToOne
+  @JoinColumn(name = "extension_id", unique = false, nullable = false, insertable = true, updatable = false)
+  private ExtensionEntity extension;
+
+  /**
+   * Constructor.
+   */
+  public ExtensionLinkEntity() {
+  }
+
+  public Long getLinkId() {
+    return linkId;
+  }
+
+  public void setLinkId(Long linkId) {
+    this.linkId = linkId;
+  }
+
+  public StackEntity getStack() {
+    return stack;
+  }
+
+  public void setStack(StackEntity stack) {
+    this.stack = stack;
+  }
+
+  public ExtensionEntity getExtension() {
+    return extension;
+  }
+
+  public void setExtension(ExtensionEntity extension) {
+    this.extension = extension;
+  }
+
+  /**
+   *
+   */
+  @Override
+  public boolean equals(Object object) {
+    if (this == object) {
+      return true;
+    }
+
+    if (object == null || getClass() != object.getClass()) {
+      return false;
+    }
+
+    ExtensionLinkEntity that = (ExtensionLinkEntity) object;
+
+    if (linkId != null ? !linkId.equals(that.linkId) : that.linkId != null) {
+      return false;
+    }
+
+    return true;
+  }
+
+  /**
+   *
+   */
+  @Override
+  public int hashCode() {
+    int result = (null != linkId) ? linkId.hashCode() : 0;
+    return result;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public String toString() {
+    StringBuilder buffer = new StringBuilder();
+    buffer.append(getClass().getSimpleName());
+    buffer.append("{");
+    buffer.append("linkId=").append(linkId);
+    buffer.append(", stackId=").append(stack.getStackId());
+    buffer.append(", extensionId=").append(extension.getExtensionId());
+    buffer.append("}");
+    return buffer.toString();
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/9ce79716/ambari-server/src/main/java/org/apache/ambari/server/stack/BaseModule.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/stack/BaseModule.java b/ambari-server/src/main/java/org/apache/ambari/server/stack/BaseModule.java
index ef2438f..0e1116f 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/stack/BaseModule.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/stack/BaseModule.java
@@ -60,7 +60,7 @@ public abstract class BaseModule<T, I> implements StackDefinitionModule<T, I> {
    * @return collection of the merged modules
    */
   protected <T extends StackDefinitionModule<T, ?>> Collection<T> mergeChildModules(
-      Map<String, StackModule> allStacks, Map<String, ServiceModule> commonServices, Map<String, T> modules, Map<String, T> parentModules)
+      Map<String, StackModule> allStacks, Map<String, ServiceModule> commonServices, Map<String, ExtensionModule> extensions, Map<String, T> modules, Map<String, T> parentModules)
         throws AmbariException {
     Set<String> addedModules = new HashSet<String>();
     Collection<T> mergedModules = new HashSet<T>();
@@ -70,7 +70,7 @@ public abstract class BaseModule<T, I> implements StackDefinitionModule<T, I> {
       addedModules.add(id);
       if (!module.isDeleted()) {
         if (parentModules.containsKey(id)) {
-          module.resolve(parentModules.get(id), allStacks, commonServices);
+          module.resolve(parentModules.get(id), allStacks, commonServices, extensions);
         }
         mergedModules.add(module);
       }

http://git-wip-us.apache.org/repos/asf/ambari/blob/9ce79716/ambari-server/src/main/java/org/apache/ambari/server/stack/CommonServiceDirectory.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/stack/CommonServiceDirectory.java b/ambari-server/src/main/java/org/apache/ambari/server/stack/CommonServiceDirectory.java
index cbbdb91..cdedbb4 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/stack/CommonServiceDirectory.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/stack/CommonServiceDirectory.java
@@ -62,15 +62,13 @@ public class CommonServiceDirectory extends ServiceDirectory {
 
   @Override
   /**
-   * Parse common service directory
+   * Calculate the common service directories
    * packageDir Format: common-services/<serviceName>/<serviceVersion>/package
    * Example:
    *  directory: "/var/lib/ambari-server/resources/common-services/HDFS/1.0"
    *  packageDir: "common-services/HDFS/1.0/package"
-   *
-   * @throws AmbariException
    */
-  protected void parsePath() throws AmbariException {
+  protected void calculateDirectories() {
     File serviceVersionDir = new File(getAbsolutePath());
     File serviceDir = serviceVersionDir.getParentFile();
 
@@ -95,6 +93,5 @@ public class CommonServiceDirectory extends ServiceDirectory {
       LOG.debug(String.format("Service upgrades folder %s for common service %s does not exist.",
           absUpgradesDir, serviceId ));
     }
-    parseMetaInfoFile();
   }
 }

http://git-wip-us.apache.org/repos/asf/ambari/blob/9ce79716/ambari-server/src/main/java/org/apache/ambari/server/stack/ComponentModule.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/stack/ComponentModule.java b/ambari-server/src/main/java/org/apache/ambari/server/stack/ComponentModule.java
index 65da145..d9d3105 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/stack/ComponentModule.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/stack/ComponentModule.java
@@ -58,7 +58,8 @@ public class ComponentModule extends BaseModule<ComponentModule, ComponentInfo>
   }
 
   @Override
-  public void resolve(ComponentModule parent, Map<String, StackModule> allStacks, Map<String, ServiceModule> commonServices) {
+  public void resolve(ComponentModule parent, Map<String, StackModule> allStacks,
+	    Map<String, ServiceModule> commonServices, Map<String, ExtensionModule> extensions) {
     if (parent != null) {
       ComponentInfo parentInfo = parent.getModuleInfo();
       if (!parent.isValid()) {

http://git-wip-us.apache.org/repos/asf/ambari/blob/9ce79716/ambari-server/src/main/java/org/apache/ambari/server/stack/ConfigurationModule.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/stack/ConfigurationModule.java b/ambari-server/src/main/java/org/apache/ambari/server/stack/ConfigurationModule.java
index 9c4e9d1..543c1ec 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/stack/ConfigurationModule.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/stack/ConfigurationModule.java
@@ -82,7 +82,8 @@ public class ConfigurationModule extends BaseModule<ConfigurationModule, Configu
   }
 
   @Override
-  public void resolve(ConfigurationModule parent, Map<String, StackModule> allStacks, Map<String, ServiceModule> commonServices) throws AmbariException {
+  public void resolve(ConfigurationModule parent, Map<String, StackModule> allStacks,
+	    Map<String, ServiceModule> commonServices, Map<String, ExtensionModule> extensions) throws AmbariException {
     // merge properties also removes deleted props so should be called even if extension is disabled
     if (parent != null) {
       if (parent.info != null) {

http://git-wip-us.apache.org/repos/asf/ambari/blob/9ce79716/ambari-server/src/main/java/org/apache/ambari/server/stack/ExtensionDirectory.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/stack/ExtensionDirectory.java b/ambari-server/src/main/java/org/apache/ambari/server/stack/ExtensionDirectory.java
new file mode 100644
index 0000000..f2647fd
--- /dev/null
+++ b/ambari-server/src/main/java/org/apache/ambari/server/stack/ExtensionDirectory.java
@@ -0,0 +1,196 @@
+/**
+ * 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.ambari.server.stack;
+
+import org.apache.ambari.server.AmbariException;
+import org.apache.ambari.server.api.services.AmbariMetaInfo;
+import org.apache.ambari.server.state.stack.ExtensionMetainfoXml;
+import org.apache.ambari.server.state.stack.RepositoryXml;
+import org.apache.ambari.server.state.stack.StackRoleCommandOrder;
+import org.apache.ambari.server.state.stack.UpgradePack;
+import org.apache.commons.io.FilenameUtils;
+import org.codehaus.jackson.map.ObjectMapper;
+import org.codehaus.jackson.type.TypeReference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.xml.bind.JAXBException;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+
+/**
+ * Encapsulates IO operations on a extension definition extension directory.
+ *
+ * An extension version is like a stack version but it contains custom services.  Linking an extension
+ * version to the current stack version allows the cluster to install the custom services contained in
+ * the extension version.
+ */
+//todo: Normalize all path return values.
+//todo: Currently some are relative and some are absolute.
+//todo: Current values were dictated by the ExtensionInfo expectations.
+public class ExtensionDirectory extends StackDefinitionDirectory {
+
+  /**
+   * collection of service directories
+   */
+  private Collection<ServiceDirectory> serviceDirectories;
+
+  /**
+   * metainfo file representation
+   */
+  private ExtensionMetainfoXml metaInfoXml;
+
+  /**
+   * file unmarshaller
+   */
+  ModuleFileUnmarshaller unmarshaller = new ModuleFileUnmarshaller();
+
+  /**
+   * extensions directory name
+   */
+  public final static String EXTENSIONS_FOLDER_NAME = "extensions";
+
+  /**
+   * metainfo file name
+   */
+  private static final String EXTENSION_METAINFO_FILE_NAME = "metainfo.xml";
+
+  /**
+   * logger instance
+   */
+  private final static Logger LOG = LoggerFactory.getLogger(ExtensionDirectory.class);
+
+
+  /**
+   * Constructor.
+   *
+   * @param directory  extension directory
+   * @throws AmbariException if unable to parse the stack directory
+   */
+  public ExtensionDirectory(String directory) throws AmbariException {
+    super(directory);
+    parsePath();
+  }
+
+  /**
+   * Obtain the extension directory name.
+   *
+   * @return extension directory name
+   */
+  public String getExtensionDirName() {
+    return getDirectory().getParentFile().getName();
+  }
+
+  /**
+   * Obtain the object representation of the extension metainfo.xml file.
+   *
+   * @return object representation of the extension metainfo.xml file
+   */
+  public ExtensionMetainfoXml getMetaInfoFile() {
+    return metaInfoXml;
+  }
+
+  /**
+   * Obtain a collection of all service directories.
+   *
+   * @return collection of all service directories
+   */
+  public Collection<ServiceDirectory> getServiceDirectories() {
+    return serviceDirectories;
+  }
+
+  /**
+   * Parse the extension directory.
+   *
+   * @throws AmbariException if unable to parse the directory
+   */
+  private void parsePath() throws AmbariException {
+    Collection<String> subDirs = Arrays.asList(directory.list());
+    parseServiceDirectories(subDirs);
+    parseMetaInfoFile();
+  }
+
+  /**
+   * Parse the extension metainfo file.
+   *
+   * @throws AmbariException if unable to parse the extension metainfo file
+   */
+  private void parseMetaInfoFile() throws AmbariException {
+    File extensionMetaInfoFile = new File(getAbsolutePath()
+        + File.separator + EXTENSION_METAINFO_FILE_NAME);
+
+    //todo: is it ok for this file not to exist?
+    if (extensionMetaInfoFile.exists()) {
+      if (LOG.isDebugEnabled()) {
+        LOG.debug("Reading extension version metainfo from file " + extensionMetaInfoFile.getAbsolutePath());
+      }
+
+      try {
+        metaInfoXml = unmarshaller.unmarshal(ExtensionMetainfoXml.class, extensionMetaInfoFile);
+      } catch (JAXBException e) {
+        metaInfoXml = new ExtensionMetainfoXml();
+        metaInfoXml.setValid(false);
+        metaInfoXml.addError("Unable to parse extension metainfo.xml file at location: " +
+            extensionMetaInfoFile.getAbsolutePath());
+      }
+    }
+  }
+
+  /**
+   * Parse the extension's service directories extension
+   * @param subDirs  extension sub directories
+   * @throws AmbariException  if unable to parse the service directories
+   */
+  private void parseServiceDirectories(Collection<String> subDirs) throws AmbariException {
+    Collection<ServiceDirectory> dirs = new HashSet<ServiceDirectory>();
+
+    if (subDirs.contains(ServiceDirectory.SERVICES_FOLDER_NAME)) {
+      String servicesDir = getAbsolutePath() + File.separator + ServiceDirectory.SERVICES_FOLDER_NAME;
+      File baseServiceDir = new File(servicesDir);
+      File[] serviceFolders = baseServiceDir.listFiles(AmbariMetaInfo.FILENAME_FILTER);
+      if (serviceFolders != null) {
+        for (File d : serviceFolders) {
+          if (d.isDirectory()) {
+            try {
+              dirs.add(new StackServiceDirectory(d.getAbsolutePath()));
+            } catch (AmbariException e) {
+              //todo: this seems as though we should propagate this exception
+              //todo: eating it now to keep backwards compatibility
+              LOG.warn(String.format("Unable to parse extension definition service at '%s'.  Ignoring service. : %s",
+                  d.getAbsolutePath(), e.toString()));
+            }
+          }
+        }
+      }
+    }
+
+    if (dirs.isEmpty()) {
+      //todo: what does it mean for a extension to have no services?
+      LOG.info("The extension defined at '" + getAbsolutePath() + "' contains no services");
+    }
+    serviceDirectories = dirs;
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/9ce79716/ambari-server/src/main/java/org/apache/ambari/server/stack/ExtensionHelper.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/stack/ExtensionHelper.java b/ambari-server/src/main/java/org/apache/ambari/server/stack/ExtensionHelper.java
new file mode 100644
index 0000000..cd4d9f3
--- /dev/null
+++ b/ambari-server/src/main/java/org/apache/ambari/server/stack/ExtensionHelper.java
@@ -0,0 +1,167 @@
+/**
+ * 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.ambari.server.stack;
+
+import org.apache.ambari.server.AmbariException;
+import org.apache.ambari.server.ServiceNotFoundException;
+import org.apache.ambari.server.state.Cluster;
+import org.apache.ambari.server.state.Clusters;
+import org.apache.ambari.server.state.ExtensionInfo;
+import org.apache.ambari.server.state.ServiceInfo;
+import org.apache.ambari.server.state.StackInfo;
+import org.apache.ambari.server.state.stack.ExtensionMetainfoXml;
+import org.apache.ambari.server.utils.VersionUtils;
+
+/**
+ * An extension version is like a stack version but it contains custom services.  Linking an extension
+ * version to the current stack version allows the cluster to install the custom services contained in
+ * the extension version.
+ */
+public class ExtensionHelper {
+
+  public static void validateDeleteLink(Clusters clusters, StackInfo stack, ExtensionInfo extension) throws AmbariException {
+    validateNotRequiredExtension(stack, extension);
+    validateServicesNotInstalled(clusters, stack, extension);
+  }
+
+  private static void validateServicesNotInstalled(Clusters clusters, StackInfo stack, ExtensionInfo extension) throws AmbariException {
+    for (Cluster cluster : clusters.getClusters().values()) {
+      for (ServiceInfo service : extension.getServices()) {
+        try {
+          if (service != null && cluster.getService(service.getName()) != null) {
+            String message = "Extension service is still installed"
+                        + ", stackName=" + stack.getName()
+                        + ", stackVersion=" + stack.getVersion()
+                        + ", service=" + service.getName()
+                        + ", extensionName=" + extension.getName()
+                        + ", extensionVersion=" + extension.getVersion();
+
+            throw new AmbariException(message);
+          }
+        }
+        catch (ServiceNotFoundException e) {
+          //Eat the exception
+        }
+      }
+    }
+  }
+
+  public static void validateCreateLink(StackInfo stack, ExtensionInfo extension) throws AmbariException {
+    validateSupportedStackVersion(stack, extension);
+    validateServiceDuplication(stack, extension);
+    validateRequiredExtensions(stack, extension);
+  }
+
+  private static void validateSupportedStackVersion(StackInfo stack, ExtensionInfo extension) throws AmbariException {
+    for (ExtensionMetainfoXml.Stack validStack : extension.getStacks()) {
+      if (validStack.getName().equals(stack.getName())) {
+        String minStackVersion = validStack.getVersion();
+        if (VersionUtils.compareVersions(stack.getVersion(), minStackVersion) >= 0) {
+          //Found a supported stack version
+          return;
+        }
+      }
+    }
+
+    String message = "Stack is not supported by extension"
+		+ ", stackName=" + stack.getName()
+		+ ", stackVersion=" + stack.getVersion()
+		+ ", extensionName=" + extension.getName()
+		+ ", extensionVersion=" + extension.getVersion();
+
+    throw new AmbariException(message);
+  }
+
+  private static void validateServiceDuplication(StackInfo stack, ExtensionInfo extension) throws AmbariException {
+    for (ServiceInfo service : extension.getServices()) {
+      if (service != null) {
+        ServiceInfo stackService = null;
+        try {
+          stackService = stack.getService(service.getName());
+        }
+        catch (Exception e) {
+          //Eat the exception
+        }
+        if (stackService != null) {
+          String message = "Existing service is included in extension"
+                      + ", stackName=" + stack.getName()
+                      + ", stackVersion=" + stack.getVersion()
+                      + ", service=" + service.getName()
+                      + ", extensionName=" + extension.getName()
+                      + ", extensionVersion=" + extension.getVersion();
+
+          throw new AmbariException(message);
+        }
+      }
+    }
+  }
+
+  private static void validateRequiredExtensions(StackInfo stack, ExtensionInfo extension) throws AmbariException {
+    for (ExtensionMetainfoXml.Extension requiredExtension : extension.getExtensions()) {
+      if (requiredExtension != null) {
+        String message = "Stack has not linked required extension"
+                    + ", stackName=" + stack.getName()
+                    + ", stackVersion=" + stack.getVersion()
+                    + ", extensionName=" + extension.getName()
+                    + ", extensionVersion=" + extension.getVersion()
+                    + ", requiredExtensionName=" + requiredExtension.getName()
+                    + ", requiredExtensionVersion=" + requiredExtension.getVersion();
+        try {
+          ExtensionInfo stackExtension = stack.getExtension(requiredExtension.getName());
+          if (stackExtension != null) {
+            String version = requiredExtension.getVersion();
+            if (version.endsWith("*")) {
+              version = version.substring(0, version.length() - 1);
+              if (!stackExtension.getVersion().startsWith(version)) {
+                throw new AmbariException(message);
+              }
+            }
+            else if (!stackExtension.getVersion().equals(version)) {
+              throw new AmbariException(message);
+            }
+          }
+        }
+        catch (Exception e) {
+          throw new AmbariException(message, e);
+        }
+      }
+    }
+  }
+
+  private static void validateNotRequiredExtension(StackInfo stack, ExtensionInfo extension) throws AmbariException {
+    for (ExtensionInfo stackExtension : stack.getExtensions()) {
+      if (stackExtension != null) {
+        for (ExtensionMetainfoXml.Extension requiredExtension : stackExtension.getExtensions()) {
+          if (requiredExtension != null && requiredExtension.getName().equals(extension.getName())) {
+            String message = "Stack extension is required by extension"
+                        + ", stackName=" + stack.getName()
+                        + ", stackVersion=" + stack.getVersion()
+                        + ", extensionName=" + extension.getName()
+                        + ", extensionVersion=" + extension.getVersion()
+                        + ", dependentExtensionName=" + stackExtension.getName()
+                        + ", dependentExtensionVersion=" + stackExtension.getVersion();
+
+            throw new AmbariException(message);
+          }
+        }
+      }
+    }
+  }
+
+}


Mime
View raw message