You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by al...@apache.org on 2016/06/20 21:34:45 UTC

[3/6] ambari git commit: AMBARI-12885. Dynamic stack extensions - install and upgrade support for custom services (Tim Thorpe via alejandro)

http://git-wip-us.apache.org/repos/asf/ambari/blob/300a7e21/ambari-server/src/main/java/org/apache/ambari/server/state/ExtensionId.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/state/ExtensionId.java b/ambari-server/src/main/java/org/apache/ambari/server/state/ExtensionId.java
new file mode 100644
index 0000000..b17da30
--- /dev/null
+++ b/ambari-server/src/main/java/org/apache/ambari/server/state/ExtensionId.java
@@ -0,0 +1,160 @@
+/**
+ * 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.state;
+
+import org.apache.ambari.server.orm.entities.ExtensionEntity;
+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 ExtensionId implements Comparable<ExtensionId> {
+
+  private static final String NAME_SEPARATOR = "-";
+
+  private String extensionName;
+  private String extensionVersion;
+
+  public ExtensionId() {
+    extensionName = "";
+    extensionVersion = "";
+  }
+
+  public ExtensionId(String extensionId) {
+    parseExtensionIdHelper(this, extensionId);
+  }
+
+  public ExtensionId(ExtensionInfo extension) {
+    extensionName = extension.getName();
+    extensionVersion = extension.getVersion();
+  }
+
+  public ExtensionId(String extensionName, String extensionVersion) {
+    this(extensionName + NAME_SEPARATOR + extensionVersion);
+  }
+
+  public ExtensionId(ExtensionEntity entity) {
+    this(entity.getExtensionName(), entity.getExtensionVersion());
+  }
+
+  /**
+   * @return the extensionName
+   */
+  public String getExtensionName() {
+    return extensionName;
+  }
+
+  /**
+   * @return the extensionVersion
+   */
+  public String getExtensionVersion() {
+    return extensionVersion;
+  }
+
+  /**
+   * @return the extensionVersion
+   */
+  public String getExtensionId() {
+    if (extensionName.isEmpty()
+        && extensionVersion.isEmpty()) {
+      return "";
+    }
+    return extensionName + NAME_SEPARATOR + extensionVersion;
+  }
+
+  /**
+   * @param extensionId the extensionVersion to set
+   */
+  public void setExtensionId(String extensionId) {
+    parseExtensionIdHelper(this, extensionId);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean equals(Object object) {
+    if (!(object instanceof ExtensionId)) {
+      return false;
+    }
+    if (this == object) {
+      return true;
+    }
+    ExtensionId s = (ExtensionId) object;
+    return extensionName.equals(s.extensionName) && extensionVersion.equals(s.extensionVersion);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public int hashCode() {
+    int result = extensionName != null ? extensionName.hashCode() : 0;
+    result = 31 * result + (extensionVersion != null ? extensionVersion.hashCode() : 0);
+    return result;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public int compareTo(ExtensionId other) {
+    if (this == other) {
+      return 0;
+    }
+
+    if (other == null) {
+      throw new RuntimeException("Cannot compare with a null value.");
+    }
+
+    int returnValue = getExtensionName().compareTo(other.getExtensionName());
+    if (returnValue == 0) {
+      returnValue = VersionUtils.compareVersions(getExtensionVersion(), other.getExtensionVersion());
+    } else {
+      throw new RuntimeException("ExtensionId with different names cannot be compared.");
+    }
+    return returnValue;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public String toString() {
+    return getExtensionId();
+  }
+
+  private void parseExtensionIdHelper(ExtensionId extensionVersion, String extensionId) {
+    if (extensionId == null || extensionId.isEmpty()) {
+      extensionVersion.extensionName = "";
+      extensionVersion.extensionVersion = "";
+      return;
+    }
+
+    int pos = extensionId.indexOf('-');
+    if (pos == -1 || (extensionId.length() <= (pos + 1))) {
+      throw new RuntimeException("Could not parse invalid Extension Id" + ", extensionId=" + extensionId);
+    }
+
+    extensionVersion.extensionName = extensionId.substring(0, pos);
+    extensionVersion.extensionVersion = extensionId.substring(pos + 1);
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/300a7e21/ambari-server/src/main/java/org/apache/ambari/server/state/ExtensionInfo.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/state/ExtensionInfo.java b/ambari-server/src/main/java/org/apache/ambari/server/state/ExtensionInfo.java
new file mode 100644
index 0000000..89a6fb5
--- /dev/null
+++ b/ambari-server/src/main/java/org/apache/ambari/server/state/ExtensionInfo.java
@@ -0,0 +1,208 @@
+/**
+ * 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.state;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.ambari.server.controller.ExtensionVersionResponse;
+import org.apache.ambari.server.stack.Validable;
+import org.apache.ambari.server.state.stack.ExtensionMetainfoXml;
+
+/**
+ * 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 ExtensionInfo implements Comparable<ExtensionInfo>, Validable{
+  private String name;
+  private String version;
+  private Collection<ServiceInfo> services;
+  private String parentExtensionVersion;
+
+  private List<ExtensionMetainfoXml.Stack> stacks;
+  private List<ExtensionMetainfoXml.Extension> extensions;
+  private boolean valid = true;
+
+  /**
+   *
+   * @return valid xml flag
+   */
+  @Override
+  public boolean isValid() {
+    return valid;
+  }
+
+  /**
+   *
+   * @param valid set validity flag
+   */
+  @Override
+  public void setValid(boolean valid) {
+    this.valid = valid;
+  }
+
+  private Set<String> errorSet = new HashSet<String>();
+
+  @Override
+  public void addError(String error) {
+    errorSet.add(error);
+  }
+
+  @Override
+  public Collection<String> getErrors() {
+    return errorSet;
+  }
+
+  @Override
+  public void addErrors(Collection<String> errors) {
+    this.errorSet.addAll(errors);
+  }
+
+  //private String stackHooksFolder;
+
+  private String upgradesFolder = null;
+
+  private volatile Map<String, PropertyInfo> requiredProperties;
+
+  public String getName() {
+    return name;
+  }
+
+  public void setName(String name) {
+    this.name = name;
+  }
+
+  public String getVersion() {
+    return version;
+  }
+
+  public void setVersion(String version) {
+    this.version = version;
+  }
+
+  public synchronized Collection<ServiceInfo> getServices() {
+    if (services == null) services = new ArrayList<ServiceInfo>();
+    return services;
+  }
+
+  public ServiceInfo getService(String name) {
+    Collection<ServiceInfo> services = getServices();
+    for (ServiceInfo service : services) {
+      if (service.getName().equals(name)) {
+        return service;
+      }
+    }
+    //todo: exception?
+    return null;
+  }
+
+  public synchronized void setServices(Collection<ServiceInfo> services) {
+    this.services = services;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder("Extension name:" + name + "\nversion:" +
+      version + " \nvalid:" + isValid());
+    if (services != null) {
+      sb.append("\n\t\tService:");
+      for (ServiceInfo service : services) {
+        sb.append("\t\t");
+        sb.append(service);
+      }
+    }
+
+    return sb.toString();
+  }
+
+
+  @Override
+  public int hashCode() {
+    return 31  + name.hashCode() + version.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (!(obj instanceof ExtensionInfo)) {
+      return false;
+    }
+    if (this == obj) {
+      return true;
+    }
+    ExtensionInfo extInfo = (ExtensionInfo) obj;
+    return getName().equals(extInfo.getName()) && getVersion().equals(extInfo.getVersion());
+  }
+
+  public ExtensionVersionResponse convertToResponse() {
+    Collection<ServiceInfo> serviceInfos = getServices();
+    // The collection of service descriptor files. A Set is being used because some Kerberos descriptor
+    // files contain multiple services, therefore the same File may be encountered more than once.
+    // For example the YARN directory may contain YARN and MAPREDUCE2 services.
+    Collection<File> serviceDescriptorFiles = new HashSet<File>();
+    if (serviceInfos != null) {
+      for (ServiceInfo serviceInfo : serviceInfos) {
+        File file = serviceInfo.getKerberosDescriptorFile();
+        if (file != null) {
+          serviceDescriptorFiles.add(file);
+        }
+      }
+    }
+
+    return new ExtensionVersionResponse(getVersion(), getParentExtensionVersion(),
+                                        isValid(), getErrors());
+  }
+
+  public String getParentExtensionVersion() {
+    return parentExtensionVersion;
+  }
+
+  public void setParentExtensionVersion(String parentExtensionVersion) {
+    this.parentExtensionVersion = parentExtensionVersion;
+  }
+
+  @Override
+  public int compareTo(ExtensionInfo o) {
+    String myId = name + "-" + version;
+    String oId = o.name + "-" + o.version;
+    return myId.compareTo(oId);
+  }
+
+  public List<ExtensionMetainfoXml.Stack> getStacks() {
+    return stacks;
+  }
+
+  public void setStacks(List<ExtensionMetainfoXml.Stack> stacks) {
+    this.stacks = stacks;
+  }
+
+  public List<ExtensionMetainfoXml.Extension> getExtensions() {
+    return extensions;
+  }
+
+  public void setExtensions(List<ExtensionMetainfoXml.Extension> extensions) {
+    this.extensions = extensions;
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/300a7e21/ambari-server/src/main/java/org/apache/ambari/server/state/ServiceInfo.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/state/ServiceInfo.java b/ambari-server/src/main/java/org/apache/ambari/server/state/ServiceInfo.java
index 76840ea..6fda8bc 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/state/ServiceInfo.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/state/ServiceInfo.java
@@ -27,6 +27,7 @@ import com.google.common.collect.Multimaps;
 import org.apache.ambari.server.api.services.AmbariMetaInfo;
 import org.apache.ambari.server.stack.Validable;
 import org.apache.ambari.server.state.stack.MetricDefinition;
+import org.apache.ambari.server.state.stack.StackRoleCommandOrder;
 import org.codehaus.jackson.annotate.JsonIgnore;
 import org.codehaus.jackson.map.annotate.JsonFilter;
 
@@ -145,6 +146,8 @@ public class ServiceInfo implements Validable{
   @XmlTransient
   private File widgetsDescriptorFile = null;
 
+  private StackRoleCommandOrder roleCommandOrder;
+
   @XmlTransient
   private boolean valid = true;
 
@@ -730,6 +733,14 @@ public String getVersion() {
     this.widgetsDescriptorFile = widgetsDescriptorFile;
   }
 
+  public StackRoleCommandOrder getRoleCommandOrder() {
+    return roleCommandOrder;
+  }
+
+  public void setRoleCommandOrder(StackRoleCommandOrder roleCommandOrder) {
+    this.roleCommandOrder = roleCommandOrder;
+  }
+
   /**
    * @return config types this service contains configuration for, but which are primarily related to another service
    */

http://git-wip-us.apache.org/repos/asf/ambari/blob/300a7e21/ambari-server/src/main/java/org/apache/ambari/server/state/StackInfo.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/state/StackInfo.java b/ambari-server/src/main/java/org/apache/ambari/server/state/StackInfo.java
index c7738cd..c30f28f 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/state/StackInfo.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/state/StackInfo.java
@@ -49,6 +49,7 @@ public class StackInfo implements Comparable<StackInfo>, Validable{
   private String widgetsDescriptorFileLocation;
   private List<RepositoryInfo> repositories;
   private Collection<ServiceInfo> services;
+  private Collection<ExtensionInfo> extensions;
   private String parentStackVersion;
   // stack-level properties
   private List<PropertyInfo> properties;
@@ -161,6 +162,34 @@ public class StackInfo implements Comparable<StackInfo>, Validable{
     this.services = services;
   }
 
+  public synchronized Collection<ExtensionInfo> getExtensions() {
+    if (extensions == null) extensions = new ArrayList<ExtensionInfo>();
+    return extensions;
+  }
+
+  public ExtensionInfo getExtension(String name) {
+    Collection<ExtensionInfo> extensions = getExtensions();
+    for (ExtensionInfo extension : extensions) {
+      if (extension.getName().equals(name)) {
+        return extension;
+      }
+    }
+    //todo: exception?
+    return null;
+  }
+
+  public ExtensionInfo getExtensionByService(String serviceName) {
+    Collection<ExtensionInfo> extensions = getExtensions();
+    for (ExtensionInfo extension : extensions) {
+      for (ServiceInfo service : services) {
+        if (service.getName().equals(serviceName))
+          return extension;
+      }
+    }
+    //todo: exception?
+    return null;
+  }
+
   public List<PropertyInfo> getProperties() {
     if (properties == null) properties = new ArrayList<PropertyInfo>();
     return properties;

http://git-wip-us.apache.org/repos/asf/ambari/blob/300a7e21/ambari-server/src/main/java/org/apache/ambari/server/state/stack/ExtensionMetainfoXml.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/state/stack/ExtensionMetainfoXml.java b/ambari-server/src/main/java/org/apache/ambari/server/state/stack/ExtensionMetainfoXml.java
new file mode 100644
index 0000000..790e514
--- /dev/null
+++ b/ambari-server/src/main/java/org/apache/ambari/server/state/stack/ExtensionMetainfoXml.java
@@ -0,0 +1,204 @@
+/**
+ * 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.state.stack;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlElementWrapper;
+import javax.xml.bind.annotation.XmlElements;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlTransient;
+
+import org.apache.ambari.server.stack.Validable;
+
+/**
+ * Represents the extension <code>metainfo.xml</code> file.
+ *
+ * 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.
+ */
+@XmlRootElement(name="metainfo")
+@XmlAccessorType(XmlAccessType.FIELD)
+public class ExtensionMetainfoXml implements Validable{
+
+  @XmlElement(name="extends")
+  private String extendsVersion = null;
+
+  @XmlElement(name="versions")
+  private Version version = new Version();
+
+  @XmlElement(name="prerequisites")
+  private Prerequisites prerequisites = new Prerequisites();
+
+  @XmlAccessorType(XmlAccessType.FIELD)
+  public static class Prerequisites {
+    private Prerequisites() {
+    }
+    @XmlElementWrapper(name="min-stack-versions")
+    @XmlElements(@XmlElement(name="stack"))
+    private List<Stack> stacks = new ArrayList<Stack>();
+
+    @XmlElementWrapper(name="min-extension-versions")
+    @XmlElements(@XmlElement(name="extension"))
+    private List<Extension> extensions = new ArrayList<Extension>();
+
+    public List<Stack> getStacks() {
+      return stacks;
+    }
+
+    public List<Extension> getExtensions() {
+      return extensions;
+    }
+  }
+
+  @XmlTransient
+  private boolean valid = true;
+
+  /**
+   *
+   * @return valid xml flag
+   */
+  @Override
+  public boolean isValid() {
+    return valid;
+  }
+
+  /**
+   *
+   * @param valid set validity flag
+   */
+  @Override
+  public void setValid(boolean valid) {
+    this.valid = valid;
+  }
+
+  @XmlTransient
+  private Set<String> errorSet = new HashSet<String>();
+
+  @Override
+  public void addError(String error) {
+    errorSet.add(error);
+  }
+
+  @Override
+  public Collection<String> getErrors() {
+    return errorSet;
+  }
+
+  @Override
+  public void addErrors(Collection<String> errors) {
+    this.errorSet.addAll(errors);
+  }
+
+  /**
+   * @return the parent stack version number
+   */
+  public String getExtends() {
+    return extendsVersion;
+  }
+
+  /**
+   * @return gets the version
+   */
+  public Version getVersion() {
+    return version;
+  }
+
+  public List<Stack> getStacks() {
+    return prerequisites.getStacks();
+  }
+
+  public List<Extension> getExtensions() {
+    return prerequisites.getExtensions();
+  }
+
+  @XmlAccessorType(XmlAccessType.FIELD)
+  public static class Version {
+    private Version() {
+    }
+    private boolean active = false;
+    private String upgrade = null;
+
+    /**
+     * @return <code>true</code> if the stack is active
+     */
+    public boolean isActive() {
+      return active;
+    }
+
+    /**
+     * @return the upgrade version number, if set
+     */
+    public String getUpgrade() {
+      return upgrade;
+    }
+  }
+
+  @XmlAccessorType(XmlAccessType.FIELD)
+  public static class Stack {
+    private Stack() {
+    }
+    private String name = null;
+    private String version = null;
+
+    /**
+     * @return the stack name
+     */
+    public String getName() {
+      return name;
+    }
+
+    /**
+     * @return the stack version, this may be something like 1.0.*
+     */
+    public String getVersion() {
+      return version;
+    }
+  }
+
+  @XmlAccessorType(XmlAccessType.FIELD)
+  public static class Extension {
+    private Extension() {
+    }
+    private String name = null;
+    private String version = null;
+
+    /**
+     * @return the extension name
+     */
+    public String getName() {
+      return name;
+    }
+
+    /**
+     * @return the extension version, this may be something like 1.0.*
+     */
+    public String getVersion() {
+      return version;
+    }
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/300a7e21/ambari-server/src/main/java/org/apache/ambari/server/state/stack/ServiceMetainfoXml.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/state/stack/ServiceMetainfoXml.java b/ambari-server/src/main/java/org/apache/ambari/server/state/stack/ServiceMetainfoXml.java
index 418c389..1fa6dcf 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/state/stack/ServiceMetainfoXml.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/state/stack/ServiceMetainfoXml.java
@@ -86,6 +86,10 @@ public class ServiceMetainfoXml implements Validable{
   public List<ServiceInfo> getServices() {
     return services;
   }
+
+  public void setServices(List<ServiceInfo> services) {
+    this.services = services;
+  }
   
   public String getSchemaVersion() {
     return schemaVersion;

http://git-wip-us.apache.org/repos/asf/ambari/blob/300a7e21/ambari-server/src/main/java/org/apache/ambari/server/upgrade/UpgradeCatalog240.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/upgrade/UpgradeCatalog240.java b/ambari-server/src/main/java/org/apache/ambari/server/upgrade/UpgradeCatalog240.java
index 81b5653..bee0942 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/upgrade/UpgradeCatalog240.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/upgrade/UpgradeCatalog240.java
@@ -161,7 +161,6 @@ public class UpgradeCatalog240 extends AbstractUpgradeCatalog {
   protected static final String PHOENIX_QUERY_SERVER_PRINCIPAL_KEY = "phoenix.queryserver.kerberos.principal";
   protected static final String PHOENIX_QUERY_SERVER_KEYTAB_KEY = "phoenix.queryserver.keytab.file";
 
-
   private static final String OOZIE_ENV_CONFIG = "oozie-env";
   private static final String HIVE_ENV_CONFIG = "hive-env";
   private static final String AMS_SITE = "ams-site";
@@ -174,6 +173,10 @@ public class UpgradeCatalog240 extends AbstractUpgradeCatalog {
   protected static final String HBASE_SITE_CONFIG = "hbase-site";
   protected static final String HBASE_SPNEGO_PRINCIPAL_KEY = "hbase.security.authentication.spnego.kerberos.principal";
   protected static final String HBASE_SPNEGO_KEYTAB_KEY = "hbase.security.authentication.spnego.kerberos.keytab";
+  protected static final String EXTENSION_TABLE = "extension";
+  protected static final String EXTENSION_ID_COLUMN = "extension_id";
+  protected static final String EXTENSION_LINK_TABLE = "extensionlink";
+  protected static final String EXTENSION_LINK_ID_COLUMN = "link_id";
 
   private static final Map<String, Integer> ROLE_ORDER;
 
@@ -265,6 +268,8 @@ public class UpgradeCatalog240 extends AbstractUpgradeCatalog {
   protected void executeDDLUpdates() throws AmbariException, SQLException {
     updateAdminPermissionTable();
     updateServiceComponentDesiredStateTable();
+    createExtensionTable();
+    createExtensionLinkTable();
     createSettingTable();
     updateRepoVersionTableDDL();
     updateServiceComponentDesiredStateTableDDL();
@@ -292,7 +297,7 @@ public class UpgradeCatalog240 extends AbstractUpgradeCatalog {
     columns.add(new DBColumnInfo("username", String.class, 255, null, false));
     columns.add(new DBColumnInfo("password", String.class, 255, null, false));
     dbAccessor.createTable(REMOTE_AMBARI_CLUSTER_TABLE, columns, CLUSTER_ID);
-    dbAccessor.addUniqueConstraint(REMOTE_AMBARI_CLUSTER_TABLE , "unq_remote_ambari_cluster" , CLUSTER_NAME);
+    dbAccessor.addUniqueConstraint(REMOTE_AMBARI_CLUSTER_TABLE , "UQ_remote_ambari_cluster" , CLUSTER_NAME);
     addSequence("remote_cluster_id_seq", 1L, false);
 
     List<DBColumnInfo> remoteClusterServiceColumns = new ArrayList<>();
@@ -465,6 +470,48 @@ public class UpgradeCatalog240 extends AbstractUpgradeCatalog {
     }
   }
 
+  private void createExtensionTable() throws SQLException {
+    List<DBColumnInfo> columns = new ArrayList<>();
+
+    // Add extension table
+    LOG.info("Creating " + EXTENSION_TABLE + " table");
+
+    columns.add(new DBColumnInfo(EXTENSION_ID_COLUMN, Long.class, null, null, false));
+    columns.add(new DBColumnInfo("extension_name", String.class, 255, null, false));
+    columns.add(new DBColumnInfo("extension_version", String.class, 255, null, false));
+    dbAccessor.createTable(EXTENSION_TABLE, columns, EXTENSION_ID_COLUMN);
+
+    // create UNIQUE constraint, ensuring column order matches SQL files
+    String[] uniqueColumns = new String[] { "extension_name", "extension_version" };
+    dbAccessor.addUniqueConstraint(EXTENSION_TABLE, "UQ_extension", uniqueColumns);
+
+    addSequence("extension_id_seq", 0L, false);
+  }
+
+  private void createExtensionLinkTable() throws SQLException {
+    List<DBColumnInfo> columns = new ArrayList<>();
+
+    // Add extension link table
+    LOG.info("Creating " + EXTENSION_LINK_TABLE + " table");
+
+    columns.add(new DBColumnInfo(EXTENSION_LINK_ID_COLUMN, Long.class, null, null, false));
+    columns.add(new DBColumnInfo("stack_id", Long.class, null, null, false));
+    columns.add(new DBColumnInfo(EXTENSION_ID_COLUMN, Long.class, null, null, false));
+    dbAccessor.createTable(EXTENSION_LINK_TABLE, columns, EXTENSION_LINK_ID_COLUMN);
+
+    // create UNIQUE constraint, ensuring column order matches SQL files
+    String[] uniqueColumns = new String[] { "stack_id", EXTENSION_ID_COLUMN };
+    dbAccessor.addUniqueConstraint(EXTENSION_LINK_TABLE, "UQ_extension_link", uniqueColumns);
+
+    dbAccessor.addFKConstraint(EXTENSION_LINK_TABLE, "FK_extensionlink_extension_id",
+      EXTENSION_ID_COLUMN, EXTENSION_TABLE, EXTENSION_ID_COLUMN, false);
+
+    dbAccessor.addFKConstraint(EXTENSION_LINK_TABLE, "FK_extensionlink_stack_id",
+      "stack_id", STACK_TABLE, "stack_id", false);
+
+    addSequence("link_id_seq", 0L, false);
+  }
+
   private void createSettingTable() throws SQLException {
     List<DBColumnInfo> columns = new ArrayList<>();
 
@@ -1429,7 +1476,7 @@ public class UpgradeCatalog240 extends AbstractUpgradeCatalog {
 
     // create UNIQUE constraint, ensuring column order matches SQL files
     String[] uniqueColumns = new String[] { "component_name", "service_name", "cluster_id" };
-    dbAccessor.addUniqueConstraint(SERVICE_COMPONENT_DS_TABLE, "unq_scdesiredstate_name",
+    dbAccessor.addUniqueConstraint(SERVICE_COMPONENT_DS_TABLE, "UQ_scdesiredstate_name",
         uniqueColumns);
 
     // add FKs back to SCDS in both HCDS and HCS tables

http://git-wip-us.apache.org/repos/asf/ambari/blob/300a7e21/ambari-server/src/main/resources/Ambari-DDL-Derby-CREATE.sql
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/resources/Ambari-DDL-Derby-CREATE.sql b/ambari-server/src/main/resources/Ambari-DDL-Derby-CREATE.sql
index 257ca42..aa8a39e 100644
--- a/ambari-server/src/main/resources/Ambari-DDL-Derby-CREATE.sql
+++ b/ambari-server/src/main/resources/Ambari-DDL-Derby-CREATE.sql
@@ -22,7 +22,23 @@ CREATE TABLE stack(
   stack_name VARCHAR(255) NOT NULL,
   stack_version VARCHAR(255) NOT NULL,
   CONSTRAINT PK_stack PRIMARY KEY (stack_id),
-  CONSTRAINT unq_stack UNIQUE (stack_name, stack_version));
+  CONSTRAINT UQ_stack UNIQUE (stack_name, stack_version));
+
+CREATE TABLE extension(
+  extension_id BIGINT NOT NULL,
+  extension_name VARCHAR(255) NOT NULL,
+  extension_version VARCHAR(255) NOT NULL,
+  CONSTRAINT PK_extension PRIMARY KEY (extension_id),
+  CONSTRAINT UQ_extension UNIQUE(extension_name, extension_version));
+
+CREATE TABLE extensionlink(
+  link_id BIGINT NOT NULL,
+  stack_id BIGINT NOT NULL,
+  extension_id BIGINT NOT NULL,
+  CONSTRAINT PK_extensionlink PRIMARY KEY (link_id),
+  CONSTRAINT FK_extensionlink_stack_id FOREIGN KEY (stack_id) REFERENCES stack(stack_id),
+  CONSTRAINT FK_extensionlink_extension_id FOREIGN KEY (extension_id) REFERENCES extension(extension_id),
+  CONSTRAINT UQ_extension_link UNIQUE(stack_id, extension_id));
 
 CREATE TABLE adminresourcetype (
   resource_type_id INTEGER NOT NULL,
@@ -176,7 +192,7 @@ CREATE TABLE servicecomponentdesiredstate (
   service_name VARCHAR(255) NOT NULL,
   recovery_enabled SMALLINT NOT NULL DEFAULT 0,
   CONSTRAINT pk_sc_desiredstate PRIMARY KEY (id),
-  CONSTRAINT unq_scdesiredstate_name UNIQUE(component_name, service_name, cluster_id),
+  CONSTRAINT UQ_scdesiredstate_name UNIQUE(component_name, service_name, cluster_id),
   CONSTRAINT FK_scds_desired_stack_id FOREIGN KEY (desired_stack_id) REFERENCES stack(stack_id),
   CONSTRAINT srvccmponentdesiredstatesrvcnm FOREIGN KEY (service_name, cluster_id) REFERENCES clusterservices (service_name, cluster_id));
 
@@ -789,7 +805,7 @@ CREATE TABLE remoteambaricluster(
   url VARCHAR(255) NOT NULL,
   password VARCHAR(255) NOT NULL,
   CONSTRAINT PK_remote_ambari_cluster PRIMARY KEY (cluster_id),
-  CONSTRAINT unq_remote_ambari_cluster UNIQUE (name));
+  CONSTRAINT UQ_remote_ambari_cluster UNIQUE (name));
 
 CREATE TABLE remoteambariclusterservice(
   id BIGINT NOT NULL,
@@ -1092,6 +1108,10 @@ INSERT INTO ambari_sequences (sequence_name, sequence_value)
   union all
   select 'stack_id_seq', 0 FROM SYSIBM.SYSDUMMY1
   union all
+  select 'extension_id_seq', 0 FROM SYSIBM.SYSDUMMY1
+  union all
+  select 'link_id_seq', 0 FROM SYSIBM.SYSDUMMY1
+  union all
   select 'topology_host_info_id_seq', 0 FROM SYSIBM.SYSDUMMY1
   union all
   select 'topology_host_request_id_seq', 0 FROM SYSIBM.SYSDUMMY1

http://git-wip-us.apache.org/repos/asf/ambari/blob/300a7e21/ambari-server/src/main/resources/Ambari-DDL-MySQL-CREATE.sql
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/resources/Ambari-DDL-MySQL-CREATE.sql b/ambari-server/src/main/resources/Ambari-DDL-MySQL-CREATE.sql
index 9b68174..3be1299 100644
--- a/ambari-server/src/main/resources/Ambari-DDL-MySQL-CREATE.sql
+++ b/ambari-server/src/main/resources/Ambari-DDL-MySQL-CREATE.sql
@@ -32,7 +32,23 @@ CREATE TABLE stack(
   stack_name VARCHAR(100) NOT NULL,
   stack_version VARCHAR(100) NOT NULL,
   CONSTRAINT PK_stack PRIMARY KEY (stack_id),
-  CONSTRAINT unq_stack UNIQUE (stack_name, stack_version));
+  CONSTRAINT UQ_stack UNIQUE (stack_name, stack_version));
+
+CREATE TABLE extension(
+  extension_id BIGINT NOT NULL,
+  extension_name VARCHAR(255) NOT NULL,
+  extension_version VARCHAR(255) NOT NULL,
+  CONSTRAINT PK_extension PRIMARY KEY (extension_id),
+  CONSTRAINT UQ_extension UNIQUE (extension_name, extension_version));
+
+CREATE TABLE extensionlink(
+  link_id BIGINT NOT NULL,
+  stack_id BIGINT NOT NULL,
+  extension_id BIGINT NOT NULL,
+  CONSTRAINT PK_extensionlink PRIMARY KEY (link_id),
+  CONSTRAINT UQ_extension_link UNIQUE (stack_id, extension_id),
+  CONSTRAINT FK_extensionlink_stack_id FOREIGN KEY (stack_id) REFERENCES stack(stack_id),
+  CONSTRAINT FK_extensionlink_extension_id FOREIGN KEY (extension_id) REFERENCES extension(extension_id));
 
 CREATE TABLE adminresourcetype (
   resource_type_id INTEGER NOT NULL,
@@ -176,7 +192,7 @@ CREATE TABLE servicecomponentdesiredstate (
   service_name VARCHAR(100) NOT NULL,
   recovery_enabled SMALLINT NOT NULL DEFAULT 0,
   CONSTRAINT pk_sc_desiredstate PRIMARY KEY (id),
-  CONSTRAINT unq_scdesiredstate_name UNIQUE(component_name, service_name, cluster_id),
+  CONSTRAINT UQ_scdesiredstate_name UNIQUE(component_name, service_name, cluster_id),
   CONSTRAINT FK_scds_desired_stack_id FOREIGN KEY (desired_stack_id) REFERENCES stack(stack_id),
   CONSTRAINT srvccmponentdesiredstatesrvcnm FOREIGN KEY (service_name, cluster_id) REFERENCES clusterservices (service_name, cluster_id));
 
@@ -796,7 +812,7 @@ CREATE TABLE remoteambaricluster(
   url VARCHAR(255) NOT NULL,
   password VARCHAR(255) NOT NULL,
   CONSTRAINT PK_remote_ambari_cluster PRIMARY KEY (cluster_id),
-  CONSTRAINT unq_remote_ambari_cluster UNIQUE (name));
+  CONSTRAINT UQ_remote_ambari_cluster UNIQUE (name));
 
 CREATE TABLE remoteambariclusterservice(
   id BIGINT NOT NULL,
@@ -1061,6 +1077,8 @@ INSERT INTO ambari_sequences(sequence_name, sequence_value) VALUES
   ('upgrade_group_id_seq', 0),
   ('upgrade_item_id_seq', 0),
   ('stack_id_seq', 0),
+  ('extension_id_seq', 0),
+  ('link_id_seq', 0),
   ('widget_id_seq', 0),
   ('widget_layout_id_seq', 0),
   ('topology_host_info_id_seq', 0),

http://git-wip-us.apache.org/repos/asf/ambari/blob/300a7e21/ambari-server/src/main/resources/Ambari-DDL-Oracle-CREATE.sql
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/resources/Ambari-DDL-Oracle-CREATE.sql b/ambari-server/src/main/resources/Ambari-DDL-Oracle-CREATE.sql
index 362c714..6bbb282 100644
--- a/ambari-server/src/main/resources/Ambari-DDL-Oracle-CREATE.sql
+++ b/ambari-server/src/main/resources/Ambari-DDL-Oracle-CREATE.sql
@@ -22,7 +22,23 @@ CREATE TABLE stack(
   stack_name VARCHAR2(255) NOT NULL,
   stack_version VARCHAR2(255) NOT NULL,
   CONSTRAINT PK_stack PRIMARY KEY (stack_id),
-  CONSTRAINT unq_stack UNIQUE (stack_name, stack_version));
+  CONSTRAINT UQ_stack UNIQUE (stack_name, stack_version));
+
+CREATE TABLE extension(
+  extension_id NUMERIC(19) NOT NULL,
+  extension_name VARCHAR2(255) NOT NULL,
+  extension_version VARCHAR2(255) NOT NULL,
+  CONSTRAINT PK_extension PRIMARY KEY (extension_id),
+  CONSTRAINT UQ_extension UNIQUE(extension_name, extension_version));
+
+CREATE TABLE extensionlink(
+  link_id NUMERIC(19) NOT NULL,
+  stack_id NUMERIC(19) NOT NULL,
+  extension_id NUMERIC(19) NOT NULL,
+  CONSTRAINT PK_extensionlink PRIMARY KEY (link_id),
+  CONSTRAINT FK_extensionlink_stack_id FOREIGN KEY (stack_id) REFERENCES stack(stack_id),
+  CONSTRAINT FK_extensionlink_extension_id FOREIGN KEY (extension_id) REFERENCES extension(extension_id),
+  CONSTRAINT UQ_extension_link UNIQUE(stack_id, extension_id));
 
 CREATE TABLE adminresourcetype (
   resource_type_id NUMBER(10) NOT NULL,
@@ -167,7 +183,7 @@ CREATE TABLE servicecomponentdesiredstate (
   service_name VARCHAR2(255) NOT NULL,
   recovery_enabled SMALLINT DEFAULT 0 NOT NULL,
   CONSTRAINT pk_sc_desiredstate PRIMARY KEY (id),
-  CONSTRAINT unq_scdesiredstate_name UNIQUE(component_name, service_name, cluster_id),
+  CONSTRAINT UQ_scdesiredstate_name UNIQUE(component_name, service_name, cluster_id),
   CONSTRAINT FK_scds_desired_stack_id FOREIGN KEY (desired_stack_id) REFERENCES stack(stack_id),
   CONSTRAINT srvccmponentdesiredstatesrvcnm FOREIGN KEY (service_name, cluster_id) REFERENCES clusterservices (service_name, cluster_id));
 
@@ -786,7 +802,7 @@ CREATE TABLE remoteambaricluster(
   url VARCHAR(255) NOT NULL,
   password VARCHAR(255) NOT NULL,
   CONSTRAINT PK_remote_ambari_cluster PRIMARY KEY (cluster_id),
-  CONSTRAINT unq_remote_ambari_cluster UNIQUE (name));
+  CONSTRAINT UQ_remote_ambari_cluster UNIQUE (name));
 
 CREATE TABLE remoteambariclusterservice(
   id NUMBER(19) NOT NULL,
@@ -1051,6 +1067,8 @@ INSERT INTO ambari_sequences(sequence_name, sequence_value) values ('upgrade_id_
 INSERT INTO ambari_sequences(sequence_name, sequence_value) values ('upgrade_group_id_seq', 0);
 INSERT INTO ambari_sequences(sequence_name, sequence_value) values ('upgrade_item_id_seq', 0);
 INSERT INTO ambari_sequences(sequence_name, sequence_value) values ('stack_id_seq', 0);
+INSERT INTO ambari_sequences(sequence_name, sequence_value) values ('extension_id_seq', 0);
+INSERT INTO ambari_sequences(sequence_name, sequence_value) values ('link_id_seq', 0);
 INSERT INTO ambari_sequences(sequence_name, sequence_value) values ('widget_id_seq', 0);
 INSERT INTO ambari_sequences(sequence_name, sequence_value) values ('widget_layout_id_seq', 0);
 INSERT INTO ambari_sequences(sequence_name, sequence_value) values ('topology_host_info_id_seq', 0);

http://git-wip-us.apache.org/repos/asf/ambari/blob/300a7e21/ambari-server/src/main/resources/Ambari-DDL-Postgres-CREATE.sql
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/resources/Ambari-DDL-Postgres-CREATE.sql b/ambari-server/src/main/resources/Ambari-DDL-Postgres-CREATE.sql
index 04a4361..b13c121 100644
--- a/ambari-server/src/main/resources/Ambari-DDL-Postgres-CREATE.sql
+++ b/ambari-server/src/main/resources/Ambari-DDL-Postgres-CREATE.sql
@@ -22,7 +22,23 @@ CREATE TABLE stack(
   stack_name VARCHAR(255) NOT NULL,
   stack_version VARCHAR(255) NOT NULL,
   CONSTRAINT PK_stack PRIMARY KEY (stack_id),
-  CONSTRAINT unq_stack UNIQUE (stack_name, stack_version));
+  CONSTRAINT UQ_stack UNIQUE (stack_name, stack_version));
+
+CREATE TABLE extension(
+  extension_id BIGINT NOT NULL,
+  extension_name VARCHAR(255) NOT NULL,
+  extension_version VARCHAR(255) NOT NULL,
+  CONSTRAINT PK_extension PRIMARY KEY (extension_id),
+  CONSTRAINT UQ_extension UNIQUE(extension_name, extension_version));
+
+CREATE TABLE extensionlink(
+  link_id BIGINT NOT NULL,
+  stack_id BIGINT NOT NULL,
+  extension_id BIGINT NOT NULL,
+  CONSTRAINT PK_extensionlink PRIMARY KEY (link_id),
+  CONSTRAINT FK_extensionlink_stack_id FOREIGN KEY (stack_id) REFERENCES stack(stack_id),
+  CONSTRAINT FK_extensionlink_extension_id FOREIGN KEY (extension_id) REFERENCES extension(extension_id),
+  CONSTRAINT UQ_extension_link UNIQUE(stack_id, extension_id));
 
 CREATE TABLE adminresourcetype (
   resource_type_id INTEGER NOT NULL,
@@ -176,7 +192,7 @@ CREATE TABLE servicecomponentdesiredstate (
   service_name VARCHAR(255) NOT NULL,
   recovery_enabled SMALLINT NOT NULL DEFAULT 0,
   CONSTRAINT pk_sc_desiredstate PRIMARY KEY (id),
-  CONSTRAINT unq_scdesiredstate_name UNIQUE(component_name, service_name, cluster_id),
+  CONSTRAINT UQ_scdesiredstate_name UNIQUE(component_name, service_name, cluster_id),
   CONSTRAINT FK_scds_desired_stack_id FOREIGN KEY (desired_stack_id) REFERENCES stack(stack_id),
   CONSTRAINT srvccmponentdesiredstatesrvcnm FOREIGN KEY (service_name, cluster_id) REFERENCES clusterservices (service_name, cluster_id));
 
@@ -788,7 +804,7 @@ CREATE TABLE remoteambaricluster(
   url VARCHAR(255) NOT NULL,
   password VARCHAR(255) NOT NULL,
   CONSTRAINT PK_remote_ambari_cluster PRIMARY KEY (cluster_id),
-  CONSTRAINT unq_remote_ambari_cluster UNIQUE (name));
+  CONSTRAINT UQ_remote_ambari_cluster UNIQUE (name));
 
 CREATE TABLE remoteambariclusterservice(
   id BIGINT NOT NULL,
@@ -1054,6 +1070,8 @@ INSERT INTO ambari_sequences (sequence_name, sequence_value) VALUES
   ('widget_layout_id_seq', 0),
   ('upgrade_item_id_seq', 0),
   ('stack_id_seq', 0),
+  ('extension_id_seq', 0),
+  ('link_id_seq', 0),
   ('topology_host_info_id_seq', 0),
   ('topology_host_request_id_seq', 0),
   ('topology_host_task_id_seq', 0),

http://git-wip-us.apache.org/repos/asf/ambari/blob/300a7e21/ambari-server/src/main/resources/Ambari-DDL-Postgres-EMBEDDED-CREATE.sql
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/resources/Ambari-DDL-Postgres-EMBEDDED-CREATE.sql b/ambari-server/src/main/resources/Ambari-DDL-Postgres-EMBEDDED-CREATE.sql
index 21e6cec..fd14e80 100644
--- a/ambari-server/src/main/resources/Ambari-DDL-Postgres-EMBEDDED-CREATE.sql
+++ b/ambari-server/src/main/resources/Ambari-DDL-Postgres-EMBEDDED-CREATE.sql
@@ -33,10 +33,28 @@ CREATE TABLE ambari.stack(
   stack_name VARCHAR(255) NOT NULL,
   stack_version VARCHAR(255) NOT NULL,
   CONSTRAINT PK_stack PRIMARY KEY (stack_id),
-  CONSTRAINT unq_stack UNIQUE (stack_name, stack_version)
+  CONSTRAINT UQ_stack UNIQUE (stack_name, stack_version)
 );
 GRANT ALL PRIVILEGES ON TABLE ambari.stack TO :username;
 
+CREATE TABLE ambari.extension(
+  extension_id BIGINT NOT NULL,
+  extension_name VARCHAR(255) NOT NULL,
+  extension_version VARCHAR(255) NOT NULL,
+  CONSTRAINT PK_extension PRIMARY KEY (extension_id),
+  CONSTRAINT UQ_extension UNIQUE(extension_name, extension_version));
+GRANT ALL PRIVILEGES ON TABLE ambari.extension TO :username;
+
+CREATE TABLE ambari.extensionlink(
+  link_id BIGINT NOT NULL,
+  stack_id BIGINT NOT NULL,
+  extension_id BIGINT NOT NULL,
+  CONSTRAINT PK_extensionlink PRIMARY KEY (link_id),
+  CONSTRAINT FK_extensionlink_stack_id FOREIGN KEY (stack_id) REFERENCES ambari.stack(stack_id),
+  CONSTRAINT FK_extensionlink_extension_id FOREIGN KEY (extension_id) REFERENCES ambari.extension(extension_id),
+  CONSTRAINT UQ_extension_link UNIQUE(stack_id, extension_id));
+GRANT ALL PRIVILEGES ON TABLE ambari.extensionlink TO :username;
+
 CREATE TABLE ambari.adminresourcetype (
   resource_type_id INTEGER NOT NULL,
   resource_type_name VARCHAR(255) NOT NULL,
@@ -215,7 +233,7 @@ CREATE TABLE ambari.servicecomponentdesiredstate (
   service_name VARCHAR(255) NOT NULL,
   recovery_enabled SMALLINT NOT NULL DEFAULT 0,
   CONSTRAINT pk_sc_desiredstate PRIMARY KEY (id),
-  CONSTRAINT unq_scdesiredstate_name UNIQUE(component_name, service_name, cluster_id),
+  CONSTRAINT UQ_scdesiredstate_name UNIQUE(component_name, service_name, cluster_id),
   CONSTRAINT FK_scds_desired_stack_id FOREIGN KEY (desired_stack_id) REFERENCES ambari.stack(stack_id),
   CONSTRAINT srvccmponentdesiredstatesrvcnm FOREIGN KEY (service_name, cluster_id) REFERENCES ambari.clusterservices (service_name, cluster_id)
 );
@@ -924,7 +942,7 @@ CREATE TABLE ambari.remoteambaricluster(
   url VARCHAR(255) NOT NULL,
   password VARCHAR(255) NOT NULL,
   CONSTRAINT PK_remote_ambari_cluster PRIMARY KEY (cluster_id),
-  CONSTRAINT unq_remote_ambari_cluster UNIQUE (name)
+  CONSTRAINT UQ_remote_ambari_cluster UNIQUE (name)
 );
 GRANT ALL PRIVILEGES ON TABLE ambari.remoteambaricluster TO :username;
 
@@ -1216,6 +1234,8 @@ INSERT INTO ambari.ambari_sequences (sequence_name, sequence_value) VALUES
   ('widget_layout_id_seq', 0),
   ('upgrade_item_id_seq', 0),
   ('stack_id_seq', 0),
+  ('extension_id_seq', 0),
+  ('link_id_seq', 0),
   ('topology_host_info_id_seq', 0),
   ('topology_host_request_id_seq', 0),
   ('topology_host_task_id_seq', 0),

http://git-wip-us.apache.org/repos/asf/ambari/blob/300a7e21/ambari-server/src/main/resources/Ambari-DDL-SQLAnywhere-CREATE.sql
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/resources/Ambari-DDL-SQLAnywhere-CREATE.sql b/ambari-server/src/main/resources/Ambari-DDL-SQLAnywhere-CREATE.sql
index 24c1b63..0574c2b 100644
--- a/ambari-server/src/main/resources/Ambari-DDL-SQLAnywhere-CREATE.sql
+++ b/ambari-server/src/main/resources/Ambari-DDL-SQLAnywhere-CREATE.sql
@@ -21,7 +21,23 @@ CREATE TABLE stack(
   stack_name VARCHAR(255) NOT NULL,
   stack_version VARCHAR(255) NOT NULL,
   CONSTRAINT PK_stack PRIMARY KEY (stack_id),
-  CONSTRAINT unq_stack UNIQUE (stack_name, stack_version));
+  CONSTRAINT UQ_stack UNIQUE (stack_name, stack_version));
+
+CREATE TABLE extension(
+  extension_id NUMERIC(19) NOT NULL,
+  extension_name VARCHAR(255) NOT NULL,
+  extension_version VARCHAR(255) NOT NULL,
+  CONSTRAINT PK_extension PRIMARY KEY (extension_id),
+  CONSTRAINT UQ_extension UNIQUE(extension_name, extension_version));
+
+CREATE TABLE extensionlink(
+  link_id NUMERIC(19) NOT NULL,
+  stack_id NUMERIC(19) NOT NULL,
+  extension_id NUMERIC(19) NOT NULL,
+  CONSTRAINT PK_extensionlink PRIMARY KEY (link_id),
+  CONSTRAINT FK_extensionlink_stack_id FOREIGN KEY (stack_id) REFERENCES stack(stack_id),
+  CONSTRAINT FK_extensionlink_extension_id FOREIGN KEY (extension_id) REFERENCES extension(extension_id),
+  CONSTRAINT UQ_extension_link UNIQUE(stack_id, extension_id));
 
 CREATE TABLE adminresourcetype (
   resource_type_id INTEGER NOT NULL,
@@ -165,7 +181,7 @@ CREATE TABLE servicecomponentdesiredstate (
   service_name VARCHAR(255) NOT NULL,
   recovery_enabled SMALLINT NOT NULL DEFAULT 0,
   CONSTRAINT pk_sc_desiredstate PRIMARY KEY (id),
-  CONSTRAINT unq_scdesiredstate_name UNIQUE(component_name, service_name, cluster_id),
+  CONSTRAINT UQ_scdesiredstate_name UNIQUE(component_name, service_name, cluster_id),
   CONSTRAINT FK_scds_desired_stack_id FOREIGN KEY (desired_stack_id) REFERENCES stack(stack_id),
   CONSTRAINT srvccmponentdesiredstatesrvcnm FOREIGN KEY (service_name, cluster_id) REFERENCES clusterservices (service_name, cluster_id));
 
@@ -785,7 +801,7 @@ CREATE TABLE remoteambaricluster(
   url VARCHAR(255) NOT NULL,
   password VARCHAR(255) NOT NULL,
   CONSTRAINT PK_remote_ambari_cluster PRIMARY KEY (cluster_id),
-  CONSTRAINT unq_remote_ambari_cluster UNIQUE (name));
+  CONSTRAINT UQ_remote_ambari_cluster UNIQUE (name));
 
 CREATE TABLE remoteambariclusterservice(
   id NUMERIC(19) NOT NULL,
@@ -1050,6 +1066,8 @@ INSERT INTO ambari_sequences(sequence_name, sequence_value) values ('upgrade_id_
 INSERT INTO ambari_sequences(sequence_name, sequence_value) values ('upgrade_group_id_seq', 0);
 INSERT INTO ambari_sequences(sequence_name, sequence_value) values ('upgrade_item_id_seq', 0);
 INSERT INTO ambari_sequences(sequence_name, sequence_value) values ('stack_id_seq', 0);
+INSERT INTO ambari_sequences(sequence_name, sequence_value) values ('extension_id_seq', 0);
+INSERT INTO ambari_sequences(sequence_name, sequence_value) values ('link_id_seq', 0);
 INSERT INTO ambari_sequences(sequence_name, sequence_value) values ('widget_id_seq', 0);
 INSERT INTO ambari_sequences(sequence_name, sequence_value) values ('widget_layout_id_seq', 0);
 INSERT INTO ambari_sequences(sequence_name, sequence_value) values ('topology_host_info_id_seq', 0);

http://git-wip-us.apache.org/repos/asf/ambari/blob/300a7e21/ambari-server/src/main/resources/Ambari-DDL-SQLServer-CREATE.sql
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/resources/Ambari-DDL-SQLServer-CREATE.sql b/ambari-server/src/main/resources/Ambari-DDL-SQLServer-CREATE.sql
index cef4866..02becf2 100644
--- a/ambari-server/src/main/resources/Ambari-DDL-SQLServer-CREATE.sql
+++ b/ambari-server/src/main/resources/Ambari-DDL-SQLServer-CREATE.sql
@@ -34,7 +34,23 @@ CREATE TABLE stack(
   stack_name VARCHAR(255) NOT NULL,
   stack_version VARCHAR(255) NOT NULL,
   CONSTRAINT PK_stack PRIMARY KEY CLUSTERED (stack_id),
-  CONSTRAINT unq_stack UNIQUE (stack_name, stack_version));
+  CONSTRAINT UQ_stack UNIQUE (stack_name, stack_version));
+
+CREATE TABLE extension(
+  extension_id BIGINT NOT NULL,
+  extension_name VARCHAR(255) NOT NULL,
+  extension_version VARCHAR(255) NOT NULL,
+  CONSTRAINT PK_extension PRIMARY KEY CLUSTERED (extension_id),
+  CONSTRAINT UQ_extension UNIQUE (extension_name, extension_version));
+
+CREATE TABLE extensionlink(
+  link_id BIGINT NOT NULL,
+  stack_id BIGINT NOT NULL,
+  extension_id BIGINT NOT NULL,
+  CONSTRAINT PK_extensionlink PRIMARY KEY CLUSTERED (link_id),
+  CONSTRAINT FK_extensionlink_stack_id FOREIGN KEY (stack_id) REFERENCES stack(stack_id),
+  CONSTRAINT FK_extensionlink_extension_id FOREIGN KEY (extension_id) REFERENCES extension(extension_id),
+  CONSTRAINT UQ_extension_link UNIQUE (stack_id, extension_id);
 
 CREATE TABLE adminresourcetype (
   resource_type_id INTEGER NOT NULL,
@@ -189,7 +205,7 @@ CREATE TABLE servicecomponentdesiredstate (
   service_name VARCHAR(255) NOT NULL,
   recovery_enabled SMALLINT NOT NULL DEFAULT 0,
   CONSTRAINT pk_sc_desiredstate PRIMARY KEY (id),
-  CONSTRAINT unq_scdesiredstate_name UNIQUE(component_name, service_name, cluster_id),
+  CONSTRAINT UQ_scdesiredstate_name UNIQUE(component_name, service_name, cluster_id),
   CONSTRAINT FK_scds_desired_stack_id FOREIGN KEY (desired_stack_id) REFERENCES stack(stack_id),
   CONSTRAINT srvccmponentdesiredstatesrvcnm FOREIGN KEY (service_name, cluster_id) REFERENCES clusterservices (service_name, cluster_id));
 
@@ -806,7 +822,7 @@ CREATE TABLE remoteambaricluster(
   url VARCHAR(255) NOT NULL,
   password VARCHAR(255) NOT NULL,
   CONSTRAINT PK_remote_ambari_cluster PRIMARY KEY (cluster_id),
-  CONSTRAINT unq_remote_ambari_cluster UNIQUE (name));
+  CONSTRAINT UQ_remote_ambari_cluster UNIQUE (name));
 
 CREATE TABLE remoteambariclusterservice(
   id BIGINT NOT NULL,
@@ -1078,6 +1094,8 @@ BEGIN TRANSACTION
     ('widget_layout_id_seq', 0),
     ('upgrade_item_id_seq', 0),
     ('stack_id_seq', 0),
+    ('extension_id_seq', 0),
+    ('link_id_seq', 0),
     ('topology_host_info_id_seq', 0),
     ('topology_host_request_id_seq', 0),
     ('topology_host_task_id_seq', 0),

http://git-wip-us.apache.org/repos/asf/ambari/blob/300a7e21/ambari-server/src/main/resources/META-INF/persistence.xml
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/resources/META-INF/persistence.xml b/ambari-server/src/main/resources/META-INF/persistence.xml
index 5671dcf..d44f484 100644
--- a/ambari-server/src/main/resources/META-INF/persistence.xml
+++ b/ambari-server/src/main/resources/META-INF/persistence.xml
@@ -33,6 +33,8 @@
     <class>org.apache.ambari.server.orm.entities.ConfigGroupEntity</class>
     <class>org.apache.ambari.server.orm.entities.ConfigGroupHostMappingEntity</class>
     <class>org.apache.ambari.server.orm.entities.ExecutionCommandEntity</class>
+    <class>org.apache.ambari.server.orm.entities.ExtensionEntity</class>
+    <class>org.apache.ambari.server.orm.entities.ExtensionLinkEntity</class>
     <class>org.apache.ambari.server.orm.entities.GroupEntity</class>
     <class>org.apache.ambari.server.orm.entities.HostComponentDesiredStateEntity</class>
     <class>org.apache.ambari.server.orm.entities.HostComponentStateEntity</class>

http://git-wip-us.apache.org/repos/asf/ambari/blob/300a7e21/ambari-server/src/main/resources/extensions/README.txt
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/resources/extensions/README.txt b/ambari-server/src/main/resources/extensions/README.txt
new file mode 100644
index 0000000..865dfc0
--- /dev/null
+++ b/ambari-server/src/main/resources/extensions/README.txt
@@ -0,0 +1,31 @@
+Extensions should include a folder with the extension name.
+Subfolders of the extension name folder represents different
+extension versions.
+
+For a sample extension MY_EXT 1.0, I would create subfolders: MY_EXT/1.0
+
+Within each extension version folder, there should be both a metainfo.xml
+file and a services folder.  The metainfo.xml should contain the
+stack versions with which the extension version are compatible.
+
+For example the following metainfo.xml shows an extension that is
+compatible with both HDP 2.4 and HDP 2.5:
+
+<metainfo>
+  <prerequisites>
+    <min-stack-versions>
+      <stack>
+        <name>HDP</name>
+        <version>2.4</version>
+      </stack>
+      <stack>
+        <name>HDP</name>
+        <version>2.5</version>
+      </stack>
+    </min-stack-versions>
+  </prerequisites>
+</metainfo>
+
+The services folder will contain all services that are part of the
+extension version.  The contents of those service folders will be the
+same as what you would find in under a stack version's services folder.

http://git-wip-us.apache.org/repos/asf/ambari/blob/300a7e21/ambari-server/src/main/resources/key_properties.json
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/resources/key_properties.json b/ambari-server/src/main/resources/key_properties.json
index 8069349..df2006a 100644
--- a/ambari-server/src/main/resources/key_properties.json
+++ b/ambari-server/src/main/resources/key_properties.json
@@ -79,13 +79,27 @@
     "StackService": "StackServiceComponents/service_name",
     "StackServiceComponent": "StackServiceComponents/component_name"
   },
-    "StackServiceComponentDependency": {
+  "StackServiceComponentDependency": {
     "Stack": "Dependencies/stack_name",
     "StackVersion": "Dependencies/stack_version",
     "StackService": "Dependencies/dependent_service_name",
     "StackServiceComponent": "Dependencies/dependent_component_name",
     "StackServiceComponentDependency": "Dependencies/component_name"
   },
+  "ExtensionLink": {
+    "ExtensionLink": "ExtensionLink/link_id",
+    "Stack": "ExtensionLink/stack_name",
+    "StackVersion": "ExtensionLink/stack_version",
+    "Extension": "ExtensionLink/extension_name",
+    "ExtensionVersion": "ExtensionLink/extension_version"
+  },
+  "Extension": {
+    "Extension": "Extensions/extension_name"
+  },
+  "ExtensionVersion": {
+    "Extension": "Versions/extension_name",
+    "ExtensionVersion": "Versions/extension_version"
+  },
   "DRFeed": {
     "DRFeed": "Feed/name"
   },

http://git-wip-us.apache.org/repos/asf/ambari/blob/300a7e21/ambari-server/src/main/resources/properties.json
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/resources/properties.json b/ambari-server/src/main/resources/properties.json
index eac0dbd..f471628 100644
--- a/ambari-server/src/main/resources/properties.json
+++ b/ambari-server/src/main/resources/properties.json
@@ -265,6 +265,26 @@
         "auto_deploy/enabled",
         "auto_deploy/location"
     ],
+    "ExtensionLink": [
+        "ExtensionLink/link_id",
+        "ExtensionLink/stack_name",
+        "ExtensionLink/stack_version",
+        "ExtensionLink/extension_name",
+        "ExtensionLink/extension_version",
+        "_"
+    ],
+    "Extension":[
+        "Extensions/extension_name",
+        "_"
+    ],
+    "ExtensionVersion":[
+        "Versions/extension_name",
+        "Versions/extension_version",
+        "Versions/valid",
+        "Versions/extension-errors",
+        "Versions/parent_extension_version",
+        "_"
+    ],
     "DRFeed":[
         "Feed/name",
         "Feed/description",
@@ -469,10 +489,10 @@
         "StackConfigurationDependency/dependency_name",
         "_"
     ],
-  "KerberosDescriptor":[
-    "KerberosDescriptors/kerberos_descriptor_name",
-    "KerberosDescriptors/kerberos_descriptor_text",
-    "_"
-  ]
+    "KerberosDescriptor":[
+        "KerberosDescriptors/kerberos_descriptor_name",
+        "KerberosDescriptors/kerberos_descriptor_text",
+        "_"
+    ]
 
 }

http://git-wip-us.apache.org/repos/asf/ambari/blob/300a7e21/ambari-server/src/test/java/org/apache/ambari/server/api/services/ExtensionsServiceTest.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/api/services/ExtensionsServiceTest.java b/ambari-server/src/test/java/org/apache/ambari/server/api/services/ExtensionsServiceTest.java
new file mode 100644
index 0000000..97fd2b5
--- /dev/null
+++ b/ambari-server/src/test/java/org/apache/ambari/server/api/services/ExtensionsServiceTest.java
@@ -0,0 +1,119 @@
+/**
+ * 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.api.services;
+
+import org.apache.ambari.server.api.resources.ResourceInstance;
+import org.apache.ambari.server.api.services.parsers.RequestBodyParser;
+import org.apache.ambari.server.api.services.serializers.ResultSerializer;
+import org.easymock.EasyMock;
+import org.junit.Test;
+
+
+import javax.ws.rs.PathParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import java.lang.reflect.Method;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.notNull;
+import static org.easymock.EasyMock.same;
+import static org.junit.Assert.assertEquals;
+
+/**
+* Unit tests for ExtensionsService.
+*/
+public class ExtensionsServiceTest extends BaseServiceTest {
+
+  @Override
+  public List<ServiceTestInvocation> getTestInvocations() throws Exception {
+    List<ServiceTestInvocation> listInvocations = new ArrayList<ServiceTestInvocation>();
+
+    // getExtension
+    ExtensionsService service = new TestExtensionsService("extensionName", null);
+    Method m = service.getClass().getMethod("getExtension", String.class, HttpHeaders.class, UriInfo.class, String.class);
+    Object[] args = new Object[] {null, getHttpHeaders(), getUriInfo(), "extensionName"};
+    listInvocations.add(new ServiceTestInvocation(Request.Type.GET, service, m, args, null));
+
+    //getExtensions
+    service = new TestExtensionsService(null, null);
+    m = service.getClass().getMethod("getExtensions", String.class, HttpHeaders.class, UriInfo.class);
+    args = new Object[] {null, getHttpHeaders(), getUriInfo()};
+    listInvocations.add(new ServiceTestInvocation(Request.Type.GET, service, m, args, null));
+
+    // getExtensionVersion
+    service = new TestExtensionsService("extensionName", "extensionVersion");
+    m = service.getClass().getMethod("getExtensionVersion", String.class, HttpHeaders.class, UriInfo.class, String.class, String.class);
+    args = new Object[] {null, getHttpHeaders(), getUriInfo(), "extensionName", "extensionVersion"};
+    listInvocations.add(new ServiceTestInvocation(Request.Type.GET, service, m, args, null));
+
+    // getExtensionVersions
+    service = new TestExtensionsService("extensionName", null);
+    m = service.getClass().getMethod("getExtensionVersions", String.class, HttpHeaders.class, UriInfo.class, String.class);
+    args = new Object[] {null, getHttpHeaders(), getUriInfo(), "extensionName"};
+    listInvocations.add(new ServiceTestInvocation(Request.Type.GET, service, m, args, null));
+
+    return listInvocations;
+  }
+
+  private class TestExtensionsService extends ExtensionsService {
+
+    private String m_extensionId;
+    private String m_extensionVersion;
+
+    private TestExtensionsService(String extensionName, String extensionVersion) {
+      m_extensionId = extensionName;
+      m_extensionVersion = extensionVersion;
+    }
+
+    @Override
+    ResourceInstance createExtensionResource(String extensionName) {
+      assertEquals(m_extensionId, extensionName);
+      return getTestResource();
+    }
+
+    @Override
+    ResourceInstance createExtensionVersionResource(String extensionName, String extensionVersion) {
+      assertEquals(m_extensionId, extensionName);
+      assertEquals(m_extensionVersion, extensionVersion);
+      return getTestResource();
+    }
+
+    @Override
+    RequestFactory getRequestFactory() {
+      return getTestRequestFactory();
+    }
+
+    @Override
+    protected RequestBodyParser getBodyParser() {
+      return getTestBodyParser();
+    }
+
+    @Override
+    protected ResultSerializer getResultSerializer() {
+      return getTestResultSerializer();
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/300a7e21/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ExtensionResourceProviderTest.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ExtensionResourceProviderTest.java b/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ExtensionResourceProviderTest.java
new file mode 100644
index 0000000..8959e4d
--- /dev/null
+++ b/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ExtensionResourceProviderTest.java
@@ -0,0 +1,91 @@
+/**
+ * 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 org.apache.ambari.server.controller.AmbariManagementController;
+import org.apache.ambari.server.controller.ExtensionRequest;
+import org.apache.ambari.server.controller.ExtensionResponse;
+import org.apache.ambari.server.controller.spi.Request;
+import org.apache.ambari.server.controller.spi.Resource;
+import org.apache.ambari.server.controller.spi.ResourceProvider;
+import org.apache.ambari.server.controller.utilities.PropertyHelper;
+import org.easymock.EasyMock;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+
+/**
+ * ExtensionResourceProvider Test
+ */
+public class ExtensionResourceProviderTest {
+  @Test
+  public void testGetResources() throws Exception {
+    Resource.Type type = Resource.Type.Extension;
+
+    AmbariManagementController managementController = createMock(AmbariManagementController.class);
+
+    Set<ExtensionResponse> allResponse = new HashSet<ExtensionResponse>();
+    allResponse.add(new ExtensionResponse("Extension1"));
+    allResponse.add(new ExtensionResponse("Extension2"));
+
+    // set expectations
+    expect(managementController.getExtensions(EasyMock.<Set<ExtensionRequest>>anyObject())).andReturn(allResponse).once();
+
+    // replay
+    replay(managementController);
+
+    ResourceProvider provider = AbstractControllerResourceProvider.getResourceProvider(
+        type,
+        PropertyHelper.getPropertyIds(type),
+        PropertyHelper.getKeyPropertyIds(type),
+        managementController);
+
+    Set<String> propertyIds = new HashSet<String>();
+
+    propertyIds.add(ExtensionResourceProvider.EXTENSION_NAME_PROPERTY_ID);
+
+    // create the request
+    Request request = PropertyHelper.getReadRequest(propertyIds);
+
+    // get all ... no predicate
+    Set<Resource> resources = provider.getResources(request, null);
+
+    Assert.assertEquals(2, resources.size());
+
+
+    Set<String> extensionNames = new HashSet<String>();
+    extensionNames.add("Extension1");
+    extensionNames.add("Extension2");
+
+    for (Resource resource : resources) {
+      String extensionName = (String) resource.getPropertyValue(ExtensionResourceProvider.EXTENSION_NAME_PROPERTY_ID);
+      Assert.assertTrue(extensionNames.contains(extensionName));
+    }
+
+    // verify
+    verify(managementController);
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/300a7e21/ambari-server/src/test/java/org/apache/ambari/server/stack/ComponentModuleTest.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/stack/ComponentModuleTest.java b/ambari-server/src/test/java/org/apache/ambari/server/stack/ComponentModuleTest.java
index a02311a..f21b250 100644
--- a/ambari-server/src/test/java/org/apache/ambari/server/stack/ComponentModuleTest.java
+++ b/ambari-server/src/test/java/org/apache/ambari/server/stack/ComponentModuleTest.java
@@ -495,7 +495,7 @@ public class ComponentModuleTest {
     ComponentModule component = new ComponentModule(info);
     ComponentModule parentComponent = new ComponentModule(parentInfo);
 
-    component.resolve(parentComponent, Collections.<String, StackModule>emptyMap(), Collections.<String, ServiceModule>emptyMap());
+    component.resolve(parentComponent, Collections.<String, StackModule>emptyMap(), Collections.<String, ServiceModule>emptyMap(), Collections.<String, ExtensionModule>emptyMap());
 
     return component;
   }

http://git-wip-us.apache.org/repos/asf/ambari/blob/300a7e21/ambari-server/src/test/java/org/apache/ambari/server/stack/QuickLinksConfigurationModuleTest.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/stack/QuickLinksConfigurationModuleTest.java b/ambari-server/src/test/java/org/apache/ambari/server/stack/QuickLinksConfigurationModuleTest.java
index 8fcc76e..38176aa 100644
--- a/ambari-server/src/test/java/org/apache/ambari/server/stack/QuickLinksConfigurationModuleTest.java
+++ b/ambari-server/src/test/java/org/apache/ambari/server/stack/QuickLinksConfigurationModuleTest.java
@@ -118,11 +118,11 @@ public class QuickLinksConfigurationModuleTest {
     QuickLinksConfigurationModule parentModule = new QuickLinksConfigurationModule(parentQuiclinksFile);
     QuickLinksConfigurationModule childModule = new QuickLinksConfigurationModule(childQuickLinksFile);
 
-    childModule.resolve(parentModule, null, null);
+    childModule.resolve(parentModule, null, null, null);
 
     QuickLinks parentQuickLinks = parentModule.getModuleInfo().getQuickLinksConfigurationMap().get(QuickLinksConfigurationModule.QUICKLINKS_CONFIGURATION_KEY);
     QuickLinks childQuickLinks = childModule.getModuleInfo().getQuickLinksConfigurationMap().get(QuickLinksConfigurationModule.QUICKLINKS_CONFIGURATION_KEY);
 
     return new QuickLinks[]{parentQuickLinks, childQuickLinks};
   }
-}
\ No newline at end of file
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/300a7e21/ambari-server/src/test/java/org/apache/ambari/server/stack/ServiceModuleTest.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/stack/ServiceModuleTest.java b/ambari-server/src/test/java/org/apache/ambari/server/stack/ServiceModuleTest.java
index c9bcf60..304fd5c 100644
--- a/ambari-server/src/test/java/org/apache/ambari/server/stack/ServiceModuleTest.java
+++ b/ambari-server/src/test/java/org/apache/ambari/server/stack/ServiceModuleTest.java
@@ -1211,7 +1211,7 @@ public class ServiceModuleTest {
   }
 
   private void resolveService(ServiceModule service, ServiceModule parent) throws AmbariException {
-    service.resolve(parent, Collections.<String, StackModule>emptyMap(), Collections.<String, ServiceModule>emptyMap());
+    service.resolve(parent, Collections.<String, StackModule>emptyMap(), Collections.<String, ServiceModule>emptyMap(), Collections.<String, ExtensionModule>emptyMap());
     // during runtime this would be called by the Stack module when it's resolve completed
     service.finalizeModule();
     parent.finalizeModule();

http://git-wip-us.apache.org/repos/asf/ambari/blob/300a7e21/ambari-server/src/test/java/org/apache/ambari/server/stack/StackManagerCommonServicesTest.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/stack/StackManagerCommonServicesTest.java b/ambari-server/src/test/java/org/apache/ambari/server/stack/StackManagerCommonServicesTest.java
index 969e07c..1d73ff3 100644
--- a/ambari-server/src/test/java/org/apache/ambari/server/stack/StackManagerCommonServicesTest.java
+++ b/ambari-server/src/test/java/org/apache/ambari/server/stack/StackManagerCommonServicesTest.java
@@ -29,14 +29,19 @@ import static org.junit.Assert.assertTrue;
 
 import java.io.File;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
 import org.apache.ambari.server.configuration.Configuration;
 import org.apache.ambari.server.metadata.ActionMetadata;
+import org.apache.ambari.server.orm.dao.ExtensionDAO;
+import org.apache.ambari.server.orm.dao.ExtensionLinkDAO;
 import org.apache.ambari.server.orm.dao.MetainfoDAO;
 import org.apache.ambari.server.orm.dao.StackDAO;
+import org.apache.ambari.server.orm.entities.ExtensionEntity;
+import org.apache.ambari.server.orm.entities.ExtensionLinkEntity;
 import org.apache.ambari.server.orm.entities.StackEntity;
 import org.apache.ambari.server.state.CommandScriptDefinition;
 import org.apache.ambari.server.state.ComponentInfo;
@@ -58,6 +63,8 @@ public class StackManagerCommonServicesTest {
   private static StackManager stackManager;
   private static MetainfoDAO metaInfoDao;
   private static StackDAO stackDao;
+  private static ExtensionDAO extensionDao;
+  private static ExtensionLinkDAO linkDao;
   private static ActionMetadata actionMetadata;
   private static OsFamily osFamily;
 
@@ -72,17 +79,22 @@ public class StackManagerCommonServicesTest {
 
     String commonServices = ClassLoader.getSystemClassLoader().getResource(
         "common-services").getPath();
-    return createTestStackManager(stack, commonServices);
+    String extensions = ClassLoader.getSystemClassLoader().getResource(
+            "extensions").getPath();
+    return createTestStackManager(stack, commonServices, extensions);
   }
 
   public static StackManager createTestStackManager(String stackRoot,
-      String commonServicesRoot) throws Exception {
+      String commonServicesRoot, String extensionRoot) throws Exception {
     // todo: dao , actionMetaData expectations
     metaInfoDao = createNiceMock(MetainfoDAO.class);
     stackDao = createNiceMock(StackDAO.class);
+    extensionDao = createNiceMock(ExtensionDAO.class);
+    linkDao = createNiceMock(ExtensionLinkDAO.class);
     actionMetadata = createNiceMock(ActionMetadata.class);
     Configuration config = createNiceMock(Configuration.class);
     StackEntity stackEntity = createNiceMock(StackEntity.class);
+    ExtensionEntity extensionEntity = createNiceMock(ExtensionEntity.class);
 
     expect(config.getSharedResourcesDirPath()).andReturn(
         ClassLoader.getSystemClassLoader().getResource("").getPath()).anyTimes();
@@ -91,13 +103,25 @@ public class StackManagerCommonServicesTest {
         stackDao.find(EasyMock.anyObject(String.class),
             EasyMock.anyObject(String.class))).andReturn(stackEntity).atLeastOnce();
 
-    replay(config, stackDao);
+
+    expect(
+        extensionDao.find(EasyMock.anyObject(String.class),
+            EasyMock.anyObject(String.class))).andReturn(extensionEntity).atLeastOnce();
+
+    List<ExtensionLinkEntity> list = Collections.emptyList();
+    expect(
+        linkDao.findByStack(EasyMock.anyObject(String.class),
+            EasyMock.anyObject(String.class))).andReturn(list).atLeastOnce();
+
+    replay(config, stackDao, extensionDao, linkDao);
+
     osFamily = new OsFamily(config);
 
     replay(metaInfoDao, actionMetadata);
 
     StackManager stackManager = new StackManager(new File(stackRoot), new File(
-        commonServicesRoot), osFamily, false, metaInfoDao, actionMetadata, stackDao);
+        commonServicesRoot), new File(extensionRoot), osFamily, true, metaInfoDao,
+        actionMetadata, stackDao, extensionDao, linkDao);
 
     EasyMock.verify( config, stackDao );
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/300a7e21/ambari-server/src/test/java/org/apache/ambari/server/stack/StackManagerExtensionTest.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/stack/StackManagerExtensionTest.java b/ambari-server/src/test/java/org/apache/ambari/server/stack/StackManagerExtensionTest.java
new file mode 100644
index 0000000..659ae12
--- /dev/null
+++ b/ambari-server/src/test/java/org/apache/ambari/server/stack/StackManagerExtensionTest.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.stack;
+
+import static org.easymock.EasyMock.createNiceMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.ambari.server.metadata.ActionMetadata;
+import org.apache.ambari.server.orm.dao.ExtensionDAO;
+import org.apache.ambari.server.orm.dao.ExtensionLinkDAO;
+import org.apache.ambari.server.orm.dao.MetainfoDAO;
+import org.apache.ambari.server.orm.dao.StackDAO;
+import org.apache.ambari.server.orm.entities.ExtensionEntity;
+import org.apache.ambari.server.orm.entities.ExtensionLinkEntity;
+import org.apache.ambari.server.orm.entities.StackEntity;
+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.OsFamily;
+import org.easymock.EasyMock;
+import org.junit.Test;
+
+/**
+ * StackManager extension unit tests.
+ */
+public class StackManagerExtensionTest  {
+
+  @Test
+  public void testExtensions() throws Exception {
+    MetainfoDAO metaInfoDao = createNiceMock(MetainfoDAO.class);
+    StackDAO stackDao = createNiceMock(StackDAO.class);
+    ExtensionDAO extensionDao = createNiceMock(ExtensionDAO.class);
+    ExtensionLinkDAO linkDao = createNiceMock(ExtensionLinkDAO.class);
+    ActionMetadata actionMetadata = createNiceMock(ActionMetadata.class);
+    OsFamily osFamily = createNiceMock(OsFamily.class);
+    StackEntity stackEntity = createNiceMock(StackEntity.class);
+    ExtensionEntity extensionEntity = createNiceMock(ExtensionEntity.class);
+    ExtensionLinkEntity linkEntity = createNiceMock(ExtensionLinkEntity.class);
+    List<ExtensionLinkEntity> list = new ArrayList<ExtensionLinkEntity>();
+    list.add(linkEntity);
+
+    expect(
+        stackDao.find(EasyMock.anyObject(String.class),
+            EasyMock.anyObject(String.class))).andReturn(stackEntity).atLeastOnce();
+
+    expect(
+        extensionDao.find(EasyMock.anyObject(String.class),
+            EasyMock.anyObject(String.class))).andReturn(extensionEntity).atLeastOnce();
+
+    expect(
+        linkDao.findByStack(EasyMock.anyObject(String.class),
+            EasyMock.anyObject(String.class))).andReturn(list).atLeastOnce();
+
+    expect(
+        linkEntity.getExtension()).andReturn(extensionEntity).atLeastOnce();
+
+    expect(
+        extensionEntity.getExtensionName()).andReturn("EXT").atLeastOnce();
+
+    expect(
+        extensionEntity.getExtensionVersion()).andReturn("0.2").atLeastOnce();
+
+    replay(actionMetadata, stackDao, metaInfoDao, osFamily, extensionDao, linkDao, extensionEntity, linkEntity);
+
+    String stacks = ClassLoader.getSystemClassLoader().getResource("stacks_with_extensions").getPath();
+    String common = ClassLoader.getSystemClassLoader().getResource("common-services").getPath();
+    String extensions = ClassLoader.getSystemClassLoader().getResource("extensions").getPath();
+
+    StackManager stackManager = new StackManager(new File(stacks),
+        new File(common), new File(extensions), osFamily, false,
+        metaInfoDao, actionMetadata, stackDao, extensionDao, linkDao);
+
+    ExtensionInfo extension = stackManager.getExtension("EXT", "0.1");
+    assertNull("EXT 0.1's parent: " + extension.getParentExtensionVersion(), extension.getParentExtensionVersion());
+    assertNotNull(extension.getService("OOZIE2"));
+    ServiceInfo oozie = extension.getService("OOZIE2");
+    assertNotNull("Package dir is " + oozie.getServicePackageFolder(), oozie.getServicePackageFolder());
+    assertTrue("Package dir is " + oozie.getServicePackageFolder(), oozie.getServicePackageFolder().contains("extensions/EXT/0.1/services/OOZIE2/package"));
+    assertEquals(oozie.getVersion(), "3.2.0");
+
+    extension = stackManager.getExtension("EXT", "0.2");
+    assertNotNull("EXT 0.2's parent: " + extension.getParentExtensionVersion(), extension.getParentExtensionVersion());
+    assertEquals("EXT 0.2's parent: " + extension.getParentExtensionVersion(), "0.1", extension.getParentExtensionVersion());
+    assertNotNull(extension.getService("OOZIE2"));
+    oozie = extension.getService("OOZIE2");
+    assertNotNull("Package dir is " + oozie.getServicePackageFolder(), oozie.getServicePackageFolder());
+    assertTrue("Package dir is " + oozie.getServicePackageFolder(), oozie.getServicePackageFolder().contains("extensions/EXT/0.1/services/OOZIE2/package"));
+    assertEquals(oozie.getVersion(), "4.0.0");
+
+    StackInfo stack = stackManager.getStack("HDP", "0.2");
+    assertNotNull(stack.getService("OOZIE2"));
+    oozie = stack.getService("OOZIE2");
+    assertNotNull("Package dir is " + oozie.getServicePackageFolder(), oozie.getServicePackageFolder());
+    assertTrue("Package dir is " + oozie.getServicePackageFolder(), oozie.getServicePackageFolder().contains("extensions/EXT/0.1/services/OOZIE2/package"));
+    assertEquals(oozie.getVersion(), "4.0.0");
+
+    assertTrue("Extensions found: " + stack.getExtensions().size(), stack.getExtensions().size() == 1);
+    extension = stack.getExtensions().iterator().next();
+    assertEquals("Extension name: " + extension.getName(), extension.getName(), "EXT");
+    assertEquals("Extension version: " + extension.getVersion(), extension.getVersion(), "0.2");
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/300a7e21/ambari-server/src/test/java/org/apache/ambari/server/stack/StackManagerMiscTest.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/stack/StackManagerMiscTest.java b/ambari-server/src/test/java/org/apache/ambari/server/stack/StackManagerMiscTest.java
index 55a3c46..ca24cd9 100644
--- a/ambari-server/src/test/java/org/apache/ambari/server/stack/StackManagerMiscTest.java
+++ b/ambari-server/src/test/java/org/apache/ambari/server/stack/StackManagerMiscTest.java
@@ -28,11 +28,16 @@ import static org.junit.Assert.fail;
 
 import java.io.File;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
 
 import org.apache.ambari.server.AmbariException;
 import org.apache.ambari.server.metadata.ActionMetadata;
+import org.apache.ambari.server.orm.dao.ExtensionDAO;
+import org.apache.ambari.server.orm.dao.ExtensionLinkDAO;
 import org.apache.ambari.server.orm.dao.MetainfoDAO;
 import org.apache.ambari.server.orm.dao.StackDAO;
+import org.apache.ambari.server.orm.entities.ExtensionLinkEntity;
 import org.apache.ambari.server.orm.entities.StackEntity;
 import org.apache.ambari.server.state.StackInfo;
 import org.apache.ambari.server.state.stack.OsFamily;
@@ -48,6 +53,8 @@ public class StackManagerMiscTest  {
   public void testCycleDetection() throws Exception {
     MetainfoDAO metaInfoDao = createNiceMock(MetainfoDAO.class);
     StackDAO stackDao = createNiceMock(StackDAO.class);
+    ExtensionDAO extensionDao = createNiceMock(ExtensionDAO.class);
+    ExtensionLinkDAO linkDao = createNiceMock(ExtensionLinkDAO.class);
     ActionMetadata actionMetadata = createNiceMock(ActionMetadata.class);
     OsFamily osFamily = createNiceMock(OsFamily.class);
     StackEntity stackEntity = createNiceMock(StackEntity.class);
@@ -56,13 +63,18 @@ public class StackManagerMiscTest  {
         stackDao.find(EasyMock.anyObject(String.class),
             EasyMock.anyObject(String.class))).andReturn(stackEntity).atLeastOnce();
 
-    replay(actionMetadata, stackDao, metaInfoDao, osFamily);
+    List<ExtensionLinkEntity> list = Collections.emptyList();
+    expect(
+        linkDao.findByStack(EasyMock.anyObject(String.class),
+            EasyMock.anyObject(String.class))).andReturn(list).atLeastOnce();
+
+    replay(actionMetadata, stackDao, extensionDao, linkDao, metaInfoDao, osFamily);
 
     try {
       String stacksCycle1 = ClassLoader.getSystemClassLoader().getResource("stacks_with_cycle").getPath();
 
-      StackManager stackManager = new StackManager(new File(stacksCycle1),
-          null, osFamily, false, metaInfoDao, actionMetadata, stackDao);
+      StackManager stackManager = new StackManager(new File(stacksCycle1), null, null, osFamily, false,
+          metaInfoDao, actionMetadata, stackDao, extensionDao, linkDao);
 
       fail("Expected exception due to cyclic stack");
     } catch (AmbariException e) {
@@ -74,7 +86,7 @@ public class StackManagerMiscTest  {
           "stacks_with_cycle2").getPath();
 
       StackManager stackManager = new StackManager(new File(stacksCycle2),
-          null, osFamily, true, metaInfoDao, actionMetadata, stackDao);
+          null, null, osFamily, true, metaInfoDao, actionMetadata, stackDao, extensionDao, linkDao);
 
       fail("Expected exception due to cyclic stack");
     } catch (AmbariException e) {
@@ -91,6 +103,8 @@ public class StackManagerMiscTest  {
   public void testGetServiceInfoFromSingleStack() throws Exception {
     MetainfoDAO metaInfoDao = createNiceMock(MetainfoDAO.class);
     StackDAO stackDao = createNiceMock(StackDAO.class);
+    ExtensionDAO extensionDao = createNiceMock(ExtensionDAO.class);
+    ExtensionLinkDAO linkDao = createNiceMock(ExtensionLinkDAO.class);
     ActionMetadata actionMetadata = createNiceMock(ActionMetadata.class);
     OsFamily  osFamily = createNiceMock(OsFamily.class);
     StackEntity stackEntity = createNiceMock(StackEntity.class);
@@ -102,14 +116,18 @@ public class StackManagerMiscTest  {
         stackDao.find(EasyMock.anyObject(String.class),
             EasyMock.anyObject(String.class))).andReturn(stackEntity).atLeastOnce();
 
-    replay(metaInfoDao, stackDao, actionMetadata, osFamily);
+    List<ExtensionLinkEntity> list = Collections.emptyList();
+    expect(
+        linkDao.findByStack(EasyMock.anyObject(String.class),
+            EasyMock.anyObject(String.class))).andReturn(list).atLeastOnce();
+
+    replay(metaInfoDao, stackDao, extensionDao, linkDao, actionMetadata, osFamily);
 
     String singleStack = ClassLoader.getSystemClassLoader().getResource("single_stack").getPath();
 
     StackManager stackManager = new StackManager(new File(singleStack.replace(
-        StackManager.PATH_DELIMITER, File.separator)),
-        null, osFamily, false, metaInfoDao, actionMetadata, stackDao);
-
+        StackManager.PATH_DELIMITER, File.separator)), null, null, osFamily, false, metaInfoDao,
+        actionMetadata, stackDao, extensionDao, linkDao);
 
     Collection<StackInfo> stacks = stackManager.getStacks();
     assertEquals(1, stacks.size());
@@ -126,6 +144,8 @@ public class StackManagerMiscTest  {
   public void testCircularDependencyForServiceUpgrade() throws Exception {
     MetainfoDAO metaInfoDao = createNiceMock(MetainfoDAO.class);
     StackDAO stackDao = createNiceMock(StackDAO.class);
+    ExtensionDAO extensionDao = createNiceMock(ExtensionDAO.class);
+    ExtensionLinkDAO linkDao = createNiceMock(ExtensionLinkDAO.class);
     ActionMetadata actionMetadata = createNiceMock(ActionMetadata.class);
     OsFamily osFamily = createNiceMock(OsFamily.class);
     StackEntity stackEntity = createNiceMock(StackEntity.class);
@@ -134,13 +154,18 @@ public class StackManagerMiscTest  {
         stackDao.find(EasyMock.anyObject(String.class),
             EasyMock.anyObject(String.class))).andReturn(stackEntity).atLeastOnce();
 
-    replay(actionMetadata, stackDao, metaInfoDao, osFamily);
+    List<ExtensionLinkEntity> list = Collections.emptyList();
+    expect(
+        linkDao.findByStack(EasyMock.anyObject(String.class),
+            EasyMock.anyObject(String.class))).andReturn(list).atLeastOnce();
+
+    replay(metaInfoDao, stackDao, extensionDao, linkDao, actionMetadata, osFamily);
 
     try {
       String upgradeCycle = ClassLoader.getSystemClassLoader().getResource("stacks_with_upgrade_cycle").getPath();
 
-      StackManager stackManager = new StackManager(new File(upgradeCycle),
-          null, osFamily, false, metaInfoDao, actionMetadata, stackDao);
+      StackManager stackManager = new StackManager(new File(upgradeCycle), null, null, osFamily, false,
+          metaInfoDao, actionMetadata, stackDao, extensionDao, linkDao);
 
       fail("Expected exception due to cyclic service upgrade xml");
     } catch (AmbariException e) {