knox-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From pzamp...@apache.org
Subject [knox] branch master updated: KNOX-2153 - CM discovery - Monitor Cloudera Manager (#239)
Date Thu, 30 Jan 2020 15:20:26 GMT
This is an automated email from the ASF dual-hosted git repository.

pzampino pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git


The following commit(s) were added to refs/heads/master by this push:
     new ce2f1bb  KNOX-2153 - CM discovery - Monitor Cloudera Manager (#239)
ce2f1bb is described below

commit ce2f1bbd84a3d50f0719dc344fb59150f72ac419
Author: Phil Zampino <pzampino@apache.org>
AuthorDate: Thu Jan 30 10:20:20 2020 -0500

    KNOX-2153 - CM discovery - Monitor Cloudera Manager (#239)
---
 .../ambari/AmbariConfigurationMonitor.java         |   5 +
 gateway-discovery-cm/pom.xml                       |  24 ++
 .../discovery/cm/ClouderaManagerCluster.java       |  28 +-
 .../cm/ClouderaManagerServiceDiscovery.java        |  64 ++-
 .../ClouderaManagerServiceDiscoveryMessages.java   |  87 ++++
 .../topology/discovery/cm/DiscoveryApiClient.java  |   9 +-
 .../topology/discovery/cm/ServiceModel.java        |  37 ++
 .../cm/model/atlas/AtlasServiceModelGenerator.java |  20 +-
 .../model/hbase/HBaseUIServiceModelGenerator.java  |  16 +-
 .../model/hbase/WebHBaseServiceModelGenerator.java |  15 +-
 .../cm/model/hdfs/HdfsUIServiceModelGenerator.java |  20 +-
 .../model/hdfs/NameNodeServiceModelGenerator.java  |  20 +-
 .../model/hdfs/WebHdfsServiceModelGenerator.java   |  33 +-
 .../model/hive/HiveOnTezServiceModelGenerator.java |  31 +-
 .../cm/model/hive/HiveServiceModelGenerator.java   |  52 ++-
 .../model/hive/WebHCatServiceModelGenerator.java   |  10 +-
 .../cm/model/hue/HueLBServiceModelGenerator.java   |  10 +-
 .../cm/model/hue/HueServiceModelGenerator.java     |  14 +-
 .../model/impala/ImpalaServiceModelGenerator.java  |  19 +-
 .../impala/ImpalaUIServiceModelGenerator.java      |  19 +-
 .../cm/model/kudu/KuduUIServiceModelGenerator.java |  20 +-
 .../cm/model/livy/LivyServiceModelGenerator.java   |  16 +-
 .../nifi/NifiRegistryServiceModelGenerator.java    |  26 +-
 .../cm/model/nifi/NifiServiceModelGenerator.java   |  25 +-
 .../cm/model/oozie/OozieServiceModelGenerator.java |  21 +-
 .../phoenix/PhoenixServiceModelGenerator.java      |  10 +-
 .../model/ranger/RangerServiceModelGenerator.java  |  20 +-
 .../cm/model/solr/SolrServiceModelGenerator.java   |  21 +-
 .../spark/SparkHistoryUIServiceModelGenerator.java |  20 +-
 .../yarn/JobHistoryUIServiceModelGenerator.java    |  14 +-
 .../yarn/JobTrackerServiceModelGenerator.java      |  12 +-
 .../cm/model/yarn/YarnUIServiceModelGenerator.java |  16 +-
 .../zeppelin/ZeppelinServiceModelGenerator.java    |  18 +-
 .../zeppelin/ZeppelinWSServiceModelGenerator.java  |   9 +-
 .../cm/monitor/AbstractConfigurationStore.java     |  86 ++++
 ...ClouderaManagerClusterConfigurationMonitor.java | 227 ++++++++++
 ...anagerClusterConfigurationMonitorProvider.java} |  19 +-
 .../cm/monitor/ClusterConfigurationCache.java      | 162 +++++++
 .../cm/monitor/ClusterConfigurationFileStore.java  | 113 +++++
 .../cm/monitor/ClusterConfigurationStore.java      |  61 +++
 .../monitor/DiscoveryConfigurationFileStore.java   | 133 ++++++
 .../cm/monitor/DiscoveryConfigurationStore.java    |  50 +++
 .../cm/monitor/PollingConfigurationAnalyzer.java   | 464 +++++++++++++++++++++
 .../cm/monitor/ServiceConfigurationModel.java      |  94 +++++
 .../cm/monitor/ServiceConfigurationRecord.java     |  55 +++
 ...y.discovery.ClusterConfigurationMonitorProvider |  19 +
 .../discovery/cm/AbstractCMDiscoveryTest.java      |  93 +++++
 .../cm/ClouderaManagerServiceDiscoveryTest.java    |   4 +-
 .../model/AbstractServiceModelGeneratorTest.java   | 118 ++++++
 .../atlas/AtlasAPIServiceModelGeneratorTest.java}  |  19 +-
 .../atlas/AtlasServiceModelGeneratorTest.java      |  56 +++
 .../hbase/HBaseUIServiceModelGeneratorTest.java    |  53 +++
 .../hbase/WebHBaseServiceModelGeneratorTest.java   |  55 +++
 .../hdfs/HdfsUIServiceModelGeneratorTest.java      |  53 +++
 .../hdfs/NameNodeServiceModelGeneratorTest.java    |  65 +++
 .../hdfs/WebHdfsServiceModelGeneratorTest.java     |  74 ++++
 .../hive/HiveOnTezServiceModelGeneratorTest.java   |  77 ++++
 .../model/hive/HiveServiceModelGeneratorTest.java  |  80 ++++
 .../model/hue/HueLBServiceModelGeneratorTest.java  |  53 +++
 .../cm/model/hue/HueServiceModelGeneratorTest.java |  54 +++
 .../impala/ImpalaServiceModelGeneratorTest.java    |  53 +++
 .../impala/ImpalaUIServiceModelGeneratorTest.java  |  73 ++++
 .../kudu/KuduUIServiceModelGeneratorTest.java      |  55 +++
 .../model/livy/LivyServiceModelGeneratorTest.java  |  54 +++
 .../NifiRegistryServiceModelGeneratorTest.java     |  55 +++
 .../model/nifi/NifiServiceModelGeneratorTest.java  |  54 +++
 .../oozie/OozieServiceModelGeneratorTest.java      |  54 +++
 .../oozie/OozieUIServiceModelGeneratorTest.java}   |  19 +-
 .../phoenix/PhoenixServiceModelGeneratorTest.java  |  53 +++
 .../ranger/RangerServiceModelGeneratorTest.java    |  54 +++
 .../ranger/RangerUIServiceModelGeneratorTest.java} |  19 +-
 .../model/solr/SolrServiceModelGeneratorTest.java  |  54 +++
 .../SparkHistoryUIServiceModelGeneratorTest.java   |  55 +++
 .../ZeppelinServiceModelGeneratorTest.java         |  55 +++
 .../ZeppelinUIServiceModelGeneratorTest.java}      |  20 +-
 .../ZeppelinWSServiceModelGeneratorTest.java}      |  19 +-
 .../cm/monitor/AbstractConfigurationStoreTest.java |  61 +++
 .../monitor/ClusterConfigurationFileStoreTest.java | 130 ++++++
 .../DiscoveryConfigurationFileStoreTest.java       | 129 ++++++
 .../monitor/PollingConfigurationAnalyzerTest.java  | 251 +++++++++++
 .../cm/monitor/ServiceConfigurationModelTest.java  | 156 +++++++
 .../topology/impl/DefaultTopologyService.java      |  18 +-
 .../topology/discovery/ServiceDiscoveryConfig.java |   6 +
 .../discovery/DefaultServiceDiscoveryConfig.java   |  10 +
 .../topology/simple/SimpleDescriptorHandler.java   |   1 +
 85 files changed, 4413 insertions(+), 213 deletions(-)

diff --git a/gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/AmbariConfigurationMonitor.java b/gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/AmbariConfigurationMonitor.java
index 7867c6e..642a84f 100644
--- a/gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/AmbariConfigurationMonitor.java
+++ b/gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/AmbariConfigurationMonitor.java
@@ -124,6 +124,11 @@ class AmbariConfigurationMonitor implements ClusterConfigurationMonitor {
                                                             }
 
                                                             @Override
+                                                            public String getCluster() {
+                                                                return props.getProperty(PROP_CLUSTER_NAME);
+                                                            }
+
+                                                            @Override
                                                             public String getUser() {
                                                                 return props.getProperty(PROP_CLUSTER_USER);
                                                             }
diff --git a/gateway-discovery-cm/pom.xml b/gateway-discovery-cm/pom.xml
index 3db6ffc..f3ee23e 100644
--- a/gateway-discovery-cm/pom.xml
+++ b/gateway-discovery-cm/pom.xml
@@ -30,6 +30,10 @@
     <dependencies>
         <dependency>
             <groupId>org.apache.knox</groupId>
+            <artifactId>gateway-server</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.knox</groupId>
             <artifactId>gateway-spi</artifactId>
         </dependency>
         <dependency>
@@ -49,9 +53,29 @@
             <artifactId>okhttp</artifactId>
         </dependency>
         <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+        </dependency>
+        <dependency>
             <groupId>org.apache.httpcomponents</groupId>
             <artifactId>httpcore</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-annotations</artifactId>
+        </dependency>
     </dependencies>
 </project>
 
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/ClouderaManagerCluster.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/ClouderaManagerCluster.java
index e6a36cb..168dca3 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/ClouderaManagerCluster.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/ClouderaManagerCluster.java
@@ -73,32 +73,8 @@ public class ClouderaManagerCluster implements ServiceDiscovery.Cluster {
     }
   }
 
-  static class ServiceConfiguration {
-
-    private String type;
-    private String version;
-    private Map<String, String> props;
-
-    ServiceConfiguration(String type, String version, Map<String, String> properties) {
-      this.type = type;
-      this.version = version;
-      this.props = properties;
-    }
-
-    public String getVersion() {
-      return version;
-    }
-
-    public String getType() {
-      return type;
-    }
-
-    public Map<String, String> getProperties() {
-      return props;
-    }
+  public Map<String, List<ServiceModel>> getServiceModels() {
+    return serviceModels;
   }
 
-
-
-
 }
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/ClouderaManagerServiceDiscovery.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/ClouderaManagerServiceDiscovery.java
index b4396ed..b6c0199 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/ClouderaManagerServiceDiscovery.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/ClouderaManagerServiceDiscovery.java
@@ -28,12 +28,17 @@ import com.cloudera.api.swagger.model.ApiRoleList;
 import com.cloudera.api.swagger.model.ApiService;
 import com.cloudera.api.swagger.model.ApiServiceConfig;
 import com.cloudera.api.swagger.model.ApiServiceList;
+import org.apache.knox.gateway.GatewayServer;
 import org.apache.knox.gateway.config.GatewayConfig;
 import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.ServiceType;
 import org.apache.knox.gateway.services.security.AliasService;
-import org.apache.knox.gateway.topology.discovery.GatewayService;
+import org.apache.knox.gateway.topology.ClusterConfigurationMonitorService;
+import org.apache.knox.gateway.topology.discovery.ClusterConfigurationMonitor;
 import org.apache.knox.gateway.topology.discovery.ServiceDiscovery;
 import org.apache.knox.gateway.topology.discovery.ServiceDiscoveryConfig;
+import org.apache.knox.gateway.topology.discovery.cm.monitor.ClouderaManagerClusterConfigurationMonitor;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -75,16 +80,22 @@ public class ClouderaManagerServiceDiscovery implements ServiceDiscovery {
 
   private boolean debug;
 
-  @GatewayService
   private AliasService aliasService;
 
+  private ClouderaManagerClusterConfigurationMonitor configChangeMonitor;
+
 
   ClouderaManagerServiceDiscovery() {
     this(false);
   }
 
   ClouderaManagerServiceDiscovery(boolean debug) {
+    GatewayServices gwServices = GatewayServer.getGatewayServices();
+    if (gwServices != null) {
+      this.aliasService = gwServices.getService(ServiceType.ALIAS_SERVICE);
+    }
     this.debug = debug;
+    this.configChangeMonitor = getConfigurationChangeMonitor();
   }
 
   @Override
@@ -105,6 +116,29 @@ public class ClouderaManagerServiceDiscovery implements ServiceDiscovery {
     return client;
   }
 
+  /**
+   * Get the ClouderaManager configuration change monitor from the associated gateway service.
+   */
+  private ClouderaManagerClusterConfigurationMonitor getConfigurationChangeMonitor() {
+    ClouderaManagerClusterConfigurationMonitor cmMonitor = null;
+
+    try {
+      GatewayServices gwServices = GatewayServer.getGatewayServices();
+      if (gwServices != null) {
+        ClusterConfigurationMonitorService clusterMonitorService =
+            GatewayServer.getGatewayServices().getService(ServiceType.CLUSTER_CONFIGURATION_MONITOR_SERVICE);
+        ClusterConfigurationMonitor monitor =
+            clusterMonitorService.getMonitor(ClouderaManagerClusterConfigurationMonitor.getType());
+        if (monitor != null && ClouderaManagerClusterConfigurationMonitor.class.isAssignableFrom(monitor.getClass())) {
+          cmMonitor = (ClouderaManagerClusterConfigurationMonitor) monitor;
+        }
+      }
+    } catch (Exception e) {
+      log.errorAccessingConfigurationChangeMonitor(e);
+    }
+    return cmMonitor;
+  }
+
   @Override
   public Map<String, Cluster> discover(GatewayConfig gatewayConfig, ServiceDiscoveryConfig discoveryConfig) {
     Map<String, Cluster> clusters = new HashMap<>();
@@ -115,7 +149,7 @@ public class ClouderaManagerServiceDiscovery implements ServiceDiscovery {
       String clusterName = apiCluster.getName();
       log.discoveredCluster(clusterName, apiCluster.getFullVersion());
 
-      Cluster cluster = discover(gatewayConfig, discoveryConfig, clusterName, client);
+      ClouderaManagerCluster cluster = discover(gatewayConfig, discoveryConfig, clusterName, client);
       clusters.put(clusterName, cluster);
     }
 
@@ -123,15 +157,17 @@ public class ClouderaManagerServiceDiscovery implements ServiceDiscovery {
   }
 
   @Override
-  public Cluster discover(GatewayConfig gatewayConfig, ServiceDiscoveryConfig discoveryConfig, String clusterName) {
+  public ClouderaManagerCluster discover(GatewayConfig          gatewayConfig,
+                                         ServiceDiscoveryConfig discoveryConfig,
+                                         String                 clusterName) {
     return discover(gatewayConfig, discoveryConfig, clusterName, getClient(discoveryConfig));
   }
 
-  protected Cluster discover(GatewayConfig          gatewayConfig,
-                             ServiceDiscoveryConfig discoveryConfig,
-                             String                 clusterName,
-                             DiscoveryApiClient     client) {
-    ServiceDiscovery.Cluster cluster = null;
+  protected ClouderaManagerCluster discover(GatewayConfig          gatewayConfig,
+                                            ServiceDiscoveryConfig discoveryConfig,
+                                            String                 clusterName,
+                                            DiscoveryApiClient     client) {
+    ClouderaManagerCluster cluster = null;
 
     if (clusterName == null || clusterName.isEmpty()) {
       log.missingDiscoveryCluster();
@@ -140,6 +176,11 @@ public class ClouderaManagerServiceDiscovery implements ServiceDiscovery {
 
     try {
       cluster = discoverCluster(client, clusterName);
+
+      if (configChangeMonitor != null) {
+        // Notify the cluster config monitor about these cluster configuration details
+        configChangeMonitor.addServiceConfiguration(cluster, discoveryConfig);
+      }
     } catch (ApiException e) {
       log.clusterDiscoveryError(clusterName, e);
     }
@@ -161,8 +202,9 @@ public class ClouderaManagerServiceDiscovery implements ServiceDiscovery {
     return clusters;
   }
 
-  private static Cluster discoverCluster(DiscoveryApiClient client, String clusterName) throws ApiException {
-    ClouderaManagerCluster cluster = null;
+  private static ClouderaManagerCluster discoverCluster(DiscoveryApiClient client, String clusterName)
+      throws ApiException {
+    ClouderaManagerCluster cluster;
 
     ServicesResourceApi servicesResourceApi = new ServicesResourceApi(client);
     RolesResourceApi rolesResourceApi = new RolesResourceApi(client);
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/ClouderaManagerServiceDiscoveryMessages.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/ClouderaManagerServiceDiscoveryMessages.java
index 8bc81d1..abc5d8e 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/ClouderaManagerServiceDiscoveryMessages.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/ClouderaManagerServiceDiscoveryMessages.java
@@ -16,6 +16,7 @@
  */
 package org.apache.knox.gateway.topology.discovery.cm;
 
+import com.cloudera.api.swagger.client.ApiException;
 import org.apache.knox.gateway.i18n.messages.Message;
 import org.apache.knox.gateway.i18n.messages.MessageLevel;
 import org.apache.knox.gateway.i18n.messages.Messages;
@@ -97,4 +98,90 @@ public interface ClouderaManagerServiceDiscoveryMessages {
            text = "No password configured for Cloudera Manager service discovery.")
   void aliasServicePasswordNotFound();
 
+  @Message(level = MessageLevel.ERROR,
+           text = "Unable to access the ClouderaManager Configuration Change Monitor: {0}")
+  void errorAccessingConfigurationChangeMonitor(@StackTrace(level = MessageLevel.DEBUG) Exception e);
+
+  @Message(level = MessageLevel.INFO, text = "Starting ClouderaManager cluster configuration monitor")
+  void startingClouderaManagerConfigMonitor();
+
+  @Message(level = MessageLevel.INFO,
+           text = "Started ClouderaManager cluster configuration monitor (checking every {0} seconds)")
+  void startedClouderaManagerConfigMonitor(long pollingInterval);
+
+  @Message(level = MessageLevel.INFO, text = "Stopping ClouderaManager cluster configuration monitor")
+  void stoppingClouderaManagerConfigMonitor();
+
+  @Message(level = MessageLevel.INFO,
+      text = "Stopped ClouderaManager cluster configuration monitor")
+  void stoppedClouderaManagerConfigMonitor();
+
+  @Message(level = MessageLevel.DEBUG, text = "Checking {0} @ {1} for configuration changes...")
+  void checkingClusterConfiguration(String clusterName, String discoveryAddress);
+
+  @Message(level = MessageLevel.ERROR,
+      text = "Error getting service configuration details from ClouderaManager: {0}")
+  void clouderaManagerConfigurationAPIError(@StackTrace(level = MessageLevel.DEBUG) ApiException e);
+
+  @Message(level = MessageLevel.ERROR,
+      text = "Error querying restart events from ClouderaManager: {0}")
+  void clouderaManagerEventsAPIError(@StackTrace(level = MessageLevel.DEBUG) ApiException e);
+
+  @Message(level = MessageLevel.ERROR,
+           text = "Failed to persist data for cluster configuration monitor {0} {1}: {2}")
+  void failedToPersistClusterMonitorData(String monitor,
+                                         String filename,
+                                         @StackTrace(level = MessageLevel.DEBUG) Exception e);
+
+  @Message(level = MessageLevel.ERROR,
+           text = "Failed to load persisted service discovery configuration for cluster monitor {0} : {1}")
+  void failedToLoadClusterMonitorServiceDiscoveryConfig(String monitor,
+                                                        @StackTrace(level = MessageLevel.DEBUG) Exception e);
+
+  @Message(level = MessageLevel.ERROR,
+           text = "Failed to load persisted service configuration data for cluster monitor {0} : {1}")
+  void failedToLoadClusterMonitorServiceConfigurations(String monitor,
+                                                       @StackTrace(level = MessageLevel.DEBUG) Exception e);
+
+  @Message(level = MessageLevel.ERROR,
+      text = "Failed to remove persisted data for cluster configuration monitor {0} {1}")
+  void failedToRemovPersistedClusterMonitorData(String monitor, String filename);
+
+  @Message(level = MessageLevel.DEBUG, text = "Getting current configuration for {0} from {1} @ {2}")
+  void gettingCurrentClusterConfiguration(String serviceName,
+                                          String clusterName,
+                                          String discoveryAddress);
+
+  @Message(level = MessageLevel.DEBUG, text = "Querying restart events from {0} @ {1} since {2}")
+  void queryingRestartEventsFromCluster(String clusterName,
+                                        String discoveryAddress,
+                                        String sinceTimestamp);
+
+  @Message(level = MessageLevel.DEBUG, text = "Analyzing current {0} configuration for changes...")
+  void analyzingCurrentServiceConfiguration(String serviceName);
+
+  @Message(level = MessageLevel.ERROR, text = "Error analyzing current {0} configuration for changes: {1}")
+  void errorAnalyzingCurrentServiceConfiguration(String serviceName,
+                                                @StackTrace(level = MessageLevel.DEBUG) Exception e);
+
+  @Message(level = MessageLevel.INFO, text = "Service property {0} value has changed from {1} to {2}")
+  void serviceConfigurationPropertyHasChanged(String propertyName,
+                                              String previousValue,
+                                              String currentValue);
+
+  @Message(level = MessageLevel.INFO, text = "Role property {0} value has changed from {1} to {2}")
+  void roleConfigurationPropertyHasChanged(String propertyName,
+                                           String previousValue,
+                                           String currentValue);
+
+  @Message(level = MessageLevel.INFO,
+           text = "The {0} service configuration has changed, such that it has been enabled for proxying.")
+  void serviceEnabled(String serviceName);
+
+  @Message(level = MessageLevel.INFO, text = "Role type {0} has been removed.")
+  void roleTypeRemoved(String roleType);
+
+  @Message(level = MessageLevel.WARN, text = "Failed to create persistence directory {0}")
+  void failedToCreatePersistenceDirectory(String path);
+
 }
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/DiscoveryApiClient.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/DiscoveryApiClient.java
index a29dd39..793292f 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/DiscoveryApiClient.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/DiscoveryApiClient.java
@@ -46,15 +46,18 @@ public class DiscoveryApiClient extends ApiClient {
 
   private boolean isKerberos;
 
-  DiscoveryApiClient(ServiceDiscoveryConfig discoveryConfig, AliasService aliasService) {
-    configure(discoveryConfig, aliasService);
+  private ServiceDiscoveryConfig config;
+
+  public DiscoveryApiClient(ServiceDiscoveryConfig discoveryConfig, AliasService aliasService) {
+    this.config = discoveryConfig;
+    configure(aliasService);
   }
 
   boolean isKerberos() {
     return isKerberos;
   }
 
-  private void configure(ServiceDiscoveryConfig config, AliasService aliasService) {
+  private void configure(AliasService aliasService) {
     String apiAddress = config.getAddress();
     apiAddress += (apiAddress.endsWith("/") ? API_PATH : "/" + API_PATH);
 
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/ServiceModel.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/ServiceModel.java
index a1bc537..96b0c31 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/ServiceModel.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/ServiceModel.java
@@ -16,18 +16,33 @@
  */
 package org.apache.knox.gateway.topology.discovery.cm;
 
+import java.util.HashMap;
+import java.util.Map;
 import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
 
+/**
+ * Model of a deployed service configuration and metadata for the configuration properties used to create the model
+ * object.
+ */
 public class ServiceModel {
 
   public enum Type {API, UI}
 
+  private static final String NULL_VALUE = "null";
+
   private final Type type;
   private final String service;
   private final String serviceType;
   private final String roleType;
   private final String serviceUrl;
 
+  // The service configuration properties used to created the model
+  private final Map<String, String> serviceConfigProperties = new ConcurrentHashMap<>();
+
+  // The role configuration properties used to created the model
+  private final Map<String, Map<String, String>> roleConfigProperties = new ConcurrentHashMap<>();
+
   /**
    * @param type        The model type
    * @param service     The service name
@@ -47,6 +62,28 @@ public class ServiceModel {
     this.serviceUrl  = serviceUrl;
   }
 
+  public void addServiceProperty(final String name, final String value) {
+    serviceConfigProperties.put(name, (value != null ? value : NULL_VALUE));
+  }
+
+  public void addRoleProperty(final String role, final String name, final String value) {
+    roleConfigProperties.computeIfAbsent(role, m -> new HashMap<>()).put(name, (value != null ? value : NULL_VALUE));
+  }
+
+  /**
+   * @return The service configuration properties employed by the model.
+   */
+  public Map<String, String> getServiceProperties() {
+    return serviceConfigProperties;
+  }
+
+  /**
+   * @return The role configuration properties employed by the model.
+   */
+  public Map<String, Map<String, String>> getRoleProperties() {
+    return roleConfigProperties;
+  }
+
   /**
    * @return The model type
    */
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/atlas/AtlasServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/atlas/AtlasServiceModelGenerator.java
index a33863e..60a327d 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/atlas/AtlasServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/atlas/AtlasServiceModelGenerator.java
@@ -31,6 +31,10 @@ public class AtlasServiceModelGenerator extends AbstractServiceModelGenerator {
   public static final String SERVICE_TYPE = "ATLAS";
   public static final String ROLE_TYPE = "ATLAS_SERVER";
 
+  static final String SSL_ENABLED = "ssl_enabled";
+  static final String HTTP_PORT   = "atlas_server_http_port";
+  static final String HTTPS_PORT  = "atlas_server_https_port";
+
   @Override
   public String getService() {
     return SERVICE;
@@ -59,19 +63,27 @@ public class AtlasServiceModelGenerator extends AbstractServiceModelGenerator {
     String hostname = role.getHostRef().getHostname();
     String scheme;
     String port;
-    boolean sslEnabled = Boolean.parseBoolean(getRoleConfigValue(roleConfig, "ssl_enabled"));
+    boolean sslEnabled = Boolean.parseBoolean(getRoleConfigValue(roleConfig, SSL_ENABLED));
     if(sslEnabled) {
       scheme = "https";
-      port = getRoleConfigValue(roleConfig, "atlas_server_https_port");
+      port = getRoleConfigValue(roleConfig, HTTPS_PORT);
     } else {
       scheme = "http";
-      port = getRoleConfigValue(roleConfig, "atlas_server_http_port");
+      port = getRoleConfigValue(roleConfig, HTTP_PORT);
     }
-    return new ServiceModel(getModelType(),
+
+    ServiceModel model =
+           new ServiceModel(getModelType(),
                             getService(),
                             getServiceType(),
                             getRoleType(),
                             String.format(Locale.getDefault(), "%s://%s:%s", scheme, hostname, port));
+
+    model.addRoleProperty(getRoleType(), SSL_ENABLED, getRoleConfigValue(roleConfig, SSL_ENABLED));
+    model.addRoleProperty(getRoleType(), HTTPS_PORT, getRoleConfigValue(roleConfig, HTTPS_PORT));
+    model.addRoleProperty(getRoleType(), HTTP_PORT, getRoleConfigValue(roleConfig, HTTP_PORT));
+
+    return model;
   }
 
 }
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hbase/HBaseUIServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hbase/HBaseUIServiceModelGenerator.java
index 00c7854..16bafba 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hbase/HBaseUIServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hbase/HBaseUIServiceModelGenerator.java
@@ -31,6 +31,9 @@ public class HBaseUIServiceModelGenerator extends AbstractServiceModelGenerator
   public static final String SERVICE_TYPE = "HBASE";
   public static final String ROLE_TYPE    = "MASTER";
 
+  static final String SSL_ENABLED      = "hbase_hadoop_ssl_enabled";
+  static final String MASTER_INFO_PORT = "hbase_master_info_port";
+
   @Override
   public String getService() {
     return SERVICE;
@@ -58,14 +61,21 @@ public class HBaseUIServiceModelGenerator extends AbstractServiceModelGenerator
                                       ApiConfigList    roleConfig) {
     String hostname = role.getHostRef().getHostname();
     String scheme;
-    String port = getRoleConfigValue(roleConfig, "hbase_master_info_port"); // TODO: Is there an SSL port, or is this property re-used?
-    boolean sslEnabled = Boolean.parseBoolean(getServiceConfigValue(serviceConfig, "hbase_hadoop_ssl_enabled"));
+    String port = getRoleConfigValue(roleConfig, MASTER_INFO_PORT); // TODO: Is there an SSL port, or is this property re-used?
+    boolean sslEnabled = Boolean.parseBoolean(getServiceConfigValue(serviceConfig, SSL_ENABLED));
     if(sslEnabled) {
       scheme = "https";
     } else {
       scheme = "http";
     }
-    return createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s", scheme, hostname, port));
+
+    ServiceModel model =
+        createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s", scheme, hostname, port));
+
+    model.addServiceProperty(SSL_ENABLED, getServiceConfigValue(serviceConfig, SSL_ENABLED));
+    model.addRoleProperty(getRoleType(), MASTER_INFO_PORT, getRoleConfigValue(roleConfig, MASTER_INFO_PORT));
+
+    return model;
   }
 
 }
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hbase/WebHBaseServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hbase/WebHBaseServiceModelGenerator.java
index c76e9a1..c446332 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hbase/WebHBaseServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hbase/WebHBaseServiceModelGenerator.java
@@ -31,6 +31,9 @@ public class WebHBaseServiceModelGenerator extends AbstractServiceModelGenerator
   public static final String SERVICE_TYPE = "HBASE";
   public static final String ROLE_TYPE    = "HBASERESTSERVER";
 
+  static final String SSL_ENABLED      = "hbase_restserver_ssl_enable";
+  static final String REST_SERVER_PORT = "hbase_restserver_port";
+
   @Override
   public String getService() {
     return SERVICE;
@@ -58,14 +61,20 @@ public class WebHBaseServiceModelGenerator extends AbstractServiceModelGenerator
                                       ApiConfigList    roleConfig) {
     String hostname = role.getHostRef().getHostname();
     String scheme;
-    String port = getRoleConfigValue(roleConfig, "hbase_restserver_port");
-    boolean sslEnabled = Boolean.parseBoolean(getRoleConfigValue(roleConfig, "hbase_restserver_ssl_enable"));
+    String port = getRoleConfigValue(roleConfig, REST_SERVER_PORT);
+    boolean sslEnabled = Boolean.parseBoolean(getRoleConfigValue(roleConfig, SSL_ENABLED));
     if(sslEnabled) {
       scheme = "https";
     } else {
       scheme = "http";
     }
-    return createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s", scheme, hostname, port));
+
+    ServiceModel model =
+        createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s", scheme, hostname, port));
+    model.addRoleProperty(getRoleType(), REST_SERVER_PORT, getRoleConfigValue(roleConfig, REST_SERVER_PORT));
+    model.addRoleProperty(getRoleType(), SSL_ENABLED, getRoleConfigValue(roleConfig, SSL_ENABLED));
+
+    return model;
   }
 
 }
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hdfs/HdfsUIServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hdfs/HdfsUIServiceModelGenerator.java
index 19d74b0..fe894fd 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hdfs/HdfsUIServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hdfs/HdfsUIServiceModelGenerator.java
@@ -26,7 +26,11 @@ import org.apache.knox.gateway.topology.discovery.cm.ServiceModel;
 import java.util.Locale;
 
 public class HdfsUIServiceModelGenerator extends NameNodeServiceModelGenerator {
-  private static final String SERVICE = "HDFSUI";
+  public static final String SERVICE = "HDFSUI";
+
+  static final String SSL_ENABLED = "hdfs_hadoop_ssl_enabled";
+  static final String HTTP_PORT   = "dfs_http_port";
+  static final String HTTPS_PORT  = "dfs_https_port";
 
   @Override
   public String getService() {
@@ -46,16 +50,22 @@ public class HdfsUIServiceModelGenerator extends NameNodeServiceModelGenerator {
     String hostname = role.getHostRef().getHostname();
     String scheme;
     String port;
-    boolean sslEnabled = Boolean.parseBoolean(getServiceConfigValue(serviceConfig, "hdfs_hadoop_ssl_enabled"));
+    boolean sslEnabled = Boolean.parseBoolean(getServiceConfigValue(serviceConfig, SSL_ENABLED));
     if(sslEnabled) {
       scheme = "https";
-      port = getRoleConfigValue(roleConfig, "dfs_https_port");
+      port = getRoleConfigValue(roleConfig, HTTPS_PORT);
     } else {
       scheme = "http";
-      port = getRoleConfigValue(roleConfig, "dfs_http_port");
+      port = getRoleConfigValue(roleConfig, HTTP_PORT);
     }
     String namenodeUrl = String.format(Locale.getDefault(), "%s://%s:%s", scheme, hostname, port);
-    return createServiceModel(namenodeUrl);
+
+    ServiceModel model = createServiceModel(namenodeUrl);
+    model.addServiceProperty(SSL_ENABLED, getServiceConfigValue(serviceConfig, SSL_ENABLED));
+    model.addRoleProperty(role.getType(), HTTPS_PORT, getRoleConfigValue(roleConfig, HTTPS_PORT));
+    model.addRoleProperty(role.getType(), HTTP_PORT, getRoleConfigValue(roleConfig, HTTP_PORT));
+
+    return model;
   }
 
 }
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hdfs/NameNodeServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hdfs/NameNodeServiceModelGenerator.java
index 1486901..f981344 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hdfs/NameNodeServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hdfs/NameNodeServiceModelGenerator.java
@@ -31,6 +31,10 @@ public class NameNodeServiceModelGenerator extends AbstractServiceModelGenerator
   public static final String SERVICE_TYPE = "HDFS";
   public static final String ROLE_TYPE    = "NAMENODE";
 
+  static final String AUTOFAILOVER_ENABLED = "autofailover_enabled";
+  static final String NN_NAMESERVICE       = "dfs_federation_namenode_nameservice";
+  static final String NN_PORT              = "namenode_port";
+
   @Override
   public String getServiceType() {
     return SERVICE_TYPE;
@@ -56,17 +60,25 @@ public class NameNodeServiceModelGenerator extends AbstractServiceModelGenerator
                                       ApiServiceConfig serviceConfig,
                                       ApiRole          role,
                                       ApiConfigList    roleConfig) throws ApiException {
-    boolean haEnabled = Boolean.parseBoolean(getRoleConfigValue(roleConfig, "autofailover_enabled"));
+    boolean haEnabled = Boolean.parseBoolean(getRoleConfigValue(roleConfig, AUTOFAILOVER_ENABLED));
     String serviceUrl;
     if(haEnabled) {
-      String nameservice = getRoleConfigValue(roleConfig, "dfs_federation_namenode_nameservice");
+      String nameservice = getRoleConfigValue(roleConfig, NN_NAMESERVICE);
       serviceUrl = String.format(Locale.getDefault(), "hdfs://%s", nameservice);
     } else {
       String hostname = role.getHostRef().getHostname();
-      String port = getRoleConfigValue(roleConfig, "namenode_port");
+      String port = getRoleConfigValue(roleConfig, NN_PORT);
       serviceUrl = String.format(Locale.getDefault(), "hdfs://%s:%s", hostname, port);
     }
-    return createServiceModel(serviceUrl);
+
+    ServiceModel model =  createServiceModel(serviceUrl);
+    model.addRoleProperty(getRoleType(), AUTOFAILOVER_ENABLED, getRoleConfigValue(roleConfig, AUTOFAILOVER_ENABLED));
+    model.addRoleProperty(getRoleType(), NN_PORT, getRoleConfigValue(roleConfig, NN_PORT));
+    if (haEnabled) {
+      model.addRoleProperty(getRoleType(), NN_NAMESERVICE, getRoleConfigValue(roleConfig, NN_NAMESERVICE));
+    }
+
+    return model;
   }
 
 }
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hdfs/WebHdfsServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hdfs/WebHdfsServiceModelGenerator.java
index 62909ec..f36e036 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hdfs/WebHdfsServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hdfs/WebHdfsServiceModelGenerator.java
@@ -23,10 +23,14 @@ import com.cloudera.api.swagger.model.ApiService;
 import com.cloudera.api.swagger.model.ApiServiceConfig;
 import org.apache.knox.gateway.topology.discovery.cm.ServiceModel;
 
+import java.util.Map;
+
 public class WebHdfsServiceModelGenerator extends HdfsUIServiceModelGenerator {
   private static final String SERVICE = "WEBHDFS";
   private static final String WEBHDFS_SUFFIX = "/webhdfs";
 
+  static final String WEBHDFS_ENABLED = "dfs_webhdfs_enabled";
+
   @Override
   public String getService() {
     return SERVICE;
@@ -43,7 +47,7 @@ public class WebHdfsServiceModelGenerator extends HdfsUIServiceModelGenerator {
                          ApiRole          role,
                          ApiConfigList    roleConfig) {
     return super.handles(service, serviceConfig, role, roleConfig) &&
-           Boolean.parseBoolean(getServiceConfigValue(serviceConfig, "dfs_webhdfs_enabled"));
+           Boolean.parseBoolean(getServiceConfigValue(serviceConfig, WEBHDFS_ENABLED));
   }
 
   @Override
@@ -51,9 +55,30 @@ public class WebHdfsServiceModelGenerator extends HdfsUIServiceModelGenerator {
                                       ApiServiceConfig serviceConfig,
                                       ApiRole          role,
                                       ApiConfigList    roleConfig) throws ApiException {
-    String serviceUrl =
-        super.generateService(service, serviceConfig, role, roleConfig).getServiceUrl() + WEBHDFS_SUFFIX;
-    return createServiceModel(serviceUrl);
+    ServiceModel parent = super.generateService(service, serviceConfig, role, roleConfig);
+    String serviceUrl = parent.getServiceUrl() + WEBHDFS_SUFFIX;
+
+    ServiceModel model = createServiceModel(serviceUrl);
+    model.addServiceProperty(WEBHDFS_ENABLED, getServiceConfigValue(serviceConfig, WEBHDFS_ENABLED));
+
+    // Add parent model metadata
+    addParentModelMetadata(model, parent);
+
+    return model;
+  }
+
+  private void addParentModelMetadata(final ServiceModel model, final ServiceModel parent) {
+    // Add parent model properties
+    for (Map.Entry<String, String> parentProp : parent.getServiceProperties().entrySet()) {
+      model.addServiceProperty(parentProp.getKey(), parentProp.getValue());
+    }
+
+    // Add parent role properties
+    for (Map.Entry<String, Map<String, String>> parentProps : parent.getRoleProperties().entrySet()) {
+      for (Map.Entry<String, String> prop : parentProps.getValue().entrySet()) {
+        model.addRoleProperty(parentProps.getKey(), prop.getKey(), prop.getValue());
+      }
+    }
   }
 
 }
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveOnTezServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveOnTezServiceModelGenerator.java
index 276daec..8db7639 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveOnTezServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveOnTezServiceModelGenerator.java
@@ -16,12 +16,20 @@
  */
 package org.apache.knox.gateway.topology.discovery.cm.model.hive;
 
+import com.cloudera.api.swagger.client.ApiException;
 import com.cloudera.api.swagger.model.ApiConfigList;
+import com.cloudera.api.swagger.model.ApiRole;
+import com.cloudera.api.swagger.model.ApiService;
+import com.cloudera.api.swagger.model.ApiServiceConfig;
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModel;
 
 public class HiveOnTezServiceModelGenerator extends HiveServiceModelGenerator {
 
   public static final String SERVICE_TYPE = "HIVE_ON_TEZ";
 
+  static final String HIVEONTEZ_TRANSPORT_MODE = TRANSPORT_MODE.replaceAll("\\.", "_");
+  static final String HIVEONTEZ_HTTP_PORT      = HTTP_PORT.replaceAll("\\.", "_");
+
   @Override
   public String getServiceType() {
     return SERVICE_TYPE;
@@ -29,7 +37,28 @@ public class HiveOnTezServiceModelGenerator extends HiveServiceModelGenerator {
 
   @Override
   protected boolean checkHiveServer2HTTPMode(ApiConfigList roleConfig) {
-    return TRANSPORT_MODE_HTTP.equals(getRoleConfigValue(roleConfig, "hive_server2_transport_mode"));
+    return TRANSPORT_MODE_HTTP.equals(getRoleConfigValue(roleConfig, HIVEONTEZ_TRANSPORT_MODE));
+  }
+
+  @Override
+  protected String getHttpPort(ApiConfigList roleConfig) {
+    return getRoleConfigValue(roleConfig, HIVEONTEZ_HTTP_PORT);
+  }
+
+  @Override
+  public ServiceModel generateService(ApiService       service,
+                                      ApiServiceConfig serviceConfig,
+                                      ApiRole          role,
+                                      ApiConfigList    roleConfig) throws ApiException {
+    ServiceModel model = super.generateService(service, serviceConfig, role, roleConfig);
+    model.addRoleProperty(getRoleType(),
+                          HIVEONTEZ_HTTP_PORT,
+                          getRoleConfigValue(roleConfig, HIVEONTEZ_HTTP_PORT));
+    model.addRoleProperty(getRoleType(),
+                          HIVEONTEZ_TRANSPORT_MODE,
+                          getRoleConfigValue(roleConfig, HIVEONTEZ_TRANSPORT_MODE));
+
+    return model;
   }
 
 }
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveServiceModelGenerator.java
index f37b0ce..33f5135 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveServiceModelGenerator.java
@@ -32,9 +32,13 @@ public class HiveServiceModelGenerator extends AbstractServiceModelGenerator {
   public static final String SERVICE_TYPE = "HIVE";
   public static final String ROLE_TYPE    = "HIVESERVER2";
 
-  protected static final String TRANSPORT_MODE_HTTP = "http";
-
+  static final String TRANSPORT_MODE_HTTP = "http";
 
+  static final String SAFETY_VALVE   = "hive_hs2_config_safety_valve";
+  static final String SSL_ENABLED    = "hive.server2.use.SSL";
+  static final String TRANSPORT_MODE = "hive.server2.transport.mode";
+  static final String HTTP_PORT      = "hive.server2.thrift.http.port";
+  static final String HTTP_PATH      = "hive.server2.thrift.http.path";
 
   @Override
   public String getService() {
@@ -58,9 +62,7 @@ public class HiveServiceModelGenerator extends AbstractServiceModelGenerator {
 
   @Override
   public boolean handles(ApiService service, ApiServiceConfig serviceConfig, ApiRole role, ApiConfigList roleConfig) {
-    return getServiceType().equals(service.getType()) &&
-           getRoleType().equals(role.getType()) &&
-           checkHiveServer2HTTPMode(roleConfig);
+    return super.handles(service, serviceConfig, role, roleConfig) && checkHiveServer2HTTPMode(roleConfig);
   }
 
   @Override
@@ -69,21 +71,39 @@ public class HiveServiceModelGenerator extends AbstractServiceModelGenerator {
                                       ApiRole          role,
                                       ApiConfigList    roleConfig) throws ApiException {
     String hostname = role.getHostRef().getHostname();
-    String hs2SafetyValve = getRoleConfigValue(roleConfig, "hive_hs2_config_safety_valve");
-    String port = getSafetyValveValue(hs2SafetyValve, "hive.server2.thrift.http.port");
-    String httpPath = getSafetyValveValue(hs2SafetyValve, "hive.server2.thrift.http.path");
-    boolean sslEnabled = Boolean.parseBoolean(getRoleConfigValue(roleConfig, "hive.server2.use.SSL"));
+    boolean sslEnabled = Boolean.parseBoolean(getRoleConfigValue(roleConfig, SSL_ENABLED));
     String scheme = sslEnabled ? "https" : "http";
-    return createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s/%s", scheme, hostname, port, httpPath));
+
+    String port     = getHttpPort(roleConfig);
+    String httpPath = getHttpPath(roleConfig);
+
+    ServiceModel model =
+        createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s/%s", scheme, hostname, port, httpPath));
+    model.addRoleProperty(getRoleType(), SSL_ENABLED, getRoleConfigValue(roleConfig, SSL_ENABLED));
+    model.addRoleProperty(getRoleType(), SAFETY_VALVE, getRoleConfigValue(roleConfig, SAFETY_VALVE));
+
+    return model;
   }
 
-  protected boolean checkHiveServer2HTTPMode(ApiConfigList roleConfig) {
-    boolean isHttp = false;
-    String hiveServer2SafetyValve = getRoleConfigValue(roleConfig, "hive_hs2_config_safety_valve");
-    if(hiveServer2SafetyValve != null) {
-      isHttp = TRANSPORT_MODE_HTTP.equals(getSafetyValveValue(hiveServer2SafetyValve, "hive.server2.transport.mode"));
+  private String getHS2SafetyValveValue(final ApiConfigList roleConfig, final String name) {
+    String value = null;
+    String hs2SafetyValve = getRoleConfigValue(roleConfig, SAFETY_VALVE);
+    if (hs2SafetyValve != null && !hs2SafetyValve.isEmpty()) {
+      value = getSafetyValveValue(hs2SafetyValve, name);
     }
-    return isHttp;
+    return value;
+  }
+
+  protected String getHttpPort(ApiConfigList roleConfig) {
+    return getHS2SafetyValveValue(roleConfig, HTTP_PORT);
+  }
+
+  protected String getHttpPath(ApiConfigList roleConfig) {
+    return getHS2SafetyValveValue(roleConfig, HTTP_PATH);
+  }
+
+  protected boolean checkHiveServer2HTTPMode(ApiConfigList roleConfig) {
+    return TRANSPORT_MODE_HTTP.equals(getHS2SafetyValveValue(roleConfig, TRANSPORT_MODE));
   }
 
 }
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/WebHCatServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/WebHCatServiceModelGenerator.java
index fa3552d..2b512d0 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/WebHCatServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/WebHCatServiceModelGenerator.java
@@ -32,6 +32,8 @@ public class WebHCatServiceModelGenerator extends AbstractServiceModelGenerator
   public static final String SERVICE_TYPE = "HIVE";
   public static final String ROLE_TYPE    = "WEBHCAT";
 
+  private static final String WEBHCAT_PORT = "hive_webhcat_address_port";
+
   @Override
   public String getService() {
     return SERVICE;
@@ -58,7 +60,11 @@ public class WebHCatServiceModelGenerator extends AbstractServiceModelGenerator
                                       ApiRole          role,
                                       ApiConfigList    roleConfig) throws ApiException {
     String hostname = role.getHostRef().getHostname();
-    String port = getRoleConfigValue(roleConfig, "hive_webhcat_address_port");
-    return createServiceModel(String.format(Locale.getDefault(), "http://%s:%s/templeton", hostname, port));
+    String port = getRoleConfigValue(roleConfig, WEBHCAT_PORT);
+
+    ServiceModel model = createServiceModel(String.format(Locale.getDefault(), "http://%s:%s/templeton", hostname, port));
+    model.addRoleProperty(getRoleType(), WEBHCAT_PORT, getRoleConfigValue(roleConfig, WEBHCAT_PORT));
+
+    return model;
   }
 }
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hue/HueLBServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hue/HueLBServiceModelGenerator.java
index 4b50aac..7077308 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hue/HueLBServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hue/HueLBServiceModelGenerator.java
@@ -31,6 +31,8 @@ public class HueLBServiceModelGenerator extends AbstractServiceModelGenerator {
   public static final String SERVICE_TYPE = "HUE";
   public static final String ROLE_TYPE = "HUE_LOAD_BALANCER";
 
+  static final String LISTEN_PORT = "listen";
+
   @Override
   public String getService() {
     return SERVICE;
@@ -58,8 +60,12 @@ public class HueLBServiceModelGenerator extends AbstractServiceModelGenerator {
                                       ApiConfigList    roleConfig) {
     String hostname = role.getHostRef().getHostname();
     String scheme = "http";
-    String port = getRoleConfigValue(roleConfig, "listen");
-    return createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s", scheme, hostname, port));
+    String port = getRoleConfigValue(roleConfig, LISTEN_PORT);
+
+    ServiceModel model = createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s", scheme, hostname, port));
+    model.addRoleProperty(getRoleType(), LISTEN_PORT, port);
+
+    return model;
   }
 
 }
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hue/HueServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hue/HueServiceModelGenerator.java
index 04a7a52..8ce4578 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hue/HueServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hue/HueServiceModelGenerator.java
@@ -31,6 +31,9 @@ public class HueServiceModelGenerator extends AbstractServiceModelGenerator {
   public static final String SERVICE_TYPE = "HUE";
   public static final String ROLE_TYPE = "HUE_SERVER";
 
+  static final String HUE_HTTP_PORT = "hue_http_port";
+  static final String SSL_ENABLED   = "ssl_enable";
+
   @Override
   public String getService() {
     return SERVICE;
@@ -58,14 +61,19 @@ public class HueServiceModelGenerator extends AbstractServiceModelGenerator {
                                       ApiConfigList    roleConfig) {
     String hostname = role.getHostRef().getHostname();
     String scheme;
-    String port = getRoleConfigValue(roleConfig, "hue_http_port");
-    boolean sslEnabled = Boolean.parseBoolean(getRoleConfigValue(roleConfig, "ssl_enable"));
+    String port = getRoleConfigValue(roleConfig, HUE_HTTP_PORT);
+    boolean sslEnabled = Boolean.parseBoolean(getRoleConfigValue(roleConfig, SSL_ENABLED));
     if(sslEnabled) {
       scheme = "https";
     } else {
       scheme = "http";
     }
-    return createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s", scheme, hostname, port));
+
+    ServiceModel model = createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s", scheme, hostname, port));
+    model.addRoleProperty(getRoleType(), HUE_HTTP_PORT, port);
+    model.addRoleProperty(getRoleType(), SSL_ENABLED, getRoleConfigValue(roleConfig, SSL_ENABLED));
+
+    return model;
   }
 
 }
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/impala/ImpalaServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/impala/ImpalaServiceModelGenerator.java
index 1a8c920..39352f0 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/impala/ImpalaServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/impala/ImpalaServiceModelGenerator.java
@@ -32,6 +32,9 @@ public class ImpalaServiceModelGenerator extends AbstractServiceModelGenerator {
   public static final String SERVICE_TYPE = "IMPALA";
   public static final String ROLE_TYPE    = "IMPALAD";
 
+  static final String SSL_ENABLED = "client_services_ssl_enabled";
+  static final String HTTP_PORT   = "hs2_http_port";
+
   @Override
   public String getService() {
     return SERVICE;
@@ -53,14 +56,22 @@ public class ImpalaServiceModelGenerator extends AbstractServiceModelGenerator {
   }
 
   @Override
-  public ServiceModel generateService(ApiService service, ApiServiceConfig serviceConfig, ApiRole role, ApiConfigList roleConfig) throws ApiException {
+  public ServiceModel generateService(ApiService       service,
+                                      ApiServiceConfig serviceConfig,
+                                      ApiRole          role,
+                                      ApiConfigList    roleConfig) throws ApiException {
     String hostname = role.getHostRef().getHostname();
 
-    boolean sslEnabled = Boolean.parseBoolean(getServiceConfigValue(serviceConfig, "client_services_ssl_enabled"));
+    boolean sslEnabled = Boolean.parseBoolean(getServiceConfigValue(serviceConfig, SSL_ENABLED));
     String scheme = sslEnabled ? "https" : "http";
 
     // Role config properties
-    String port = getRoleConfigValue(roleConfig, "hs2_http_port");
-    return createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s/", scheme, hostname, port));
+    String port = getRoleConfigValue(roleConfig, HTTP_PORT);
+
+    ServiceModel model = createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s/", scheme, hostname, port));
+    model.addServiceProperty(SSL_ENABLED, getServiceConfigValue(serviceConfig, SSL_ENABLED));
+    model.addRoleProperty(getRoleType(), HTTP_PORT, port);
+
+    return model;
   }
 }
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/impala/ImpalaUIServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/impala/ImpalaUIServiceModelGenerator.java
index 17da335..15ac9b6 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/impala/ImpalaUIServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/impala/ImpalaUIServiceModelGenerator.java
@@ -32,6 +32,9 @@ public class ImpalaUIServiceModelGenerator extends AbstractServiceModelGenerator
   public static final String SERVICE_TYPE = "IMPALA";
   public static final String ROLE_TYPE    = "IMPALAD";
 
+  static final String ENABLE_WEBSERVER = "impalad_enable_webserver";
+  static final String SSL_ENABLED      = "client_services_ssl_enabled";
+  static final String WEBSERVER_PORT   = "impalad_webserver_port";
 
   @Override
   public String getService() {
@@ -56,7 +59,7 @@ public class ImpalaUIServiceModelGenerator extends AbstractServiceModelGenerator
   @Override
   public boolean handles(ApiService service, ApiServiceConfig serviceConfig, ApiRole role, ApiConfigList roleConfig) {
     return super.handles(service, serviceConfig, role, roleConfig) &&
-           Boolean.parseBoolean(getRoleConfigValue(roleConfig, "impalad_enable_webserver"));
+           Boolean.parseBoolean(getRoleConfigValue(roleConfig, ENABLE_WEBSERVER));
   }
 
   @Override
@@ -66,10 +69,16 @@ public class ImpalaUIServiceModelGenerator extends AbstractServiceModelGenerator
                                       ApiConfigList    roleConfig) throws ApiException {
     String hostname = role.getHostRef().getHostname();
 
-    boolean sslEnabled = Boolean.parseBoolean(getServiceConfigValue(serviceConfig, "client_services_ssl_enabled"));
-    String scheme = sslEnabled ? "https" : "http";
+    String sslEnabled = getServiceConfigValue(serviceConfig, SSL_ENABLED);
+    String scheme = Boolean.parseBoolean(sslEnabled) ? "https" : "http";
 
-    String port = getRoleConfigValue(roleConfig, "impalad_webserver_port");
-    return createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s/", scheme, hostname, port));
+    String port = getRoleConfigValue(roleConfig, WEBSERVER_PORT);
+
+    ServiceModel model = createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s/", scheme, hostname, port));
+    model.addServiceProperty(SSL_ENABLED, sslEnabled);
+    model.addRoleProperty(getRoleType(), WEBSERVER_PORT, port);
+    model.addRoleProperty(getRoleType(), ENABLE_WEBSERVER, getRoleConfigValue(roleConfig, ENABLE_WEBSERVER));
+
+    return model;
   }
 }
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/kudu/KuduUIServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/kudu/KuduUIServiceModelGenerator.java
index 017cacf..7108ce4 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/kudu/KuduUIServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/kudu/KuduUIServiceModelGenerator.java
@@ -32,6 +32,8 @@ public class KuduUIServiceModelGenerator extends AbstractServiceModelGenerator {
   public static final String SERVICE_TYPE = "KUDU";
   public static final String ROLE_TYPE    = "KUDU_MASTER";
 
+  static final String WEBSERVER_PORT = "webserver_port";
+  static final String SSL_ENABLED    = "ssl_enabled";
 
   @Override
   public String getService() {
@@ -54,13 +56,21 @@ public class KuduUIServiceModelGenerator extends AbstractServiceModelGenerator {
   }
 
   @Override
-  public ServiceModel generateService(ApiService service, ApiServiceConfig serviceConfig, ApiRole role, ApiConfigList roleConfig) throws ApiException {
+  public ServiceModel generateService(ApiService       service,
+                                      ApiServiceConfig serviceConfig,
+                                      ApiRole          role,
+                                      ApiConfigList    roleConfig) throws ApiException {
     String hostname = role.getHostRef().getHostname();
 
-    boolean sslEnabled = Boolean.parseBoolean(getRoleConfigValue(roleConfig, "ssl_enabled"));
-    String scheme = sslEnabled ? "https" : "http";
+    String sslEnabled = getRoleConfigValue(roleConfig, SSL_ENABLED);
+    String scheme = Boolean.parseBoolean(sslEnabled) ? "https" : "http";
 
-    String port = getRoleConfigValue(roleConfig, "webserver_port");
-    return createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s/", scheme, hostname, port));
+    String port = getRoleConfigValue(roleConfig, WEBSERVER_PORT);
+
+    ServiceModel model = createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s/", scheme, hostname, port));
+    model.addRoleProperty(getRoleType(), SSL_ENABLED, sslEnabled);
+    model.addRoleProperty(getRoleType(), WEBSERVER_PORT, port);
+
+    return model;
   }
 }
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/livy/LivyServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/livy/LivyServiceModelGenerator.java
index dfa4aad..7471f6b 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/livy/LivyServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/livy/LivyServiceModelGenerator.java
@@ -31,6 +31,9 @@ public class LivyServiceModelGenerator extends AbstractServiceModelGenerator {
   public static final String SERVICE_TYPE = "LIVY";
   public static final String ROLE_TYPE    = "LIVY_SERVER";
 
+  static final String SSL_ENABLED      = "ssl_enabled";
+  static final String LIVY_SERVER_PORT = "livy_server_port";
+
   @Override
   public String getService() {
     return SERVICE;
@@ -58,14 +61,19 @@ public class LivyServiceModelGenerator extends AbstractServiceModelGenerator {
                                       ApiConfigList    roleConfig) {
     String hostname = role.getHostRef().getHostname();
     String scheme;
-    String port = getRoleConfigValue(roleConfig, "livy_server_port");
-    boolean sslEnabled = Boolean.parseBoolean(getRoleConfigValue(roleConfig, "ssl_enabled"));
-    if(sslEnabled) {
+    String port = getRoleConfigValue(roleConfig, LIVY_SERVER_PORT);
+    String sslEnabled = getRoleConfigValue(roleConfig, SSL_ENABLED);
+    if(Boolean.parseBoolean(sslEnabled)) {
       scheme = "https";
     } else {
       scheme = "http";
     }
-    return createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s", scheme, hostname, port));
+
+    ServiceModel model = createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s", scheme, hostname, port));
+    model.addRoleProperty(getRoleType(), SSL_ENABLED, sslEnabled);
+    model.addRoleProperty(getRoleType(), LIVY_SERVER_PORT, port);
+
+    return model;
   }
 
 }
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/nifi/NifiRegistryServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/nifi/NifiRegistryServiceModelGenerator.java
index 29766a9..c2aea76 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/nifi/NifiRegistryServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/nifi/NifiRegistryServiceModelGenerator.java
@@ -33,6 +33,10 @@ public class NifiRegistryServiceModelGenerator extends AbstractServiceModelGener
   public static final String SERVICE_TYPE = "NIFIREGISTRY";
   public static final String ROLE_TYPE = "NIFI_REGISTRY_SERVER";
 
+  static final String SSL_ENABLED = "ssl_enabled";
+  static final String HTTP_PORT   = "nifi.registry.web.http.port";
+  static final String HTTPS_PORT  = "nifi.registry.web.https.port";
+
   /**
    * @return The name of the Knox service for which the implementation will
    * generate a model.
@@ -70,18 +74,24 @@ public class NifiRegistryServiceModelGenerator extends AbstractServiceModelGener
     String hostname = role.getHostRef().getHostname();
     String scheme;
     String port;
-    boolean sslEnabled = Boolean.parseBoolean(getRoleConfigValue(roleConfig, "ssl_enabled"));
+    boolean sslEnabled = Boolean.parseBoolean(getRoleConfigValue(roleConfig, SSL_ENABLED));
     if(sslEnabled) {
       scheme = "https";
-      port = getRoleConfigValue(roleConfig, "nifi.registry.web.https.port");
+      port = getRoleConfigValue(roleConfig, HTTPS_PORT);
     } else {
       scheme = "http";
-      port = getRoleConfigValue(roleConfig, "nifi.registry.web.http.port");
+      port = getRoleConfigValue(roleConfig, HTTP_PORT);
     }
-    return new ServiceModel(getModelType(),
-        getService(),
-        getServiceType(),
-        getRoleType(),
-        String.format(Locale.getDefault(), "%s://%s:%s", scheme, hostname, port));
+
+    ServiceModel model =new ServiceModel(getModelType(),
+                                         getService(),
+                                         getServiceType(),
+                                         getRoleType(),
+                                         String.format(Locale.getDefault(), "%s://%s:%s", scheme, hostname, port));
+    model.addRoleProperty(getRoleType(), SSL_ENABLED, getRoleConfigValue(roleConfig, SSL_ENABLED));
+    model.addRoleProperty(getRoleType(), HTTP_PORT, getRoleConfigValue(roleConfig, HTTP_PORT));
+    model.addRoleProperty(getRoleType(), HTTPS_PORT, getRoleConfigValue(roleConfig, HTTPS_PORT));
+
+    return model;
   }
 }
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/nifi/NifiServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/nifi/NifiServiceModelGenerator.java
index dcf26cf..4f74c2a 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/nifi/NifiServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/nifi/NifiServiceModelGenerator.java
@@ -33,6 +33,10 @@ public class NifiServiceModelGenerator extends AbstractServiceModelGenerator {
   public static final String SERVICE_TYPE = "NIFI";
   public static final String ROLE_TYPE = "NIFI_NODE";
 
+  static final String SSL_ENABLED = "ssl_enabled";
+  static final String HTTP_PORT   = "nifi.web.http.port";
+  static final String HTTPS_PORT  = "nifi.web.https.port";
+
   /**
    * @return The name of the Knox service for which the implementation will
    * generate a model.
@@ -70,18 +74,23 @@ public class NifiServiceModelGenerator extends AbstractServiceModelGenerator {
     String hostname = role.getHostRef().getHostname();
     String scheme;
     String port;
-    boolean sslEnabled = Boolean.parseBoolean(getRoleConfigValue(roleConfig, "ssl_enabled"));
+    boolean sslEnabled = Boolean.parseBoolean(getRoleConfigValue(roleConfig, SSL_ENABLED));
     if(sslEnabled) {
       scheme = "https";
-      port = getRoleConfigValue(roleConfig, "nifi.web.https.port");
+      port = getRoleConfigValue(roleConfig, HTTPS_PORT);
     } else {
       scheme = "http";
-      port = getRoleConfigValue(roleConfig, "nifi.web.http.port");
+      port = getRoleConfigValue(roleConfig, HTTP_PORT);
     }
-    return new ServiceModel(getModelType(),
-        getService(),
-        getServiceType(),
-        getRoleType(),
-        String.format(Locale.getDefault(), "%s://%s:%s", scheme, hostname, port));
+    ServiceModel model = new ServiceModel(getModelType(),
+                                          getService(),
+                                          getServiceType(),
+                                          getRoleType(),
+                                          String.format(Locale.getDefault(), "%s://%s:%s", scheme, hostname, port));
+    model.addRoleProperty(getRoleType(), SSL_ENABLED, getRoleConfigValue(roleConfig, SSL_ENABLED));
+    model.addRoleProperty(getRoleType(), HTTP_PORT, getRoleConfigValue(roleConfig, HTTP_PORT));
+    model.addRoleProperty(getRoleType(), HTTPS_PORT, getRoleConfigValue(roleConfig, HTTPS_PORT));
+
+    return model;
   }
 }
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/oozie/OozieServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/oozie/OozieServiceModelGenerator.java
index a6adf0f..ab0aaea 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/oozie/OozieServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/oozie/OozieServiceModelGenerator.java
@@ -31,6 +31,10 @@ public class OozieServiceModelGenerator extends AbstractServiceModelGenerator {
   public static final String SERVICE_TYPE = "OOZIE";
   public static final String ROLE_TYPE    = "OOZIE_SERVER";
 
+  static final String USE_SSL    = "oozie_use_ssl";
+  static final String HTTP_PORT  = "oozie_http_port";
+  static final String HTTPS_PORT = "oozie_https_port";
+
   @Override
   public String getService() {
     return SERVICE;
@@ -59,15 +63,22 @@ public class OozieServiceModelGenerator extends AbstractServiceModelGenerator {
     String hostname = role.getHostRef().getHostname();
     String scheme;
     String port;
-    boolean sslEnabled = Boolean.parseBoolean(getServiceConfigValue(serviceConfig, "oozie_use_ssl"));
-    if(sslEnabled) {
+    String sslEnabled = getServiceConfigValue(serviceConfig, USE_SSL);
+    if(Boolean.parseBoolean(sslEnabled)) {
       scheme = "https";
-      port = getRoleConfigValue(roleConfig, "oozie_https_port");
+      port = getRoleConfigValue(roleConfig, HTTPS_PORT);
     } else {
       scheme = "http";
-      port = getRoleConfigValue(roleConfig, "oozie_http_port");
+      port = getRoleConfigValue(roleConfig, HTTP_PORT);
     }
-    return createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s/oozie/", scheme, hostname, port));
+
+    ServiceModel model =
+        createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s/oozie/", scheme, hostname, port));
+    model.addServiceProperty(USE_SSL, sslEnabled);
+    model.addRoleProperty(getRoleType(), HTTP_PORT, getRoleConfigValue(roleConfig, HTTP_PORT));
+    model.addRoleProperty(getRoleType(), HTTPS_PORT, getRoleConfigValue(roleConfig, HTTPS_PORT));
+
+    return model;
   }
 
 }
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/phoenix/PhoenixServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/phoenix/PhoenixServiceModelGenerator.java
index fcdbb21..e6e157d 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/phoenix/PhoenixServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/phoenix/PhoenixServiceModelGenerator.java
@@ -31,6 +31,8 @@ public class PhoenixServiceModelGenerator extends AbstractServiceModelGenerator
   public static final String SERVICE_TYPE = "PHOENIX";
   public static final String ROLE_TYPE    = "PHOENIX_QUERY_SERVER";
 
+  static final String QUERY_SERVER_PORT = "phoenix_query_server_port";
+
   @Override
   public String getService() {
     return SERVICE;
@@ -59,8 +61,12 @@ public class PhoenixServiceModelGenerator extends AbstractServiceModelGenerator
     String hostname = role.getHostRef().getHostname();
     // Phoenix Query Server does not support https
     String scheme = "http";
-    String port = getRoleConfigValue(roleConfig, "phoenix_query_server_port");
-    return createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s", scheme, hostname, port));
+    String port = getRoleConfigValue(roleConfig, QUERY_SERVER_PORT);
+
+    ServiceModel model = createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s", scheme, hostname, port));
+    model.addRoleProperty(getRoleType(), QUERY_SERVER_PORT, port);
+
+    return model;
   }
 
 }
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/ranger/RangerServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/ranger/RangerServiceModelGenerator.java
index 236ef29..37b5344 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/ranger/RangerServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/ranger/RangerServiceModelGenerator.java
@@ -30,6 +30,10 @@ public class RangerServiceModelGenerator extends AbstractServiceModelGenerator {
   public static final String SERVICE_TYPE = "RANGER";
   public static final String ROLE_TYPE    = "RANGER_ADMIN";
 
+  static final String SSL_ENABLED = "ssl_enabled";
+  static final String HTTP_PORT   = "ranger_service_http_port";
+  static final String HTTPS_PORT  = "ranger_service_https_port";
+
   @Override
   public String getService() {
     return SERVICE;
@@ -58,15 +62,21 @@ public class RangerServiceModelGenerator extends AbstractServiceModelGenerator {
     String hostname = role.getHostRef().getHostname();
     String scheme;
     String port;
-    boolean sslEnabled = Boolean.parseBoolean(getRoleConfigValue(roleConfig, "ssl_enabled"));
-    if(sslEnabled) {
+    String sslEnabled = getRoleConfigValue(roleConfig, SSL_ENABLED);
+    if(Boolean.parseBoolean(sslEnabled)) {
       scheme = "https";
-      port = getServiceConfigValue(serviceConfig, "ranger_service_https_port");
+      port = getServiceConfigValue(serviceConfig, HTTPS_PORT);
     } else {
       scheme = "http";
-      port = getServiceConfigValue(serviceConfig, "ranger_service_http_port");
+      port = getServiceConfigValue(serviceConfig, HTTP_PORT);
     }
-    return createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s", scheme, hostname, port));
+
+    ServiceModel model = createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s", scheme, hostname, port));
+    model.addServiceProperty(HTTP_PORT, getServiceConfigValue(serviceConfig, HTTP_PORT));
+    model.addServiceProperty(HTTPS_PORT, getServiceConfigValue(serviceConfig, HTTPS_PORT));
+    model.addRoleProperty(getRoleType(), SSL_ENABLED, sslEnabled);
+
+    return model;
   }
 
 }
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/solr/SolrServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/solr/SolrServiceModelGenerator.java
index cc6ae41..75ae53b 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/solr/SolrServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/solr/SolrServiceModelGenerator.java
@@ -30,6 +30,10 @@ public class SolrServiceModelGenerator extends AbstractServiceModelGenerator {
   public static final String SERVICE_TYPE = "SOLR";
   public static final String ROLE_TYPE    = "SOLR_SERVER";
 
+  static final String USE_SSL    = "solr_use_ssl";
+  static final String HTTP_PORT  = "solr_http_port";
+  static final String HTTPS_PORT = "solr_https_port";
+
   @Override
   public String getService() {
     return SERVICE;
@@ -58,15 +62,22 @@ public class SolrServiceModelGenerator extends AbstractServiceModelGenerator {
     String hostname = role.getHostRef().getHostname();
     String scheme;
     String port;
-    boolean sslEnabled = Boolean.parseBoolean(getServiceConfigValue(serviceConfig, "solr_use_ssl"));
-    if(sslEnabled) {
+    String sslEnabled = getServiceConfigValue(serviceConfig, USE_SSL);
+    if(Boolean.parseBoolean(sslEnabled)) {
       scheme = "https";
-      port = getRoleConfigValue(roleConfig, "solr_https_port");
+      port = getRoleConfigValue(roleConfig, HTTPS_PORT);
     } else {
       scheme = "http";
-      port = getRoleConfigValue(roleConfig, "solr_http_port");
+      port = getRoleConfigValue(roleConfig, HTTP_PORT);
     }
-    return createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s/solr/", scheme, hostname, port));
+
+    ServiceModel model =
+        createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s/solr/", scheme, hostname, port));
+    model.addServiceProperty(USE_SSL, sslEnabled);
+    model.addRoleProperty(getRoleType(), HTTP_PORT, getRoleConfigValue(roleConfig, HTTP_PORT));
+    model.addRoleProperty(getRoleType(), HTTPS_PORT, getRoleConfigValue(roleConfig, HTTPS_PORT));
+
+    return model;
   }
 
 }
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/spark/SparkHistoryUIServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/spark/SparkHistoryUIServiceModelGenerator.java
index 75c466f..ad98271 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/spark/SparkHistoryUIServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/spark/SparkHistoryUIServiceModelGenerator.java
@@ -30,6 +30,10 @@ public class SparkHistoryUIServiceModelGenerator extends AbstractServiceModelGen
   public static final String SERVICE_TYPE = "SPARK_ON_YARN";
   public static final String ROLE_TYPE    = "SPARK_YARN_HISTORY_SERVER";
 
+  static final String SSL_ENABLED         = "ssl_enabled";
+  static final String SSL_SERVER_PORT     = "ssl_server_port";
+  static final String HISTORY_SERVER_PORT = "history_server_web_port";
+
   @Override
   public String getService() {
     return SERVICE;
@@ -58,15 +62,21 @@ public class SparkHistoryUIServiceModelGenerator extends AbstractServiceModelGen
     String hostname = role.getHostRef().getHostname();
     String scheme;
     String port;
-    boolean sslEnabled = Boolean.parseBoolean(getRoleConfigValue(roleConfig, "ssl_enabled"));
-    if(sslEnabled) {
+    String sslEnabled = getRoleConfigValue(roleConfig, SSL_ENABLED);
+    if(Boolean.parseBoolean(sslEnabled)) {
       scheme = "https";
-      port = getRoleConfigValue(roleConfig, "ssl_server_port");
+      port = getRoleConfigValue(roleConfig, SSL_SERVER_PORT);
     } else {
       scheme = "http";
-      port = getRoleConfigValue(roleConfig, "history_server_web_port");
+      port = getRoleConfigValue(roleConfig, HISTORY_SERVER_PORT);
     }
-    return createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s", scheme, hostname, port));
+
+    ServiceModel model = createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s", scheme, hostname, port));
+    model.addRoleProperty(getRoleType(), SSL_ENABLED, sslEnabled);
+    model.addRoleProperty(getRoleType(), SSL_SERVER_PORT, getRoleConfigValue(roleConfig, SSL_SERVER_PORT));
+    model.addRoleProperty(getRoleType(), HISTORY_SERVER_PORT, getRoleConfigValue(roleConfig, HISTORY_SERVER_PORT));
+
+    return model;
   }
 
 }
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/yarn/JobHistoryUIServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/yarn/JobHistoryUIServiceModelGenerator.java
index 9b7e637..110c8d7 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/yarn/JobHistoryUIServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/yarn/JobHistoryUIServiceModelGenerator.java
@@ -32,6 +32,9 @@ public class JobHistoryUIServiceModelGenerator extends AbstractServiceModelGener
   private static final String SERVICE_TYPE = "YARN";
   private static final String ROLE_TYPE = "JOBHISTORY";
 
+  private static final String HTTPS_PORT = "mapreduce_jobhistory_webapp_https_address";
+  private static final String HTTP_PORT  = "mapreduce_jobhistory_webapp_address";
+
   @Override
   public String getService() {
     return SERVICE;
@@ -63,12 +66,17 @@ public class JobHistoryUIServiceModelGenerator extends AbstractServiceModelGener
 
     if(isSSLEnabled(service, serviceConfig)) {
       scheme = "https";
-      port = getRoleConfigValue(roleConfig, "mapreduce_jobhistory_webapp_https_address");
+      port = getRoleConfigValue(roleConfig, HTTPS_PORT);
     } else {
       scheme = "http";
-      port = getRoleConfigValue(roleConfig, "mapreduce_jobhistory_webapp_address");
+      port = getRoleConfigValue(roleConfig, HTTP_PORT);
     }
-    return createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s", scheme, hostname, port));
+
+    ServiceModel model = createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s", scheme, hostname, port));
+    model.addRoleProperty(getRoleType(), HTTP_PORT, getRoleConfigValue(roleConfig, HTTP_PORT));
+    model.addRoleProperty(getRoleType(), HTTPS_PORT, getRoleConfigValue(roleConfig, HTTPS_PORT));
+
+    return model;
   }
 
   private boolean isSSLEnabled(ApiService service, ApiServiceConfig serviceConfig)
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/yarn/JobTrackerServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/yarn/JobTrackerServiceModelGenerator.java
index 4d2f337..548124a 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/yarn/JobTrackerServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/yarn/JobTrackerServiceModelGenerator.java
@@ -29,6 +29,8 @@ public class JobTrackerServiceModelGenerator extends ResourceManagerServiceModel
 
   private static final String SERVICE = "JOBTRACKER";
 
+  private static final String RM_PORT = "yarn_resourcemanager_address";
+
   @Override
   public String getService() {
     return SERVICE;
@@ -46,8 +48,14 @@ public class JobTrackerServiceModelGenerator extends ResourceManagerServiceModel
                                       ApiConfigList    roleConfig) throws ApiException {
 
     String hostname = role.getHostRef().getHostname();
-    String port = getRoleConfigValue(roleConfig, "yarn_resourcemanager_address");
+    String port = getRoleConfigValue(roleConfig, RM_PORT);
+
+    ServiceModel model = createServiceModel(String.format(Locale.getDefault(), "rpc://%s:%s", hostname, port));
+    model.addRoleProperty(getRoleType(), RM_PORT, port);
+
+    // N.B. It is not necessary to register the hdfs_hadoop_ssl_enabled configuration property for monitoring here
+    //      because that property is already registered for the HDFS ServiceModelGenerator types.
 
-    return createServiceModel(String.format(Locale.getDefault(), "rpc://%s:%s", hostname, port));
+    return model;
   }
 }
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/yarn/YarnUIServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/yarn/YarnUIServiceModelGenerator.java
index ec562c8..af8f564 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/yarn/YarnUIServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/yarn/YarnUIServiceModelGenerator.java
@@ -29,6 +29,9 @@ import java.util.Locale;
 public class YarnUIServiceModelGenerator extends ResourceManagerServiceModelGeneratorBase {
   private static final String SERVICE = "YARNUI";
 
+  static final String RM_HTTPS_PORT = "resourcemanager_webserver_https_port";
+  static final String RM_HTTP_PORT  = "resourcemanager_webserver_port";
+
   @Override
   public String getService() {
     return SERVICE;
@@ -44,7 +47,14 @@ public class YarnUIServiceModelGenerator extends ResourceManagerServiceModelGene
                                       ApiServiceConfig serviceConfig,
                                       ApiRole          role,
                                       ApiConfigList    roleConfig) throws ApiException {
-    return createServiceModel(generateURL(service, serviceConfig, role, roleConfig));
+    ServiceModel model = createServiceModel(generateURL(service, serviceConfig, role, roleConfig));
+    model.addRoleProperty(getRoleType(), RM_HTTP_PORT, getRoleConfigValue(roleConfig, RM_HTTP_PORT));
+    model.addRoleProperty(getRoleType(), RM_HTTPS_PORT, getRoleConfigValue(roleConfig, RM_HTTPS_PORT));
+
+    // N.B. It is not necessary to register the hdfs_hadoop_ssl_enabled configuration property for monitoring here
+    //      because that property is already registered for the HDFS ServiceModelGenerator types.
+
+    return model;
   }
 
   protected String generateURL(ApiService       service,
@@ -58,10 +68,10 @@ public class YarnUIServiceModelGenerator extends ResourceManagerServiceModelGene
 
     if(isSSLEnabled(service, serviceConfig)) {
       scheme = "https";
-      port = getRoleConfigValue(roleConfig, "resourcemanager_webserver_https_port");
+      port = getRoleConfigValue(roleConfig, RM_HTTPS_PORT);
     } else {
       scheme = "http";
-      port = getRoleConfigValue(roleConfig, "resourcemanager_webserver_port");
+      port = getRoleConfigValue(roleConfig, RM_HTTP_PORT);
     }
     return String.format(Locale.getDefault(), "%s://%s:%s", scheme, hostname, port);
   }
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/zeppelin/ZeppelinServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/zeppelin/ZeppelinServiceModelGenerator.java
index 8e99afb..c948d81 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/zeppelin/ZeppelinServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/zeppelin/ZeppelinServiceModelGenerator.java
@@ -31,6 +31,10 @@ public class ZeppelinServiceModelGenerator extends AbstractServiceModelGenerator
   public static final String SERVICE_TYPE = "ZEPPELIN";
   public static final String ROLE_TYPE    = "ZEPPELIN_SERVER";
 
+  protected static final String SSL_ENABLED     = "ssl_enabled";
+  protected static final String SERVER_SSL_PORT = "zeppelin_server_ssl_port";
+  protected static final String SERVER_PORT     = "zeppelin_server_port";
+
   @Override
   public String getService() {
     return SERVICE;
@@ -66,19 +70,25 @@ public class ZeppelinServiceModelGenerator extends AbstractServiceModelGenerator
       scheme = "http";
       port = getPort(roleConfig);
     }
-    return createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s", scheme, hostname, port));
+
+    ServiceModel model = createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s", scheme, hostname, port));
+    model.addRoleProperty(getRoleType(), SSL_ENABLED, getRoleConfigValue(roleConfig, SSL_ENABLED));
+    model.addRoleProperty(getRoleType(), SERVER_PORT, getPort(roleConfig));
+    model.addRoleProperty(getRoleType(), SERVER_SSL_PORT, getSSLPort(roleConfig));
+
+    return model;
   }
 
   protected boolean isSSL(ApiConfigList roleConfig) {
-    return Boolean.parseBoolean(getRoleConfigValue(roleConfig, "ssl_enabled"));
+    return Boolean.parseBoolean(getRoleConfigValue(roleConfig, SSL_ENABLED));
   }
 
   protected String getPort(ApiConfigList roleConfig) {
-    return getRoleConfigValue(roleConfig, "zeppelin_server_port");
+    return getRoleConfigValue(roleConfig, SERVER_PORT);
   }
 
   protected String getSSLPort(ApiConfigList roleConfig) {
-    return getRoleConfigValue(roleConfig, "zeppelin_server_ssl_port");
+    return getRoleConfigValue(roleConfig, SERVER_SSL_PORT);
   }
 
 }
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/zeppelin/ZeppelinWSServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/zeppelin/ZeppelinWSServiceModelGenerator.java
index 5b5f526..df823c9 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/zeppelin/ZeppelinWSServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/zeppelin/ZeppelinWSServiceModelGenerator.java
@@ -52,7 +52,14 @@ public class ZeppelinWSServiceModelGenerator extends ZeppelinServiceModelGenerat
       scheme = "ws";
       port = getPort(roleConfig);
     }
-    return createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s/ws", scheme, hostname, port));
+
+    ServiceModel model =
+        createServiceModel(String.format(Locale.getDefault(), "%s://%s:%s/ws", scheme, hostname, port));
+    model.addRoleProperty(getRoleType(), SSL_ENABLED, getRoleConfigValue(roleConfig, SSL_ENABLED));
+    model.addRoleProperty(getRoleType(), SERVER_PORT, getPort(roleConfig));
+    model.addRoleProperty(getRoleType(), SERVER_SSL_PORT, getSSLPort(roleConfig));
+
+    return model;
   }
 
 }
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/AbstractConfigurationStore.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/AbstractConfigurationStore.java
new file mode 100644
index 0000000..7d74cc8
--- /dev/null
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/AbstractConfigurationStore.java
@@ -0,0 +1,86 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.monitor;
+
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.topology.discovery.cm.ClouderaManagerServiceDiscoveryMessages;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Locale;
+
+import static org.apache.knox.gateway.topology.discovery.cm.monitor.ClouderaManagerClusterConfigurationMonitor.getType;
+
+public abstract class AbstractConfigurationStore {
+
+  // The name of the directory, under the gateway data directory, in which files will be persisted
+  static final String CLUSTERS_DATA_DIR_NAME = getType().toLowerCase(Locale.ROOT) + "-clusters";
+
+  protected static final ClouderaManagerServiceDiscoveryMessages log =
+                      MessagesFactory.get(ClouderaManagerServiceDiscoveryMessages.class);
+
+  protected GatewayConfig gatewayConfig;
+
+  public AbstractConfigurationStore(GatewayConfig gatewayConfig) {
+    this.gatewayConfig = gatewayConfig;
+  }
+
+  public void remove(String address, String cluster) {
+    // Delete the associated persisted record
+    File persisted = getPersistenceFile(address, cluster);
+    if (persisted.exists()) {
+      if (!persisted.delete()) {
+        log.failedToRemovPersistedClusterMonitorData(ClouderaManagerClusterConfigurationMonitor.getType(),
+                                                     persisted.getAbsolutePath());
+      }
+    }
+  }
+
+  protected abstract File getPersistenceFile(String address, String cluster);
+
+  protected String getMonitorType() {
+    return getType();
+  }
+
+  protected File getPersistenceFile(final String address, final String clusterName, final String ext) {
+    String fileName = address.replace(":", "_").replace("/", "_") + "-" + clusterName + "." + ext;
+    return getPersistenceDir().resolve(fileName).toFile();
+  }
+
+  protected Path getPersistenceDir() {
+    Path persistenceDir = null;
+
+    Path dataDir = Paths.get(gatewayConfig.getGatewayDataDir());
+    if (Files.exists(dataDir)) {
+      Path clustersDir = dataDir.resolve(CLUSTERS_DATA_DIR_NAME);
+      if (Files.notExists(clustersDir)) {
+        try {
+          Files.createDirectories(clustersDir);
+        } catch (IOException e) {
+          log.failedToCreatePersistenceDirectory(clustersDir.toAbsolutePath().toString());
+        }
+      }
+      persistenceDir = clustersDir;
+    }
+
+    return persistenceDir;
+  }
+}
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/ClouderaManagerClusterConfigurationMonitor.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/ClouderaManagerClusterConfigurationMonitor.java
new file mode 100644
index 0000000..3c24a41
--- /dev/null
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/ClouderaManagerClusterConfigurationMonitor.java
@@ -0,0 +1,227 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.monitor;
+
+import org.apache.commons.lang3.concurrent.BasicThreadFactory;
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.services.security.AliasService;
+import org.apache.knox.gateway.topology.discovery.ClusterConfigurationMonitor;
+import org.apache.knox.gateway.topology.discovery.ServiceDiscoveryConfig;
+import org.apache.knox.gateway.topology.discovery.cm.ClouderaManagerCluster;
+import org.apache.knox.gateway.topology.discovery.cm.ClouderaManagerServiceDiscoveryMessages;
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModel;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
+import static org.apache.knox.gateway.topology.discovery.ClusterConfigurationMonitor.ConfigurationChangeListener;
+
+/**
+ * ClusterConfigurationMonitor implementation for clusters managed by ClouderaManager.
+ */
+public class ClouderaManagerClusterConfigurationMonitor implements ClusterConfigurationMonitor,
+                                                                   ConfigurationChangeListener {
+
+  private static final String TYPE = "CM";
+
+  private static final ClouderaManagerServiceDiscoveryMessages log =
+      MessagesFactory.get(ClouderaManagerServiceDiscoveryMessages.class);
+
+  // The internal monitor implementation
+  private PollingConfigurationAnalyzer internalMonitor;
+
+  // Listeners to notify when configuration changes are discovered
+  private final List<ConfigurationChangeListener> changeListeners = new ArrayList<>();
+
+  // Cache of configuration data used by the monitor
+  private ClusterConfigurationCache configCache;
+
+  // Persistent store of discovery configuration
+  private DiscoveryConfigurationStore discoveryConfigStore;
+
+  // Persistent store of cluster service configurations
+  private ClusterConfigurationStore serviceConfigStore;
+
+  private ExecutorService executorService;
+
+
+  public static String getType() {
+    return TYPE;
+  }
+
+
+  ClouderaManagerClusterConfigurationMonitor(final GatewayConfig config, final AliasService aliasService) {
+    // Initialize the config cache
+    configCache = new ClusterConfigurationCache();
+
+    // Initialize the persistent stores
+    discoveryConfigStore = new DiscoveryConfigurationFileStore(config);
+    serviceConfigStore = new ClusterConfigurationFileStore(config);
+
+    // Configure the executor service
+    ThreadFactory tf =
+          new BasicThreadFactory.Builder().namingPattern("ClouderaManagerConfigurationMonitor-%d").build();
+    this.executorService = Executors.newSingleThreadExecutor(tf);
+
+    // Initialize the internal monitor
+    internalMonitor = new PollingConfigurationAnalyzer(configCache, aliasService, this);
+
+    // Override the default polling interval if it has been configured
+    // (org.apache.knox.gateway.topology.discovery.cm.monitor.interval)
+    int interval = config.getClusterMonitorPollingInterval(getType());
+    if (interval > 0) {
+      setPollingInterval(interval);
+    }
+
+    // Load any previously-persisted discovery configuration data
+    loadDiscoveryConfiguration();
+
+    // Load any previously-persisted cluster service configuration data
+    loadServiceConfiguration();
+  }
+
+  @Override
+  public void start() {
+    log.startingClouderaManagerConfigMonitor();
+    executorService.execute(internalMonitor);
+  }
+
+  @Override
+  public void stop() {
+    log.stoppingClouderaManagerConfigMonitor();
+    internalMonitor.stop();
+  }
+
+  @Override
+  public void setPollingInterval(int interval) {
+    internalMonitor.setInterval(interval);
+  }
+
+  @Override
+  public void addListener(final ConfigurationChangeListener listener) {
+    changeListeners.add(listener);
+  }
+
+  /**
+   * Add the specified cluster service configurations to the monitor.
+   *
+   * @param cluster         The cluster to be added.
+   * @param discoveryConfig The discovery configuration associated with the cluster.
+   */
+  public void addServiceConfiguration(final ClouderaManagerCluster cluster,
+                                      final ServiceDiscoveryConfig discoveryConfig) {
+
+    String address     = discoveryConfig.getAddress();
+    String clusterName = cluster.getName();
+
+    // Because this is the result of a discovery, disregard restart events which
+    // occurred before "now" in future polling
+    internalMonitor.setEventQueryTimestamp(address, clusterName, Instant.now());
+
+    // Persist the discovery configuration
+    discoveryConfigStore.store(discoveryConfig);
+
+    // Add the discovery configuration to the cache
+    configCache.addDiscoveryConfig(discoveryConfig);
+
+    Map<String, List<ServiceModel>> serviceModels = cluster.getServiceModels();
+
+    // Process the service models
+    Map<String, ServiceConfigurationModel> scpMap = new HashMap<>();
+    for (String service : serviceModels.keySet()) {
+      for (ServiceModel model : serviceModels.get(service)) {
+        ServiceConfigurationModel scp =
+            scpMap.computeIfAbsent(model.getServiceType(), p -> new ServiceConfigurationModel());
+
+        Map<String, String> serviceProps = model.getServiceProperties();
+        for (Map.Entry<String, String> entry : serviceProps.entrySet()) {
+          scp.addServiceProperty(entry.getKey(), entry.getValue());
+        }
+
+        Map<String, Map<String, String>> roleProps = model.getRoleProperties();
+        for (String roleName : roleProps.keySet()) {
+          Map<String, String> rp = roleProps.get(roleName);
+          for (Map.Entry<String, String> entry : rp.entrySet()) {
+            scp.addRoleProperty(roleName, entry.getKey(), entry.getValue());
+          }
+        }
+      }
+    }
+
+    // Persist the service configurations
+    serviceConfigStore.store(address, clusterName, scpMap);
+
+    // Add the service configurations to the cache
+    configCache.addServiceConfiguration(address, clusterName, scpMap);
+  }
+
+  /**
+   * Load any previously-persisted service discovery configurations.
+   */
+  private void loadDiscoveryConfiguration() {
+    for (ServiceDiscoveryConfig sdc : discoveryConfigStore.getAll()) {
+      configCache.addDiscoveryConfig(sdc);
+    }
+  }
+
+  /**
+   * Load any previously-persisted cluster service configuration data records, so the monitor can check
+   * previously-deployed topologies against the current cluster configuration, even across gateway restarts.
+   */
+  private void loadServiceConfiguration() {
+    for (ServiceConfigurationRecord record : serviceConfigStore.getAll()) {
+      configCache.addServiceConfiguration(record.getDiscoveryAddress(),
+                                            record.getClusterName(),
+                                            record.getConfigs());
+    }
+  }
+
+  @Override
+  public void clearCache(final String source, final String clusterName) {
+    configCache.removeServiceConfiguration(source, clusterName);
+
+    // Delete the associated persisted record
+    serviceConfigStore.remove(source, clusterName);
+  }
+
+  @Override
+  public void onConfigurationChange(final String source, final String clusterName) {
+    // Respond to change notifications from the internal monitor by notifying
+    // the listeners registered with this object
+    notifyChangeListeners(source, clusterName);
+  }
+
+  /**
+   * Notify any registered change listeners.
+   *
+   * @param source      The address of the ClouderaManager instance from which the cluster details were determined.
+   * @param clusterName The name of the cluster whose configuration details have changed.
+   */
+  private void notifyChangeListeners(final String source, final String clusterName) {
+    for (ConfigurationChangeListener listener : changeListeners) {
+      listener.onConfigurationChange(source, clusterName);
+    }
+  }
+
+}
\ No newline at end of file
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveOnTezServiceModelGenerator.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/ClouderaManagerClusterConfigurationMonitorProvider.java
similarity index 53%
copy from gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveOnTezServiceModelGenerator.java
copy to gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/ClouderaManagerClusterConfigurationMonitorProvider.java
index 276daec..c8f29bc 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveOnTezServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/ClouderaManagerClusterConfigurationMonitorProvider.java
@@ -14,22 +14,23 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package org.apache.knox.gateway.topology.discovery.cm.model.hive;
+package org.apache.knox.gateway.topology.discovery.cm.monitor;
 
-import com.cloudera.api.swagger.model.ApiConfigList;
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.services.security.AliasService;
+import org.apache.knox.gateway.topology.discovery.ClusterConfigurationMonitor;
+import org.apache.knox.gateway.topology.discovery.ClusterConfigurationMonitorProvider;
 
-public class HiveOnTezServiceModelGenerator extends HiveServiceModelGenerator {
-
-  public static final String SERVICE_TYPE = "HIVE_ON_TEZ";
+public class ClouderaManagerClusterConfigurationMonitorProvider implements ClusterConfigurationMonitorProvider {
 
   @Override
-  public String getServiceType() {
-    return SERVICE_TYPE;
+  public String getType() {
+    return ClouderaManagerClusterConfigurationMonitor.getType();
   }
 
   @Override
-  protected boolean checkHiveServer2HTTPMode(ApiConfigList roleConfig) {
-    return TRANSPORT_MODE_HTTP.equals(getRoleConfigValue(roleConfig, "hive_server2_transport_mode"));
+  public ClusterConfigurationMonitor newInstance(GatewayConfig config, AliasService aliasService) {
+    return new ClouderaManagerClusterConfigurationMonitor(config, aliasService);
   }
 
 }
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/ClusterConfigurationCache.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/ClusterConfigurationCache.java
new file mode 100644
index 0000000..297a0f7
--- /dev/null
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/ClusterConfigurationCache.java
@@ -0,0 +1,162 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.monitor;
+
+import org.apache.knox.gateway.topology.discovery.ServiceDiscoveryConfig;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+/**
+ * Cache of configuration used by the ClouderaManager cluster configuration monitor.
+ */
+class ClusterConfigurationCache {
+
+  // ClouderaManager address
+  //    clusterName -> ServiceDiscoveryConfig
+  //
+  private final Map<String, Map<String, ServiceDiscoveryConfig>> clusterMonitorConfigurations =
+      new ConcurrentHashMap<>();
+
+  // ClouderaManager address
+  //    clusterName
+  //        serviceType -> Properties
+  //
+  private final Map<String, Map<String, Map<String, ServiceConfigurationModel>>> clusterServiceConfigurations =
+      new ConcurrentHashMap<>();
+
+  private final ReadWriteLock serviceConfigurationsLock = new ReentrantReadWriteLock();
+
+  private final ReadWriteLock clusterMonitorConfigurationsLock = new ReentrantReadWriteLock();
+
+
+  void addServiceConfiguration(final String address,
+                               final String cluster,
+                               final Map<String, ServiceConfigurationModel> configs) {
+    serviceConfigurationsLock.writeLock().lock();
+    try {
+      clusterServiceConfigurations.computeIfAbsent(address, k -> new HashMap<>()).put(cluster, configs);
+    } finally {
+      serviceConfigurationsLock.writeLock().unlock();
+    }
+  }
+
+  /**
+   * Add discovery configuration details for the specified cluster, so the monitor
+   * knows how to connect to check for changes.
+   *
+   * @param config The associated service discovery configuration.
+   */
+  void addDiscoveryConfig(final ServiceDiscoveryConfig config) {
+
+    clusterMonitorConfigurationsLock.writeLock().lock();
+    try {
+      clusterMonitorConfigurations.computeIfAbsent(config.getAddress(), k -> new HashMap<>())
+                                  .put(config.getCluster(), config);
+    } finally {
+      clusterMonitorConfigurationsLock.writeLock().unlock();
+    }
+  }
+
+  /**
+   * Remove the specified cluster from monitoring.
+   *
+   * @param address     The address of the ClouderaManager instance.
+   * @param clusterName The name of the cluster.
+   */
+  void removeServiceConfiguration(final String address, final String clusterName) {
+    serviceConfigurationsLock.writeLock().lock();
+    try {
+      clusterServiceConfigurations.get(address).remove(clusterName);
+    } finally {
+      serviceConfigurationsLock.writeLock().unlock();
+    }
+  }
+
+  /**
+   * Get the service discovery configuration associated with the specified ClouderaManager instance and cluster.
+   *
+   * @param address     An ClouderaManager instance address.
+   * @param clusterName The name of a cluster associated with the ClouderaManager instance.
+   * @return The associated ServiceDiscoveryConfig object.
+   */
+  ServiceDiscoveryConfig getDiscoveryConfig(final String address, final String clusterName) {
+    ServiceDiscoveryConfig config = null;
+    clusterMonitorConfigurationsLock.readLock().lock();
+    try {
+      Map<String, ServiceDiscoveryConfig> clusterMap = clusterMonitorConfigurations.get(address);
+      if (clusterMap != null) {
+        config = clusterMap.get(clusterName);
+      }
+    } finally {
+      clusterMonitorConfigurationsLock.readLock().unlock();
+    }
+    return config;
+  }
+
+  /**
+   * Get the service configuration details for the specified cluster and ClouderaManager instance.
+   *
+   * @param address     A ClouderaManager instance address.
+   * @param clusterName The name of a cluster associated with the ClouderaManager instance.
+   * @return A Map of service types to their corresponding configuration properties.
+   */
+  Map<String, ServiceConfigurationModel> getClusterServiceConfigurations(final String address,
+                                                                         final String clusterName) {
+    Map<String, ServiceConfigurationModel> result = new HashMap<>();
+
+    serviceConfigurationsLock.readLock().lock();
+    try {
+      Map<String, Map<String, ServiceConfigurationModel>> clusterMap =
+                                     clusterServiceConfigurations.get(address);
+      if (clusterMap != null) {
+        result.putAll(clusterMap.get(clusterName));
+      }
+    } finally {
+      serviceConfigurationsLock.readLock().unlock();
+    }
+
+    return result;
+  }
+
+  /**
+   * Get all the clusters the monitor knows about.
+   *
+   * @return A Map of ClouderaManager instance addresses to associated cluster names.
+   */
+  Map<String, List<String>> getClusterNames() {
+    Map<String, List<String>> result = new HashMap<>();
+
+    serviceConfigurationsLock.readLock().lock();
+    try {
+      for (Map.Entry<String, Map<String, Map<String, ServiceConfigurationModel>>> configs :
+                                                                            clusterServiceConfigurations.entrySet()) {
+         result.put(configs.getKey(), new ArrayList<>(configs.getValue().keySet()));
+      }
+    } finally {
+      serviceConfigurationsLock.readLock().unlock();
+    }
+
+    return result;
+  }
+
+}
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/ClusterConfigurationFileStore.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/ClusterConfigurationFileStore.java
new file mode 100644
index 0000000..92bea8a
--- /dev/null
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/ClusterConfigurationFileStore.java
@@ -0,0 +1,113 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.monitor;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.io.FileUtils;
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.topology.discovery.cm.ClouderaManagerServiceDiscoveryMessages;
+
+import java.io.File;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+
+/**
+ * A file-based ClusterConfigurationStore implementation.
+ */
+public class ClusterConfigurationFileStore extends AbstractConfigurationStore
+                                           implements ClusterConfigurationStore {
+
+  private static final ClouderaManagerServiceDiscoveryMessages log =
+                            MessagesFactory.get(ClouderaManagerServiceDiscoveryMessages.class);
+
+  private ObjectMapper mapper;
+
+
+  ClusterConfigurationFileStore(GatewayConfig gatewayConfig) {
+    super(gatewayConfig);
+    mapper = new ObjectMapper();
+    mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
+  }
+
+  @Override
+  public void store(String address, String cluster, Map<String, ServiceConfigurationModel> configs) {
+    Path persistenceDir = getPersistenceDir();
+    if (persistenceDir != null && Files.exists(persistenceDir)) {
+      File persistenceFile = getPersistenceFile(address, cluster);
+      try (OutputStream out = Files.newOutputStream(persistenceFile.toPath())) {
+        ServiceConfigurationRecord record = new ServiceConfigurationRecord();
+        record.setClusterName(cluster);
+        record.setDiscoveryAddress(address);
+        record.setConfigs(configs);
+
+        mapper.writeValue(out, record);
+      } catch (Exception e) {
+        log.failedToPersistClusterMonitorData(getMonitorType(), persistenceFile.getAbsolutePath(), e);
+      }
+    }
+  }
+
+  @Override
+  public Set<ServiceConfigurationRecord> getAll() {
+    Set<ServiceConfigurationRecord> result = new HashSet<>();
+
+    Path persistenceDir = getPersistenceDir();
+    if (persistenceDir != null && Files.exists(persistenceDir)) {
+      Collection<File> persistedConfigs = FileUtils.listFiles(persistenceDir.toFile(), new String[]{"ver"}, false);
+      for (File persisted : persistedConfigs) {
+        result.add(get(persisted));
+      }
+    }
+
+    return result;
+  }
+
+  @Override
+  public ServiceConfigurationRecord get(String address, String cluster) {
+    return get(getPersistenceFile(address, cluster));
+  }
+
+  @Override
+  protected File getPersistenceFile(final String address, final String clusterName) {
+    return getPersistenceFile(address, clusterName.replaceAll(" ", "_"), "ver");
+  }
+
+  private ServiceConfigurationRecord get(final File persisted) {
+    ServiceConfigurationRecord result = null;
+
+    if (persisted != null && persisted.exists()) {
+      try (InputStream in = Files.newInputStream(persisted.toPath())) {
+        result = mapper.readValue(in, ServiceConfigurationRecord.class);
+      } catch (Exception e) {
+        log.failedToLoadClusterMonitorServiceConfigurations(getMonitorType(), e);
+      }
+    }
+
+    return result;
+  }
+
+}
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/ClusterConfigurationStore.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/ClusterConfigurationStore.java
new file mode 100644
index 0000000..69716b2
--- /dev/null
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/ClusterConfigurationStore.java
@@ -0,0 +1,61 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.monitor;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Interface for managing the persistence of cluster configuration data.
+ */
+public interface ClusterConfigurationStore {
+
+  /**
+   * Store the configuration for the specified cluster.
+   *
+   * @param address The address of the ClouderaManager instance managing the cluster
+   * @param cluster The name of the cluster
+   * @param configs The cluster configuration
+   */
+  void store(String address, String cluster, Map<String, ServiceConfigurationModel> configs);
+
+  /**
+   * Get all the stored cluster configurations.
+   *
+   * @return A Set of all the stored configurations
+   */
+  Set<ServiceConfigurationRecord> getAll();
+
+  /**
+   * Get the stored configuration for the specified cluster.
+   *
+   * @param address The address of the ClouderaManager instance managing the cluster
+   * @param cluster The name of the cluster
+   *
+   * @return A ServiceConfigurationRecord object
+   */
+  ServiceConfigurationRecord get(String address, String cluster);
+
+  /**
+   * Remove the configuration for the specified cluster.
+   *
+   * @param address The address of the ClouderaManager instance managing the cluster
+   * @param cluster The name of the cluster
+   */
+  void remove(String address, String cluster);
+
+}
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/DiscoveryConfigurationFileStore.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/DiscoveryConfigurationFileStore.java
new file mode 100644
index 0000000..bb0d8c7
--- /dev/null
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/DiscoveryConfigurationFileStore.java
@@ -0,0 +1,133 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.monitor;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.topology.discovery.ServiceDiscoveryConfig;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Properties;
+import java.util.Set;
+
+/**
+ * A file-based DiscoveryConfigurationStore implementation.
+ */
+public class DiscoveryConfigurationFileStore extends AbstractConfigurationStore
+                                             implements DiscoveryConfigurationStore {
+
+  private static final String PERSISTED_FILE_COMMENT = "Generated File. Do Not Edit!";
+
+  private static final String PROP_CLUSTER_PREFIX = "cluster.";
+  private static final String PROP_CLUSTER_SOURCE = PROP_CLUSTER_PREFIX + "source";
+  private static final String PROP_CLUSTER_NAME = PROP_CLUSTER_PREFIX + "name";
+  private static final String PROP_CLUSTER_USER = PROP_CLUSTER_PREFIX + "user";
+  private static final String PROP_CLUSTER_ALIAS = PROP_CLUSTER_PREFIX + "pwd.alias";
+
+  DiscoveryConfigurationFileStore(GatewayConfig gatewayConfig) {
+    super(gatewayConfig);
+  }
+
+  @Override
+  public void store(final ServiceDiscoveryConfig config) {
+    Path persistenceDir = getPersistenceDir();
+    if (persistenceDir != null && Files.exists(persistenceDir)) {
+
+      String address = config.getAddress();
+      String cluster = config.getCluster();
+
+      Properties props = new Properties();
+      props.setProperty(PROP_CLUSTER_NAME, cluster);
+      props.setProperty(PROP_CLUSTER_SOURCE, address);
+
+      String username = config.getUser();
+      if (username != null) {
+        props.setProperty(PROP_CLUSTER_USER, username);
+      }
+      String pwdAlias = config.getPasswordAlias();
+      if (pwdAlias != null) {
+        props.setProperty(PROP_CLUSTER_ALIAS, pwdAlias);
+      }
+
+      persist(props, getPersistenceFile(address,cluster));
+    }
+  }
+
+  @Override
+  public Set<ServiceDiscoveryConfig> getAll() {
+    Set<ServiceDiscoveryConfig> result = new HashSet<>();
+
+    Path persistenceDir = getPersistenceDir();
+    if (persistenceDir != null && Files.exists(persistenceDir)) {
+      Collection<File> persistedConfigs = FileUtils.listFiles(persistenceDir.toFile(), new String[]{"conf"}, false);
+      for (File persisted : persistedConfigs) {
+        Properties props = new Properties();
+        try (InputStream in = Files.newInputStream(persisted.toPath())) {
+          props.load(in);
+
+          result.add(new ServiceDiscoveryConfig() {
+            @Override
+            public String getAddress() {
+              return props.getProperty(PROP_CLUSTER_SOURCE);
+            }
+
+            @Override
+            public String getCluster() {
+              return props.getProperty(PROP_CLUSTER_NAME);
+            }
+
+            @Override
+            public String getUser() {
+              return props.getProperty(PROP_CLUSTER_USER);
+            }
+
+            @Override
+            public String getPasswordAlias() {
+              return props.getProperty(PROP_CLUSTER_ALIAS);
+            }
+          });
+        } catch (IOException e) {
+          log.failedToLoadClusterMonitorServiceDiscoveryConfig(getMonitorType(), e);
+        }
+      }
+    }
+
+    return result;
+  }
+
+  @Override
+  protected File getPersistenceFile(final String address, final String clusterName) {
+    return getPersistenceFile(address, clusterName.replaceAll(" ", "_"), "conf");
+  }
+
+  private void persist(final Properties props, final File dest) {
+    try (OutputStream out = Files.newOutputStream(dest.toPath())) {
+      props.store(out, PERSISTED_FILE_COMMENT);
+      out.flush();
+    } catch (Exception e) {
+      log.failedToPersistClusterMonitorData(getMonitorType(), dest.getAbsolutePath(), e);
+    }
+  }
+
+}
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/DiscoveryConfigurationStore.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/DiscoveryConfigurationStore.java
new file mode 100644
index 0000000..c808ed2
--- /dev/null
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/DiscoveryConfigurationStore.java
@@ -0,0 +1,50 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.monitor;
+
+import org.apache.knox.gateway.topology.discovery.ServiceDiscoveryConfig;
+
+import java.util.Set;
+
+/**
+ * Interface for managing the persistence of discovery configurations.
+ */
+public interface DiscoveryConfigurationStore {
+
+  /**
+   * Store the specified configuration.
+   *
+   * @param config A ServiceDiscoveryConfig
+   */
+  void store(ServiceDiscoveryConfig config);
+
+  /**
+   * Get all the stored discovery configurations.
+   *
+   * @return A Set of ServiceDiscoveryConfig objects
+   */
+  Set<ServiceDiscoveryConfig> getAll();
+
+  /**
+   * Remove the discovery configuration identified by the specified discovery address and cluster name.
+   *
+   * @param address The discovery address
+   * @param cluster The cluster name
+   */
+  void remove(String address, String cluster);
+
+}
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/PollingConfigurationAnalyzer.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/PollingConfigurationAnalyzer.java
new file mode 100644
index 0000000..81400cf
--- /dev/null
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/PollingConfigurationAnalyzer.java
@@ -0,0 +1,464 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.monitor;
+
+import com.cloudera.api.swagger.EventsResourceApi;
+import com.cloudera.api.swagger.RolesResourceApi;
+import com.cloudera.api.swagger.ServicesResourceApi;
+import com.cloudera.api.swagger.client.ApiClient;
+import com.cloudera.api.swagger.client.ApiException;
+import com.cloudera.api.swagger.model.ApiConfigList;
+import com.cloudera.api.swagger.model.ApiEvent;
+import com.cloudera.api.swagger.model.ApiEventAttribute;
+import com.cloudera.api.swagger.model.ApiEventCategory;
+import com.cloudera.api.swagger.model.ApiEventQueryResult;
+import com.cloudera.api.swagger.model.ApiRole;
+import com.cloudera.api.swagger.model.ApiRoleList;
+import com.cloudera.api.swagger.model.ApiServiceConfig;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.services.security.AliasService;
+import org.apache.knox.gateway.topology.discovery.ServiceDiscoveryConfig;
+import org.apache.knox.gateway.topology.discovery.cm.ClouderaManagerServiceDiscoveryMessages;
+import org.apache.knox.gateway.topology.discovery.cm.DiscoveryApiClient;
+
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static org.apache.knox.gateway.topology.discovery.ClusterConfigurationMonitor.ConfigurationChangeListener;
+
+@SuppressWarnings("PMD.DoNotUseThreads")
+public class PollingConfigurationAnalyzer implements Runnable {
+
+  // The format of the filter employed when restart events are queried from ClouderaManager
+  private static final String RESTART_EVENTS_QUERY_FORMAT =
+                                "category==" + ApiEventCategory.AUDIT_EVENT.getValue() +
+                                ";attributes.command==Restart" +
+                                ";attributes.command_status==SUCCEEDED" +
+                                ";attributes.cluster==\"%s\"%s";
+
+  // The format of the timestamp element of the restart events query filter
+  private static final String EVENTS_QUERY_TIMESTAMP_FORMAT = ";timeOccurred=gt=%s";
+
+  // The default amount of time before "now" to check for restart events the first time
+  private static final long DEFAULT_EVENT_QUERY_DEFAULT_TIMESTAMP_OFFSET = (60 * 60 * 1000); // one hour
+
+  private static final int DEFAULT_POLLING_INTERVAL = 60;
+
+  private static final ClouderaManagerServiceDiscoveryMessages log =
+                  MessagesFactory.get(ClouderaManagerServiceDiscoveryMessages.class);
+
+  private ClusterConfigurationCache configCache;
+
+  // Single listener for configuration change events
+  private ConfigurationChangeListener changeListener;
+
+  private AliasService aliasService;
+
+  // Polling interval in seconds
+  private int interval;
+
+  // Cache of ClouderaManager API clients, keyed by discovery address
+  private final Map<String, DiscoveryApiClient> clients = new ConcurrentHashMap<>();
+
+  // Timestamp records of the most recent restart event query per discovery address
+  private Map<String, String> eventQueryTimestamps = new ConcurrentHashMap<>();
+
+  // The amount of time before "now" to will check for restart events the first time
+  private long eventQueryDefaultTimestampOffset = DEFAULT_EVENT_QUERY_DEFAULT_TIMESTAMP_OFFSET;
+
+  private boolean isActive;
+
+
+  PollingConfigurationAnalyzer(final ClusterConfigurationCache   configCache,
+                               final AliasService                aliasService,
+                               final ConfigurationChangeListener changeListener) {
+    this(configCache, aliasService, changeListener, DEFAULT_POLLING_INTERVAL);
+  }
+
+  PollingConfigurationAnalyzer(final ClusterConfigurationCache   configCache,
+                               final AliasService                aliasService,
+                               final ConfigurationChangeListener changeListener,
+                               int                               interval) {
+    this.configCache    = configCache;
+    this.aliasService   = aliasService;
+    this.changeListener = changeListener;
+    this.interval       = interval;
+  }
+
+  void setInterval(int interval) {
+    this.interval = interval;
+  }
+
+  void stop() {
+    isActive = false;
+  }
+
+  private void waitFor(long seconds) {
+    try {
+      Thread.sleep(seconds * 1000L);
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+    }
+  }
+
+  @Override
+  public void run() {
+    log.startedClouderaManagerConfigMonitor(interval);
+    isActive = true;
+
+    while (isActive) {
+      for (Map.Entry<String, List<String>> entry : configCache.getClusterNames().entrySet()) {
+        String address = entry.getKey();
+        for (String clusterName : entry.getValue()) {
+          log.checkingClusterConfiguration(clusterName, address);
+
+          // Configuration changes don't mean anything without corresponding service restarts. Therefore, monitor
+          // restart events, and check the configuration only of the restarted service(s) to identify changes
+          // that should trigger re-discovery.
+          List<RestartEvent> restartEvents = getRestartEvents(address, clusterName);
+
+          // If there are no recent restart events, then nothing to do now
+          if (!restartEvents.isEmpty()) {
+            boolean configHasChanged = false;
+
+            // If there are restart events, then check the previously-recorded properties for the same service to
+            // identify if the configuration has changed
+            Map<String, ServiceConfigurationModel> serviceConfigurations =
+                                    configCache.getClusterServiceConfigurations(address, clusterName);
+
+            // Those services for which a restart even has been handled
+            List<String> handledServiceTypes = new ArrayList<>();
+
+            for (RestartEvent re : restartEvents) {
+              String serviceType = re.getServiceType();
+
+              // Determine if we've already handled a restart event for this service type
+              if (!handledServiceTypes.contains(serviceType)) {
+
+                // Get the previously-recorded configuration
+                ServiceConfigurationModel serviceConfig = serviceConfigurations.get(re.getServiceType());
+
+                if (serviceConfig != null) {
+                  // Get the current config for the restarted service, and compare with the previously-recorded config
+                  ServiceConfigurationModel currentConfig =
+                                  getCurrentServiceConfiguration(address, clusterName, re.getService());
+
+                  if (currentConfig != null) {
+                    log.analyzingCurrentServiceConfiguration(re.getService());
+                    try {
+                      configHasChanged = hasConfigurationChanged(serviceConfig, currentConfig);
+                    } catch (Exception e) {
+                      log.errorAnalyzingCurrentServiceConfiguration(re.getService(), e);
+                    }
+                  }
+                } else {
+                  // A new service (no prior config) represent a config change, since a descriptor may have referenced
+                  // the "new" service, but discovery had previously not succeeded because the service had not been
+                  // configured (appropriately) at that time.
+                  log.serviceEnabled(re.getService());
+                  configHasChanged = true;
+                }
+
+                handledServiceTypes.add(serviceType);
+              }
+
+              if (configHasChanged) {
+                break; // No need to continue checking once we've identified one reason to perform discovery again
+              }
+            }
+
+            // If a change has occurred, notify the listeners
+            if (configHasChanged) {
+              notifyChangeListener(address, clusterName);
+            }
+          }
+        }
+      }
+
+      waitFor(interval);
+    }
+
+    log.stoppedClouderaManagerConfigMonitor();
+  }
+
+  /**
+   * Notify the registered change listener.
+   *
+   * @param source      The address of the ClouderaManager instance from which the cluster details were determined.
+   * @param clusterName The name of the cluster whose configuration details have changed.
+   */
+  private void notifyChangeListener(final String source, final String clusterName) {
+    if (changeListener != null) {
+      changeListener.onConfigurationChange(source, clusterName);
+    }
+  }
+
+  void setEventQueryTimestamp(final String address, final String cluster, final Instant timestamp) {
+    eventQueryTimestamps.put((address + ":" + cluster), timestamp.toString());
+  }
+
+  private String getEventQueryTimestamp(final String address, final String cluster) {
+    return eventQueryTimestamps.get(address + ":" + cluster);
+  }
+
+  /**
+   * Get a DiscoveryApiClient for the ClouderaManager instance described by the specified discovery configuration.
+   *
+   * @param discoveryConfig The discovery configuration for interacting with a ClouderaManager instance.
+   */
+  private DiscoveryApiClient getApiClient(final ServiceDiscoveryConfig discoveryConfig) {
+    return clients.computeIfAbsent(discoveryConfig.getAddress(),
+                                   c -> new DiscoveryApiClient(discoveryConfig, aliasService));
+  }
+
+  /**
+   * Get restart events for the specified ClouderaManager cluster.
+   *
+   * @param address     The address of the ClouderaManager instance.
+   * @param clusterName The name of the cluster.
+   *
+   * @return A List of RestartEvent objects for service restart events since the last time they were queried.
+   */
+  private List<RestartEvent> getRestartEvents(final String address, final String clusterName) {
+    List<RestartEvent> restartEvents = new ArrayList<>();
+
+    // Get the last event query timestamp
+    String lastTimestamp = getEventQueryTimestamp(address, clusterName);
+
+    // If this is the first query, then define the last timestamp
+    if (lastTimestamp == null) {
+      lastTimestamp = Instant.now().minus(eventQueryDefaultTimestampOffset, ChronoUnit.MILLIS).toString();
+    }
+
+    log.queryingRestartEventsFromCluster(clusterName, address, lastTimestamp);
+
+    // Record the new event query timestamp for this address/cluster
+    setEventQueryTimestamp(address, clusterName, Instant.now());
+
+    // Query the event log from CM for service/cluster restart events
+    List<ApiEvent> events = queryRestartEvents(getApiClient(configCache.getDiscoveryConfig(address, clusterName)),
+                                               clusterName,
+                                               lastTimestamp);
+    for (ApiEvent event : events) {
+      restartEvents.add(new RestartEvent(event));
+    }
+
+    return restartEvents;
+  }
+
+  /**
+   * Query the ClouderaManager instance associated with the specified client for any service restart events in the
+   * specified cluster since the specified time.
+   *
+   * @param client      A ClouderaManager API client.
+   * @param clusterName The name of the cluster for which events should be queried.
+   * @param since       The ISO8601 timestamp indicating from which time to query.
+   *
+   * @return A List of ApiEvent objects representing the relevant events since the specified time.
+   */
+  protected List<ApiEvent> queryRestartEvents(final ApiClient client, final String clusterName, final String since) {
+    List<ApiEvent> events = new ArrayList<>();
+
+    // Setup the query for restart events
+    String timeFilter =
+        (since != null) ? String.format(Locale.ROOT, EVENTS_QUERY_TIMESTAMP_FORMAT, since) : "";
+
+    String queryString = String.format(Locale.ROOT,
+                                       RESTART_EVENTS_QUERY_FORMAT,
+                                       clusterName,
+                                       timeFilter);
+
+    try {
+      ApiEventQueryResult eventsResult = (new EventsResourceApi(client)).readEvents(20, queryString, 0);
+      events.addAll(eventsResult.getItems());
+    } catch (ApiException e) {
+      log.clouderaManagerEventsAPIError(e);
+    }
+
+    return events;
+  }
+
+  /**
+   * Get the current configuration for the specified service.
+   *
+   * @param address     The address of the ClouderaManager instance.
+   * @param clusterName The name of the cluster.
+   * @param service     The name of the service.
+   *
+   * @return A ServiceConfigurationModel object with the configuration properties associated with the specified
+   * service.
+   */
+  protected ServiceConfigurationModel getCurrentServiceConfiguration(final String address,
+                                                                     final String clusterName,
+                                                                     final String service) {
+    ServiceConfigurationModel currentConfig = null;
+
+    log.gettingCurrentClusterConfiguration(service, clusterName, address);
+
+    ApiClient apiClient = getApiClient(configCache.getDiscoveryConfig(address, clusterName));
+    ServicesResourceApi api = new ServicesResourceApi(apiClient);
+    try {
+      ApiServiceConfig svcConfig = api.readServiceConfig(clusterName, service, "full");
+
+      Map<ApiRole, ApiConfigList> roleConfigs = new HashMap<>();
+      RolesResourceApi rolesApi = (new RolesResourceApi(apiClient));
+      ApiRoleList roles = rolesApi.readRoles(clusterName, service, "", "full");
+      for (ApiRole role : roles.getItems()) {
+        ApiConfigList config = rolesApi.readRoleConfig(clusterName, role.getName(), service, "full");
+        roleConfigs.put(role, config);
+      }
+      currentConfig = new ServiceConfigurationModel(svcConfig, roleConfigs);
+    } catch (ApiException e) {
+      log.clouderaManagerConfigurationAPIError(e);
+    }
+    return currentConfig;
+  }
+
+  /**
+   * Examine the ServiceConfigurationModel objects for significant differences.
+   *
+   * @param previous The previously-recorded service configuration properties.
+   * @param current  The current service configuration properties.
+   *
+   * @return true, if the current service configuration values differ from those properties defined in the previous
+   * service configuration; Otherwise, false.
+   */
+  private boolean hasConfigurationChanged(final ServiceConfigurationModel previous,
+                                          final ServiceConfigurationModel current) {
+    boolean hasChanged = false;
+
+    // Compare the service configuration properties first
+    Map<String, String> previousProps = previous.getServiceProps();
+    Map<String, String> currentProps = current.getServiceProps();
+    for (String name : previousProps.keySet()) {
+      String prevValue = previousProps.get(name);
+      String currValue = currentProps.get(name);
+      if (!prevValue.equals(currValue)) {
+        log.serviceConfigurationPropertyHasChanged(name, prevValue, currValue);
+        hasChanged = true;
+        break;
+      }
+    }
+
+    // If service config has not changed, check the role configuration properties
+    if (!hasChanged) {
+      Set<String> previousRoleTypes = previous.getRoleTypes();
+      Set<String> currentRoleTypes = current.getRoleTypes();
+      for (String roleType : previousRoleTypes) {
+        if (!currentRoleTypes.contains(roleType)) {
+          log.roleTypeRemoved(roleType);
+          hasChanged = true;
+          break;
+        } else {
+          previousProps = previous.getRoleProps(roleType);
+          currentProps = current.getRoleProps(roleType);
+          for (String name : previousProps.keySet()) {
+            String prevValue = previousProps.get(name);
+            String currValue = currentProps.get(name);
+            if (currValue == null) { // A missing/removed property
+              if (!(prevValue == null || "null".equals(prevValue))) {
+                log.roleConfigurationPropertyHasChanged(name, prevValue, "null");
+                hasChanged = true;
+                break;
+              }
+            } else if (!currValue.equals(prevValue)) {
+              log.roleConfigurationPropertyHasChanged(name, prevValue, currValue);
+              hasChanged = true;
+              break;
+            }
+          }
+        }
+      }
+    }
+
+    return hasChanged;
+  }
+
+  /**
+   * Internal representation of a ClouderaManager service restart event
+   */
+  static final class RestartEvent {
+
+    private static final String ATTR_CLUSTER = "CLUSTER";
+    private static final String ATTR_SERVICE_TYPE = "SERVICE_TYPE";
+    private static final String ATTR_SERVICE = "SERVICE";
+
+    private static List<String> attrsOfInterest = new ArrayList<>();
+
+    static {
+      attrsOfInterest.add(ATTR_CLUSTER);
+      attrsOfInterest.add(ATTR_SERVICE_TYPE);
+      attrsOfInterest.add(ATTR_SERVICE);
+    }
+
+    private ApiEvent auditEvent;
+    private String clusterName;
+    private String serviceType;
+    private String service;
+
+    RestartEvent(final ApiEvent auditEvent) {
+      if (ApiEventCategory.AUDIT_EVENT != auditEvent.getCategory()) {
+        throw new IllegalArgumentException("Invalid event category " + auditEvent.getCategory().getValue());
+      }
+      this.auditEvent = auditEvent;
+      for (ApiEventAttribute attribute : auditEvent.getAttributes()) {
+        if (attrsOfInterest.contains(attribute.getName())) {
+          setPropertyFromAttribute(attribute);
+        }
+      }
+    }
+
+    String getTimestamp() {
+      return auditEvent.getTimeOccurred();
+    }
+
+    String getClusterName() {
+      return clusterName;
+    }
+
+    String getServiceType() {
+      return serviceType;
+    }
+
+    String getService() {
+      return service;
+    }
+
+    private void setPropertyFromAttribute(final ApiEventAttribute attribute) {
+      switch (attribute.getName()) {
+        case ATTR_CLUSTER:
+          clusterName = attribute.getValues().get(0);
+          break;
+        case ATTR_SERVICE_TYPE:
+          serviceType = attribute.getValues().get(0);
+          break;
+        case ATTR_SERVICE:
+          service = attribute.getValues().get(0);
+          break;
+        default:
+      }
+    }
+  }
+
+}
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/ServiceConfigurationModel.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/ServiceConfigurationModel.java
new file mode 100644
index 0000000..0524378
--- /dev/null
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/ServiceConfigurationModel.java
@@ -0,0 +1,94 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.monitor;
+
+import com.cloudera.api.swagger.model.ApiConfig;
+import com.cloudera.api.swagger.model.ApiConfigList;
+import com.cloudera.api.swagger.model.ApiRole;
+import com.cloudera.api.swagger.model.ApiServiceConfig;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Data model for the cluster configuration monitor.
+ */
+final class ServiceConfigurationModel {
+
+  private static final String NULL_VALUE = "null";
+
+  private Map<String, String> serviceProps = new ConcurrentHashMap<>();
+  private Map<String, Map<String, String>> roleProps = new ConcurrentHashMap<>();
+
+  ServiceConfigurationModel() {
+  }
+
+  ServiceConfigurationModel(final ApiServiceConfig            serviceConfig,
+                            final Map<ApiRole, ApiConfigList> roles) {
+    // Service properties
+    for (ApiConfig item : serviceConfig.getItems()) {
+      String value = item.getValue();
+      if (value == null || value.isEmpty()) {
+        value = item.getDefault();
+      }
+      addServiceProperty(item.getName(), value);
+    }
+
+    // Role properties
+    if (roles != null && !roles.isEmpty()) {
+      for (Map.Entry<ApiRole, ApiConfigList> entry : roles.entrySet()) {
+        ApiRole role = entry.getKey();
+        ApiConfigList roleConfigList = entry.getValue();
+
+        for (ApiConfig roleConfig : roleConfigList.getItems()) {
+          roleConfig.getName();
+          String value = roleConfig.getValue();
+          if (value == null) {
+            value = roleConfig.getDefault();
+          }
+          addRoleProperty(role.getType(), roleConfig.getName(), value);
+        }
+      }
+    }
+  }
+
+  void addServiceProperty(final String name, final String value) {
+    serviceProps.put(name, (value != null ? value : NULL_VALUE));
+  }
+
+  Map<String, String> getServiceProps() {
+    return serviceProps;
+  }
+
+  void addRoleProperty(final String roleType, final String name, final String value) {
+    roleProps.computeIfAbsent(roleType, p -> new ConcurrentHashMap<>())
+        .put(name, (value != null ? value : NULL_VALUE));
+  }
+
+  Set<String> getRoleTypes() {
+    return roleProps.keySet();
+  }
+
+  Map<String, String> getRoleProps(final String roleType) {
+    return roleProps.get(roleType);
+  }
+
+  Map<String, Map<String, String>> getRoleProps() {
+    return roleProps;
+  }
+}
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/ServiceConfigurationRecord.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/ServiceConfigurationRecord.java
new file mode 100644
index 0000000..9dfb513
--- /dev/null
+++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/ServiceConfigurationRecord.java
@@ -0,0 +1,55 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.monitor;
+
+import java.util.Map;
+
+/**
+ * Model of the persisted data for a ClouderaManager-managed cluster service configurations.
+ */
+final class ServiceConfigurationRecord {
+  private String clusterName;
+  private String discoveryAddress;
+
+  // Map of services to their associated configuration models
+  private Map<String, ServiceConfigurationModel> configs;
+
+
+  public void setClusterName(final String clusterName) {
+    this.clusterName = clusterName;
+  }
+
+  public void setDiscoveryAddress(final String discoveryAddress) {
+    this.discoveryAddress = discoveryAddress;
+  }
+
+  public void setConfigs(Map<String, ServiceConfigurationModel> configs) {
+    this.configs = configs;
+  }
+
+  public String getClusterName() {
+    return clusterName;
+  }
+
+  public String getDiscoveryAddress() {
+    return discoveryAddress;
+  }
+
+  public Map<String, ServiceConfigurationModel> getConfigs() {
+    return configs;
+  }
+}
diff --git a/gateway-discovery-cm/src/main/resources/META-INF/services/org.apache.knox.gateway.topology.discovery.ClusterConfigurationMonitorProvider b/gateway-discovery-cm/src/main/resources/META-INF/services/org.apache.knox.gateway.topology.discovery.ClusterConfigurationMonitorProvider
new file mode 100644
index 0000000..05d3473
--- /dev/null
+++ b/gateway-discovery-cm/src/main/resources/META-INF/services/org.apache.knox.gateway.topology.discovery.ClusterConfigurationMonitorProvider
@@ -0,0 +1,19 @@
+##########################################################################
+# 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.
+##########################################################################
+
+org.apache.knox.gateway.topology.discovery.cm.monitor.ClouderaManagerClusterConfigurationMonitorProvider
\ No newline at end of file
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/AbstractCMDiscoveryTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/AbstractCMDiscoveryTest.java
new file mode 100644
index 0000000..fa2198c
--- /dev/null
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/AbstractCMDiscoveryTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm;
+
+import com.cloudera.api.swagger.model.ApiConfig;
+import com.cloudera.api.swagger.model.ApiConfigList;
+import com.cloudera.api.swagger.model.ApiHostRef;
+import com.cloudera.api.swagger.model.ApiRole;
+import com.cloudera.api.swagger.model.ApiService;
+import com.cloudera.api.swagger.model.ApiServiceConfig;
+import org.easymock.EasyMock;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class AbstractCMDiscoveryTest {
+  protected static ApiService createApiServiceMock(final String serviceType) {
+    return createApiServiceMock(serviceType + "-1", serviceType);
+  }
+
+  protected static ApiService createApiServiceMock(final String serviceName, final String serviceType) {
+    ApiService service = EasyMock.createNiceMock(ApiService.class);
+    EasyMock.expect(service.getName()).andReturn(serviceName).anyTimes();
+    EasyMock.expect(service.getType()).andReturn(serviceType).anyTimes();
+    EasyMock.replay(service);
+    return service;
+  }
+
+  protected static ApiServiceConfig createApiServiceConfigMock(Map<String, String> configProps) {
+    ApiServiceConfig serviceConfig = EasyMock.createNiceMock(ApiServiceConfig.class);
+    EasyMock.expect(serviceConfig.getItems()).andReturn(createMockApiConfigs(configProps)).anyTimes();
+    EasyMock.replay(serviceConfig);
+    return serviceConfig;
+  }
+
+  protected static ApiRole createApiRoleMock(final String roleType) {
+    return createApiRoleMock(roleType + "-12345", roleType);
+  }
+
+  protected static ApiRole createApiRoleMock(final String roleName, final String roleType) {
+    ApiRole role = EasyMock.createNiceMock(ApiRole.class);
+    EasyMock.expect(role.getName()).andReturn(roleName).anyTimes();
+    EasyMock.expect(role.getType()).andReturn(roleType).anyTimes();
+
+    ApiHostRef hostRef = EasyMock.createNiceMock(ApiHostRef.class);
+    EasyMock.expect(hostRef.getHostname()).andReturn("localhost").anyTimes();
+    EasyMock.replay(hostRef);
+    EasyMock.expect(role.getHostRef()).andReturn(hostRef).anyTimes();
+
+    EasyMock.replay(role);
+    return role;
+  }
+
+  protected static ApiConfigList createApiConfigListMock(final Map<String, String> configProps) {
+    ApiConfigList configList = EasyMock.createNiceMock(ApiConfigList.class);
+    EasyMock.expect(configList.getItems()).andReturn(createMockApiConfigs(configProps)).anyTimes();
+    EasyMock.replay(configList);
+    return configList;
+  }
+
+  protected static List<ApiConfig> createMockApiConfigs(final Map<String, String> configProps) {
+    List<ApiConfig> configList = new ArrayList<>();
+
+    for (Map.Entry<String, String> entry : configProps.entrySet()) {
+      configList.add(createApiConfigMock(entry.getKey(), entry.getValue()));
+    }
+
+    return configList;
+  }
+
+  protected static ApiConfig createApiConfigMock(final String name, final String value) {
+    ApiConfig apiConfig = EasyMock.createNiceMock(ApiConfig.class);
+    EasyMock.expect(apiConfig.getName()).andReturn(name).anyTimes();
+    EasyMock.expect(apiConfig.getValue()).andReturn(value).anyTimes();
+    EasyMock.replay(apiConfig);
+    return apiConfig;
+  }
+}
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/ClouderaManagerServiceDiscoveryTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/ClouderaManagerServiceDiscoveryTest.java
index 2175596..853197d 100644
--- a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/ClouderaManagerServiceDiscoveryTest.java
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/ClouderaManagerServiceDiscoveryTest.java
@@ -878,11 +878,11 @@ public class ClouderaManagerServiceDiscoveryTest {
                                                                    final String  thriftPath,
                                                                    final boolean enableSSL) {
     final String hs2SafetyValveValue =
-        "<property><name>hive.server2.thrift.http.port</name><value>" + thriftPort + "</value></property>\n" +
         "<property><name>hive.server2.thrift.http.path</name><value>" + thriftPath + "</value></property>";
 
     // Configure the role
     Map<String, String> roleProperties = new HashMap<>();
+    roleProperties.put("hive_server2_thrift_http_port", thriftPort);
     roleProperties.put("hive_server2_transport_mode", "http");
     roleProperties.put("hive_hs2_config_safety_valve", hs2SafetyValveValue);
     roleProperties.put("hive.server2.use.SSL", String.valueOf(enableSSL));
@@ -987,7 +987,7 @@ public class ClouderaManagerServiceDiscoveryTest {
 
   private ServiceDiscovery.Cluster doTestLivyDiscovery(final String  hostName,
                                                        final String  port,
-                                                       final boolean isSSL) {
+                                                       final Boolean isSSL) {
     // Configure the role
     Map<String, String> roleProperties = new HashMap<>();
     roleProperties.put("livy_server_port", port);
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/AbstractServiceModelGeneratorTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/AbstractServiceModelGeneratorTest.java
new file mode 100644
index 0000000..ab55e0f
--- /dev/null
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/AbstractServiceModelGeneratorTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.model;
+
+import com.cloudera.api.swagger.client.ApiException;
+import org.apache.knox.gateway.topology.discovery.cm.AbstractCMDiscoveryTest;
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModel;
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public abstract class AbstractServiceModelGeneratorTest extends AbstractCMDiscoveryTest {
+
+  protected abstract String getServiceType();
+
+  protected abstract String getRoleType();
+
+  protected abstract ServiceModelGenerator newGenerator();
+
+  @Test
+  public void testHandles() {
+    assertTrue(doTestHandles(newGenerator(), getServiceType(), getRoleType(), Collections.emptyMap()));
+  }
+
+  @Test
+  public void testHandlesWrongRoleType() {
+    assertFalse(doTestHandles(newGenerator(), getServiceType(), "INCORRECT_ROLE_TYPE", Collections.emptyMap()));
+  }
+
+  @Test
+  public void testHandlesWrongServiceType() {
+    assertFalse(doTestHandles(newGenerator(), "INCORRECT_SERVICE_TYPE", getRoleType(), Collections.emptyMap()));
+  }
+
+
+  protected boolean doTestHandles(final ServiceModelGenerator generator,
+                                  final String                serviceType,
+                                  final String                roleType,
+                                  final Map<String, String>   roleProps) {
+    return doTestHandles(generator,
+                         serviceType,
+                         Collections.emptyMap(),
+                         roleType,
+                         roleProps);
+  }
+
+  protected boolean doTestHandles(final ServiceModelGenerator generator,
+                                  final String                serviceType,
+                                  final Map<String, String>   serviceConfig,
+                                  final String                roleType,
+                                  final Map<String, String>   roleProps) {
+    return generator.handles(createApiServiceMock(serviceType),
+                             createApiServiceConfigMock(serviceConfig),
+                             createApiRoleMock(roleType),
+                             createApiConfigListMock(roleProps));
+  }
+
+
+  protected ServiceModel createServiceModel(Map<String, String> serviceConfig, Map<String, String> roleConfig) {
+    ServiceModel model = null;
+    try {
+      model = newGenerator().generateService(createApiServiceMock(getServiceType()),
+                                             createApiServiceConfigMock(serviceConfig),
+                                             createApiRoleMock(getRoleType()),
+                                             createApiConfigListMock(roleConfig));
+    } catch (ApiException e) {
+      fail(e.getMessage());
+    }
+
+    return model;
+  }
+
+  protected void validateServiceModel(ServiceModel        candidate,
+                                      Map<String, String> serviceConfig,
+                                      Map<String, String> roleConfig) {
+
+    assertNotNull(candidate);
+
+    // Validate the service configuration
+    Map<String, String> modelServiceProps = candidate.getServiceProperties();
+    assertEquals(serviceConfig.size(), modelServiceProps.size());
+    for (Map.Entry<String, String> serviceProp : serviceConfig.entrySet()) {
+      assertTrue(modelServiceProps.containsKey(serviceProp.getKey()));
+      assertEquals(serviceConfig.get(serviceProp.getKey()), modelServiceProps.get(serviceProp.getKey()));
+    }
+
+    // Validate the role configuration
+    Map<String, String> modelRoleProperties = candidate.getRoleProperties().get(getRoleType());
+    assertEquals(roleConfig.size(), modelRoleProperties.size());
+    for (Map.Entry<String, String> roleProp : roleConfig.entrySet()) {
+      assertTrue(modelRoleProperties.containsKey(roleProp.getKey()));
+      assertEquals(roleConfig.get(roleProp.getKey()), modelRoleProperties.get(roleProp.getKey()));
+    }
+  }
+
+}
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveOnTezServiceModelGenerator.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/atlas/AtlasAPIServiceModelGeneratorTest.java
similarity index 61%
copy from gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveOnTezServiceModelGenerator.java
copy to gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/atlas/AtlasAPIServiceModelGeneratorTest.java
index 276daec..ac42055 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveOnTezServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/atlas/AtlasAPIServiceModelGeneratorTest.java
@@ -14,22 +14,25 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package org.apache.knox.gateway.topology.discovery.cm.model.hive;
+package org.apache.knox.gateway.topology.discovery.cm.model.atlas;
 
-import com.cloudera.api.swagger.model.ApiConfigList;
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator;
 
-public class HiveOnTezServiceModelGenerator extends HiveServiceModelGenerator {
+public class AtlasAPIServiceModelGeneratorTest extends AtlasServiceModelGeneratorTest {
 
-  public static final String SERVICE_TYPE = "HIVE_ON_TEZ";
+  @Override
+  protected String getServiceType() {
+    return AtlasAPIServiceModelGenerator.SERVICE_TYPE;
+  }
 
   @Override
-  public String getServiceType() {
-    return SERVICE_TYPE;
+  protected String getRoleType() {
+    return AtlasAPIServiceModelGenerator.ROLE_TYPE;
   }
 
   @Override
-  protected boolean checkHiveServer2HTTPMode(ApiConfigList roleConfig) {
-    return TRANSPORT_MODE_HTTP.equals(getRoleConfigValue(roleConfig, "hive_server2_transport_mode"));
+  protected ServiceModelGenerator newGenerator() {
+    return new AtlasAPIServiceModelGenerator();
   }
 
 }
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/atlas/AtlasServiceModelGeneratorTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/atlas/AtlasServiceModelGeneratorTest.java
new file mode 100644
index 0000000..6b623ac
--- /dev/null
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/atlas/AtlasServiceModelGeneratorTest.java
@@ -0,0 +1,56 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.model.atlas;
+
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator;
+import org.apache.knox.gateway.topology.discovery.cm.model.AbstractServiceModelGeneratorTest;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+
+public class AtlasServiceModelGeneratorTest extends AbstractServiceModelGeneratorTest {
+
+  @Test
+  public void testServiceModelMetadata() {
+    final Map<String, String> serviceConfig = Collections.emptyMap();
+    final Map<String, String> roleConfig = new HashMap<>();
+    roleConfig.put(AtlasServiceModelGenerator.SSL_ENABLED, "false");
+    roleConfig.put(AtlasServiceModelGenerator.HTTP_PORT, "2468");
+    roleConfig.put(AtlasServiceModelGenerator.HTTPS_PORT, "3579");
+
+    validateServiceModel(createServiceModel(serviceConfig, roleConfig), serviceConfig, roleConfig);
+  }
+
+  @Override
+  protected String getServiceType() {
+    return AtlasServiceModelGenerator.SERVICE_TYPE;
+  }
+
+  @Override
+  protected String getRoleType() {
+    return AtlasServiceModelGenerator.ROLE_TYPE;
+  }
+
+  @Override
+  protected ServiceModelGenerator newGenerator() {
+    return new AtlasServiceModelGenerator();
+  }
+
+}
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/hbase/HBaseUIServiceModelGeneratorTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/hbase/HBaseUIServiceModelGeneratorTest.java
new file mode 100644
index 0000000..d1b6eb6
--- /dev/null
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/hbase/HBaseUIServiceModelGeneratorTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.model.hbase;
+
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator;
+import org.apache.knox.gateway.topology.discovery.cm.model.AbstractServiceModelGeneratorTest;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class HBaseUIServiceModelGeneratorTest extends AbstractServiceModelGeneratorTest {
+
+  @Test
+  public void testServiceModelMetadata() {
+    final Map<String, String> serviceConfig = new HashMap<>();
+    serviceConfig.put(HBaseUIServiceModelGenerator.SSL_ENABLED, "false");
+    final Map<String, String> roleConfig = new HashMap<>();
+    roleConfig.put(HBaseUIServiceModelGenerator.MASTER_INFO_PORT, "22008");
+
+    validateServiceModel(createServiceModel(serviceConfig, roleConfig), serviceConfig, roleConfig);
+  }
+
+  @Override
+  protected String getServiceType() {
+    return HBaseUIServiceModelGenerator.SERVICE_TYPE;
+  }
+
+  @Override
+  protected String getRoleType() {
+    return HBaseUIServiceModelGenerator.ROLE_TYPE;
+  }
+
+  @Override
+  protected ServiceModelGenerator newGenerator() {
+    return new HBaseUIServiceModelGenerator();
+  }
+
+}
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/hbase/WebHBaseServiceModelGeneratorTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/hbase/WebHBaseServiceModelGeneratorTest.java
new file mode 100644
index 0000000..17a8fb5
--- /dev/null
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/hbase/WebHBaseServiceModelGeneratorTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.model.hbase;
+
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator;
+import org.apache.knox.gateway.topology.discovery.cm.model.AbstractServiceModelGeneratorTest;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+
+public class WebHBaseServiceModelGeneratorTest extends AbstractServiceModelGeneratorTest {
+
+  @Test
+  public void testServiceModelMetadata() {
+    final Map<String, String> serviceConfig = Collections.emptyMap();
+    final Map<String, String> roleConfig = new HashMap<>();
+    roleConfig.put(WebHBaseServiceModelGenerator.SSL_ENABLED, "false");
+    roleConfig.put(WebHBaseServiceModelGenerator.REST_SERVER_PORT, "22009");
+
+    validateServiceModel(createServiceModel(serviceConfig, roleConfig), serviceConfig, roleConfig);
+  }
+
+  @Override
+  protected String getServiceType() {
+    return WebHBaseServiceModelGenerator.SERVICE_TYPE;
+  }
+
+  @Override
+  protected String getRoleType() {
+    return WebHBaseServiceModelGenerator.ROLE_TYPE;
+  }
+
+  @Override
+  protected ServiceModelGenerator newGenerator() {
+    return new WebHBaseServiceModelGenerator();
+  }
+
+}
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/hdfs/HdfsUIServiceModelGeneratorTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/hdfs/HdfsUIServiceModelGeneratorTest.java
new file mode 100644
index 0000000..5879d65
--- /dev/null
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/hdfs/HdfsUIServiceModelGeneratorTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.model.hdfs;
+
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator;
+import org.apache.knox.gateway.topology.discovery.cm.model.AbstractServiceModelGeneratorTest;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class HdfsUIServiceModelGeneratorTest extends AbstractServiceModelGeneratorTest {
+
+  @Test
+  public void testServiceModelMetadata() {
+    final Map<String, String> serviceConfig = new HashMap<>();
+    serviceConfig.put(HdfsUIServiceModelGenerator.SSL_ENABLED, "false");
+    final Map<String, String> roleConfig = new HashMap<>();
+    roleConfig.put(HdfsUIServiceModelGenerator.HTTP_PORT, "12345");
+    roleConfig.put(HdfsUIServiceModelGenerator.HTTPS_PORT, "54321");
+
+    validateServiceModel(createServiceModel(serviceConfig, roleConfig), serviceConfig, roleConfig);
+  }
+
+  @Override
+  protected String getServiceType() {
+    return HdfsUIServiceModelGenerator.SERVICE_TYPE;
+  }
+
+  @Override
+  protected String getRoleType() {
+    return HdfsUIServiceModelGenerator.ROLE_TYPE;
+  }
+
+  @Override
+  protected ServiceModelGenerator newGenerator() {
+    return new HdfsUIServiceModelGenerator();
+  }
+}
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/hdfs/NameNodeServiceModelGeneratorTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/hdfs/NameNodeServiceModelGeneratorTest.java
new file mode 100644
index 0000000..ef1848d
--- /dev/null
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/hdfs/NameNodeServiceModelGeneratorTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.model.hdfs;
+
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator;
+import org.apache.knox.gateway.topology.discovery.cm.model.AbstractServiceModelGeneratorTest;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class NameNodeServiceModelGeneratorTest extends AbstractServiceModelGeneratorTest {
+
+  @Test
+  public void testServiceModelMetadataWithNameService() {
+    final Map<String, String> serviceConfig = Collections.emptyMap();
+    final Map<String, String> roleConfig = new HashMap<>();
+    roleConfig.put(NameNodeServiceModelGenerator.AUTOFAILOVER_ENABLED, "true");
+    roleConfig.put(NameNodeServiceModelGenerator.NN_NAMESERVICE, "myService");
+    roleConfig.put(NameNodeServiceModelGenerator.NN_PORT, "12345");
+
+    validateServiceModel(createServiceModel(serviceConfig, roleConfig), serviceConfig, roleConfig);
+  }
+
+  @Test
+  public void testServiceModelMetadataWithNNPort() {
+    final Map<String, String> serviceConfig = Collections.emptyMap();
+    final Map<String, String> roleConfig = new HashMap<>();
+    roleConfig.put(NameNodeServiceModelGenerator.AUTOFAILOVER_ENABLED, "false");
+    roleConfig.put(NameNodeServiceModelGenerator.NN_PORT, "12345");
+
+    validateServiceModel(createServiceModel(serviceConfig, roleConfig), serviceConfig, roleConfig);
+  }
+
+
+  @Override
+  protected String getServiceType() {
+    return NameNodeServiceModelGenerator.SERVICE_TYPE;
+  }
+
+  @Override
+  protected String getRoleType() {
+    return NameNodeServiceModelGenerator.ROLE_TYPE;
+  }
+
+  @Override
+  protected ServiceModelGenerator newGenerator() {
+    return new NameNodeServiceModelGenerator();
+  }
+}
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/hdfs/WebHdfsServiceModelGeneratorTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/hdfs/WebHdfsServiceModelGeneratorTest.java
new file mode 100644
index 0000000..4e90e77
--- /dev/null
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/hdfs/WebHdfsServiceModelGeneratorTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.model.hdfs;
+
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator;
+import org.apache.knox.gateway.topology.discovery.cm.model.AbstractServiceModelGeneratorTest;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class WebHdfsServiceModelGeneratorTest extends AbstractServiceModelGeneratorTest {
+
+  @Test
+  @Override
+  public void testHandles() {
+    final Map<String, String> serviceConfig = new HashMap<>();
+    serviceConfig.put(WebHdfsServiceModelGenerator.WEBHDFS_ENABLED, "true");
+    assertTrue(doTestHandles(newGenerator(), getServiceType(), serviceConfig, getRoleType(), Collections.emptyMap()));
+  }
+
+  @Test
+  public void testHandlesWebHdfsDisabled() {
+    final Map<String, String> serviceConfig = new HashMap<>();
+    serviceConfig.put(WebHdfsServiceModelGenerator.WEBHDFS_ENABLED, "false");
+    assertFalse(doTestHandles(newGenerator(), getServiceType(), serviceConfig, getRoleType(), Collections.emptyMap()));
+  }
+
+  @Test
+  public void testServiceModelMetadata() {
+    final Map<String, String> serviceConfig = new HashMap<>();
+    serviceConfig.put(WebHdfsServiceModelGenerator.WEBHDFS_ENABLED, "true");
+    serviceConfig.put(WebHdfsServiceModelGenerator.SSL_ENABLED, "false");
+
+    final Map<String, String> roleConfig = new HashMap<>();
+    roleConfig.put(WebHdfsServiceModelGenerator.HTTP_PORT, "12345");
+    roleConfig.put(WebHdfsServiceModelGenerator.HTTPS_PORT, "54321");
+
+    validateServiceModel(createServiceModel(serviceConfig, roleConfig), serviceConfig, roleConfig);
+  }
+
+  @Override
+  protected String getServiceType() {
+    return WebHdfsServiceModelGenerator.SERVICE_TYPE;
+  }
+
+  @Override
+  protected String getRoleType() {
+    return WebHdfsServiceModelGenerator.ROLE_TYPE;
+  }
+
+  @Override
+  protected ServiceModelGenerator newGenerator() {
+    return new WebHdfsServiceModelGenerator();
+  }
+}
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveOnTezServiceModelGeneratorTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveOnTezServiceModelGeneratorTest.java
new file mode 100644
index 0000000..0d4de06
--- /dev/null
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveOnTezServiceModelGeneratorTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.model.hive;
+
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator;
+import org.apache.knox.gateway.topology.discovery.cm.model.AbstractServiceModelGeneratorTest;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class HiveOnTezServiceModelGeneratorTest extends AbstractServiceModelGeneratorTest {
+
+  @Test
+  @Override
+  public void testHandles() {
+    Map<String, String> roleConfig = new HashMap<>();
+    roleConfig.put(HiveOnTezServiceModelGenerator.HIVEONTEZ_TRANSPORT_MODE,
+                   HiveOnTezServiceModelGenerator.TRANSPORT_MODE_HTTP);
+    assertTrue(doTestHandles(newGenerator(), getServiceType(), Collections.emptyMap(), getRoleType(), roleConfig));
+  }
+
+  @Test
+  public void testHandlesWhenTransportModeIsBinary() {
+    Map<String, String> roleConfig = new HashMap<>();
+    roleConfig.put(HiveOnTezServiceModelGenerator.HIVEONTEZ_TRANSPORT_MODE, "binary");
+    assertFalse(doTestHandles(newGenerator(), getServiceType(), Collections.emptyMap(), getRoleType(), roleConfig));
+  }
+
+  @Test
+  public void testServiceModelMetadata() {
+    final Map<String, String> serviceConfig = Collections.emptyMap();
+
+    final Map<String, String> roleConfig = new HashMap<>();
+    roleConfig.put(HiveOnTezServiceModelGenerator.SSL_ENABLED, "false");
+    roleConfig.put(HiveOnTezServiceModelGenerator.HIVEONTEZ_TRANSPORT_MODE,
+                   HiveOnTezServiceModelGenerator.TRANSPORT_MODE_HTTP);
+    roleConfig.put(HiveOnTezServiceModelGenerator.HIVEONTEZ_HTTP_PORT, "12345");
+    roleConfig.put(HiveOnTezServiceModelGenerator.SAFETY_VALVE, "null");
+
+    validateServiceModel(createServiceModel(serviceConfig, roleConfig), serviceConfig, roleConfig);
+  }
+
+  @Override
+  protected String getServiceType() {
+    return HiveOnTezServiceModelGenerator.SERVICE_TYPE;
+  }
+
+  @Override
+  protected String getRoleType() {
+    return HiveOnTezServiceModelGenerator.ROLE_TYPE;
+  }
+
+  @Override
+  protected ServiceModelGenerator newGenerator() {
+    return new HiveOnTezServiceModelGenerator();
+  }
+
+}
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveServiceModelGeneratorTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveServiceModelGeneratorTest.java
new file mode 100644
index 0000000..f811584
--- /dev/null
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveServiceModelGeneratorTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.model.hive;
+
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator;
+import org.apache.knox.gateway.topology.discovery.cm.model.AbstractServiceModelGeneratorTest;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class HiveServiceModelGeneratorTest extends AbstractServiceModelGeneratorTest {
+
+  @Test
+  @Override
+  public void testHandles() {
+    Map<String, String> roleConfig = new HashMap<>();
+    roleConfig.put(HiveServiceModelGenerator.SAFETY_VALVE,
+                   getSafetyValveConfig(HiveServiceModelGenerator.TRANSPORT_MODE_HTTP));
+    assertTrue(doTestHandles(newGenerator(), getServiceType(), Collections.emptyMap(), getRoleType(), roleConfig));
+  }
+
+  @Test
+  public void testHandlesWhenTransportModeIsBinary() {
+    Map<String, String> roleConfig = new HashMap<>();
+    roleConfig.put(HiveServiceModelGenerator.SAFETY_VALVE, getSafetyValveConfig("binary"));
+    assertFalse(doTestHandles(newGenerator(), getServiceType(), Collections.emptyMap(), getRoleType(), roleConfig));
+  }
+
+  @Test
+  public void testServiceModelMetadata() {
+    final Map<String, String> serviceConfig = Collections.emptyMap();
+
+    final Map<String, String> roleConfig = new HashMap<>();
+    roleConfig.put(HiveServiceModelGenerator.SSL_ENABLED, "false");
+    roleConfig.put(HiveServiceModelGenerator.SAFETY_VALVE,
+                   getSafetyValveConfig(HiveServiceModelGenerator.TRANSPORT_MODE_HTTP));
+
+    validateServiceModel(createServiceModel(serviceConfig, roleConfig), serviceConfig, roleConfig);
+  }
+
+  @Override
+  protected String getServiceType() {
+    return HiveServiceModelGenerator.SERVICE_TYPE;
+  }
+
+  @Override
+  protected String getRoleType() {
+    return HiveServiceModelGenerator.ROLE_TYPE;
+  }
+
+  @Override
+  protected ServiceModelGenerator newGenerator() {
+    return new HiveServiceModelGenerator();
+  }
+
+  private String getSafetyValveConfig(final String transportMode){
+    return "<property><name>" + HiveServiceModelGenerator.HTTP_PORT + "</name><value>12345</value></property>" +
+           "<property><name>" + HiveServiceModelGenerator.HTTP_PATH + "</name><value>cliService</value></property>" +
+           "<property><name>" + HiveServiceModelGenerator.TRANSPORT_MODE + "</name><value>" + transportMode + "</value></property>";
+  }
+}
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/hue/HueLBServiceModelGeneratorTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/hue/HueLBServiceModelGeneratorTest.java
new file mode 100644
index 0000000..f761ca5
--- /dev/null
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/hue/HueLBServiceModelGeneratorTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.model.hue;
+
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator;
+import org.apache.knox.gateway.topology.discovery.cm.model.AbstractServiceModelGeneratorTest;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class HueLBServiceModelGeneratorTest extends AbstractServiceModelGeneratorTest {
+
+  @Test
+  public void testServiceModelMetadata() {
+    final Map<String, String> serviceConfig = Collections.emptyMap();
+    final Map<String, String> roleConfig = new HashMap<>();
+    roleConfig.put(HueLBServiceModelGenerator.LISTEN_PORT, "12345");
+
+    validateServiceModel(createServiceModel(serviceConfig, roleConfig), serviceConfig, roleConfig);
+  }
+
+  @Override
+  protected String getServiceType() {
+    return HueLBServiceModelGenerator.SERVICE_TYPE;
+  }
+
+  @Override
+  protected String getRoleType() {
+    return HueLBServiceModelGenerator.ROLE_TYPE;
+  }
+
+  @Override
+  protected ServiceModelGenerator newGenerator() {
+    return new HueLBServiceModelGenerator();
+  }
+
+}
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/hue/HueServiceModelGeneratorTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/hue/HueServiceModelGeneratorTest.java
new file mode 100644
index 0000000..6f9c583
--- /dev/null
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/hue/HueServiceModelGeneratorTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.model.hue;
+
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator;
+import org.apache.knox.gateway.topology.discovery.cm.model.AbstractServiceModelGeneratorTest;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class HueServiceModelGeneratorTest extends AbstractServiceModelGeneratorTest {
+
+  @Test
+  public void testServiceModelMetadata() {
+    final Map<String, String> serviceConfig = Collections.emptyMap();
+    final Map<String, String> roleConfig = new HashMap<>();
+    roleConfig.put(HueServiceModelGenerator.SSL_ENABLED, "false");
+    roleConfig.put(HueServiceModelGenerator.HUE_HTTP_PORT, "2468");
+
+    validateServiceModel(createServiceModel(serviceConfig, roleConfig), serviceConfig, roleConfig);
+  }
+
+  @Override
+  protected String getServiceType() {
+    return HueServiceModelGenerator.SERVICE_TYPE;
+  }
+
+  @Override
+  protected String getRoleType() {
+    return HueServiceModelGenerator.ROLE_TYPE;
+  }
+
+  @Override
+  protected ServiceModelGenerator newGenerator() {
+    return new HueServiceModelGenerator();
+  }
+
+}
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/impala/ImpalaServiceModelGeneratorTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/impala/ImpalaServiceModelGeneratorTest.java
new file mode 100644
index 0000000..4f3ac49
--- /dev/null
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/impala/ImpalaServiceModelGeneratorTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.model.impala;
+
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator;
+import org.apache.knox.gateway.topology.discovery.cm.model.AbstractServiceModelGeneratorTest;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class ImpalaServiceModelGeneratorTest extends AbstractServiceModelGeneratorTest {
+
+  @Test
+  public void testServiceModelMetadata() {
+    final Map<String, String> serviceConfig = new HashMap<>();
+    serviceConfig.put(ImpalaServiceModelGenerator.SSL_ENABLED, "false");
+    final Map<String, String> roleConfig = new HashMap<>();
+    roleConfig.put(ImpalaServiceModelGenerator.HTTP_PORT, "1357");
+
+    validateServiceModel(createServiceModel(serviceConfig, roleConfig), serviceConfig, roleConfig);
+  }
+
+  @Override
+  protected String getServiceType() {
+    return ImpalaServiceModelGenerator.SERVICE_TYPE;
+  }
+
+  @Override
+  protected String getRoleType() {
+    return ImpalaServiceModelGenerator.ROLE_TYPE;
+  }
+
+  @Override
+  protected ServiceModelGenerator newGenerator() {
+    return new ImpalaServiceModelGenerator();
+  }
+
+}
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/impala/ImpalaUIServiceModelGeneratorTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/impala/ImpalaUIServiceModelGeneratorTest.java
new file mode 100644
index 0000000..1fb2f5e
--- /dev/null
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/impala/ImpalaUIServiceModelGeneratorTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.model.impala;
+
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator;
+import org.apache.knox.gateway.topology.discovery.cm.model.AbstractServiceModelGeneratorTest;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class ImpalaUIServiceModelGeneratorTest extends AbstractServiceModelGeneratorTest {
+
+  @Test
+  @Override
+  public void testHandles() {
+    Map<String, String> roleConfig = new HashMap<>();
+    roleConfig.put(ImpalaUIServiceModelGenerator.ENABLE_WEBSERVER, "true");
+    assertTrue(doTestHandles(newGenerator(), getServiceType(), Collections.emptyMap(), getRoleType(), roleConfig));
+  }
+
+  @Test
+  public void testHandlesDisabled() {
+    Map<String, String> roleConfig = new HashMap<>();
+    roleConfig.put(ImpalaUIServiceModelGenerator.ENABLE_WEBSERVER, "false");
+    assertFalse(doTestHandles(newGenerator(), getServiceType(), Collections.emptyMap(), getRoleType(), roleConfig));
+  }
+
+  @Test
+  public void testServiceModelMetadata() {
+    final Map<String, String> serviceConfig = new HashMap<>();
+    serviceConfig.put(ImpalaUIServiceModelGenerator.SSL_ENABLED, "false");
+    final Map<String, String> roleConfig = new HashMap<>();
+    roleConfig.put(ImpalaUIServiceModelGenerator.ENABLE_WEBSERVER, "true");
+    roleConfig.put(ImpalaUIServiceModelGenerator.WEBSERVER_PORT, "1357");
+
+    validateServiceModel(createServiceModel(serviceConfig, roleConfig), serviceConfig, roleConfig);
+  }
+
+  @Override
+  protected String getServiceType() {
+    return ImpalaUIServiceModelGenerator.SERVICE_TYPE;
+  }
+
+  @Override
+  protected String getRoleType() {
+    return ImpalaUIServiceModelGenerator.ROLE_TYPE;
+  }
+
+  @Override
+  protected ServiceModelGenerator newGenerator() {
+    return new ImpalaUIServiceModelGenerator();
+  }
+
+}
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/kudu/KuduUIServiceModelGeneratorTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/kudu/KuduUIServiceModelGeneratorTest.java
new file mode 100644
index 0000000..de4f977
--- /dev/null
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/kudu/KuduUIServiceModelGeneratorTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.model.kudu;
+
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator;
+import org.apache.knox.gateway.topology.discovery.cm.model.AbstractServiceModelGeneratorTest;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class KuduUIServiceModelGeneratorTest extends AbstractServiceModelGeneratorTest {
+
+  @Test
+  public void testServiceModelMetadata() {
+    final Map<String, String> serviceConfig = Collections.emptyMap();
+    final Map<String, String> roleConfig = new HashMap<>();
+    roleConfig.put(KuduUIServiceModelGenerator.SSL_ENABLED, "false");
+    roleConfig.put(KuduUIServiceModelGenerator.WEBSERVER_PORT, "1357");
+
+    validateServiceModel(createServiceModel(serviceConfig, roleConfig), serviceConfig, roleConfig);
+  }
+
+
+  @Override
+  protected String getServiceType() {
+    return KuduUIServiceModelGenerator.SERVICE_TYPE;
+  }
+
+  @Override
+  protected String getRoleType() {
+    return KuduUIServiceModelGenerator.ROLE_TYPE;
+  }
+
+  @Override
+  protected ServiceModelGenerator newGenerator() {
+    return new KuduUIServiceModelGenerator();
+  }
+
+}
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/livy/LivyServiceModelGeneratorTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/livy/LivyServiceModelGeneratorTest.java
new file mode 100644
index 0000000..12c57cc
--- /dev/null
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/livy/LivyServiceModelGeneratorTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.model.livy;
+
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator;
+import org.apache.knox.gateway.topology.discovery.cm.model.AbstractServiceModelGeneratorTest;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class LivyServiceModelGeneratorTest extends AbstractServiceModelGeneratorTest {
+
+  @Test
+  public void testServiceModelMetadata() {
+    final Map<String, String> serviceConfig = Collections.emptyMap();
+    final Map<String, String> roleConfig = new HashMap<>();
+    roleConfig.put(LivyServiceModelGenerator.SSL_ENABLED, "false");
+    roleConfig.put(LivyServiceModelGenerator.LIVY_SERVER_PORT, "1357");
+
+    validateServiceModel(createServiceModel(serviceConfig, roleConfig), serviceConfig, roleConfig);
+  }
+
+  @Override
+  protected String getServiceType() {
+    return LivyServiceModelGenerator.SERVICE_TYPE;
+  }
+
+  @Override
+  protected String getRoleType() {
+    return LivyServiceModelGenerator.ROLE_TYPE;
+  }
+
+  @Override
+  protected ServiceModelGenerator newGenerator() {
+    return new LivyServiceModelGenerator();
+  }
+
+}
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/nifi/NifiRegistryServiceModelGeneratorTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/nifi/NifiRegistryServiceModelGeneratorTest.java
new file mode 100644
index 0000000..6b15df2
--- /dev/null
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/nifi/NifiRegistryServiceModelGeneratorTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.model.nifi;
+
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator;
+import org.apache.knox.gateway.topology.discovery.cm.model.AbstractServiceModelGeneratorTest;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class NifiRegistryServiceModelGeneratorTest extends AbstractServiceModelGeneratorTest {
+
+  @Test
+  public void testServiceModelMetadata() {
+    final Map<String, String> serviceConfig = Collections.emptyMap();
+    final Map<String, String> roleConfig = new HashMap<>();
+    roleConfig.put(NifiRegistryServiceModelGenerator.SSL_ENABLED, "false");
+    roleConfig.put(NifiRegistryServiceModelGenerator.HTTP_PORT, "12345");
+    roleConfig.put(NifiRegistryServiceModelGenerator.HTTPS_PORT, "13579");
+
+    validateServiceModel(createServiceModel(serviceConfig, roleConfig), serviceConfig, roleConfig);
+  }
+
+  @Override
+  protected String getServiceType() {
+    return NifiRegistryServiceModelGenerator.SERVICE_TYPE;
+  }
+
+  @Override
+  protected String getRoleType() {
+    return NifiRegistryServiceModelGenerator.ROLE_TYPE;
+  }
+
+  @Override
+  protected ServiceModelGenerator newGenerator() {
+    return new NifiRegistryServiceModelGenerator();
+  }
+
+}
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/nifi/NifiServiceModelGeneratorTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/nifi/NifiServiceModelGeneratorTest.java
new file mode 100644
index 0000000..42d4aa0
--- /dev/null
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/nifi/NifiServiceModelGeneratorTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.model.nifi;
+
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator;
+import org.apache.knox.gateway.topology.discovery.cm.model.AbstractServiceModelGeneratorTest;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class NifiServiceModelGeneratorTest extends AbstractServiceModelGeneratorTest {
+
+  @Test
+  public void testServiceModelMetadata() {
+    final Map<String, String> serviceConfig = Collections.emptyMap();
+    final Map<String, String> roleConfig = new HashMap<>();
+    roleConfig.put(NifiServiceModelGenerator.SSL_ENABLED, "false");
+    roleConfig.put(NifiServiceModelGenerator.HTTP_PORT, "2468");
+    roleConfig.put(NifiServiceModelGenerator.HTTPS_PORT, "1357");
+
+    validateServiceModel(createServiceModel(serviceConfig, roleConfig), serviceConfig, roleConfig);
+  }
+
+  @Override
+  protected String getServiceType() {
+    return NifiServiceModelGenerator.SERVICE_TYPE;
+  }
+
+  @Override
+  protected String getRoleType() {
+    return NifiServiceModelGenerator.ROLE_TYPE;
+  }
+
+  @Override
+  protected ServiceModelGenerator newGenerator() {
+    return new NifiServiceModelGenerator();
+  }
+}
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/oozie/OozieServiceModelGeneratorTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/oozie/OozieServiceModelGeneratorTest.java
new file mode 100644
index 0000000..4e74708
--- /dev/null
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/oozie/OozieServiceModelGeneratorTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.model.oozie;
+
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator;
+import org.apache.knox.gateway.topology.discovery.cm.model.AbstractServiceModelGeneratorTest;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class OozieServiceModelGeneratorTest extends AbstractServiceModelGeneratorTest {
+
+  @Test
+  public void testServiceModelMetadata() {
+    final Map<String, String> serviceConfig = new HashMap<>();
+    serviceConfig.put(OozieServiceModelGenerator.USE_SSL, "false");
+    final Map<String, String> roleConfig = new HashMap<>();
+    roleConfig.put(OozieServiceModelGenerator.HTTP_PORT, "2468");
+    roleConfig.put(OozieServiceModelGenerator.HTTPS_PORT, "1357");
+
+    validateServiceModel(createServiceModel(serviceConfig, roleConfig), serviceConfig, roleConfig);
+  }
+
+  @Override
+  protected String getServiceType() {
+    return OozieServiceModelGenerator.SERVICE_TYPE;
+  }
+
+  @Override
+  protected String getRoleType() {
+    return OozieServiceModelGenerator.ROLE_TYPE;
+  }
+
+  @Override
+  protected ServiceModelGenerator newGenerator() {
+    return new OozieServiceModelGenerator();
+  }
+
+}
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveOnTezServiceModelGenerator.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/oozie/OozieUIServiceModelGeneratorTest.java
similarity index 61%
copy from gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveOnTezServiceModelGenerator.java
copy to gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/oozie/OozieUIServiceModelGeneratorTest.java
index 276daec..fcc073a 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveOnTezServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/oozie/OozieUIServiceModelGeneratorTest.java
@@ -14,22 +14,25 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package org.apache.knox.gateway.topology.discovery.cm.model.hive;
+package org.apache.knox.gateway.topology.discovery.cm.model.oozie;
 
-import com.cloudera.api.swagger.model.ApiConfigList;
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator;
 
-public class HiveOnTezServiceModelGenerator extends HiveServiceModelGenerator {
+public class OozieUIServiceModelGeneratorTest extends OozieServiceModelGeneratorTest {
 
-  public static final String SERVICE_TYPE = "HIVE_ON_TEZ";
+  @Override
+  protected String getServiceType() {
+    return OozieUIServiceModelGenerator.SERVICE_TYPE;
+  }
 
   @Override
-  public String getServiceType() {
-    return SERVICE_TYPE;
+  protected String getRoleType() {
+    return OozieUIServiceModelGenerator.ROLE_TYPE;
   }
 
   @Override
-  protected boolean checkHiveServer2HTTPMode(ApiConfigList roleConfig) {
-    return TRANSPORT_MODE_HTTP.equals(getRoleConfigValue(roleConfig, "hive_server2_transport_mode"));
+  protected ServiceModelGenerator newGenerator() {
+    return new OozieUIServiceModelGenerator();
   }
 
 }
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/phoenix/PhoenixServiceModelGeneratorTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/phoenix/PhoenixServiceModelGeneratorTest.java
new file mode 100644
index 0000000..8bd3c6c
--- /dev/null
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/phoenix/PhoenixServiceModelGeneratorTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.model.phoenix;
+
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator;
+import org.apache.knox.gateway.topology.discovery.cm.model.AbstractServiceModelGeneratorTest;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class PhoenixServiceModelGeneratorTest extends AbstractServiceModelGeneratorTest {
+
+  @Test
+  public void testServiceModelMetadata() {
+    final Map<String, String> serviceConfig = Collections.emptyMap();
+    final Map<String, String> roleConfig = new HashMap<>();
+    roleConfig.put(PhoenixServiceModelGenerator.QUERY_SERVER_PORT, "12345");
+
+    validateServiceModel(createServiceModel(serviceConfig, roleConfig), serviceConfig, roleConfig);
+  }
+
+  @Override
+  protected String getServiceType() {
+    return PhoenixServiceModelGenerator.SERVICE_TYPE;
+  }
+
+  @Override
+  protected String getRoleType() {
+    return PhoenixServiceModelGenerator.ROLE_TYPE;
+  }
+
+  @Override
+  protected ServiceModelGenerator newGenerator() {
+    return new PhoenixServiceModelGenerator();
+  }
+
+}
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/ranger/RangerServiceModelGeneratorTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/ranger/RangerServiceModelGeneratorTest.java
new file mode 100644
index 0000000..db0f395
--- /dev/null
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/ranger/RangerServiceModelGeneratorTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.model.ranger;
+
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator;
+import org.apache.knox.gateway.topology.discovery.cm.model.AbstractServiceModelGeneratorTest;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class RangerServiceModelGeneratorTest extends AbstractServiceModelGeneratorTest {
+
+  @Test
+  public void testServiceModelMetadata() {
+    final Map<String, String> serviceConfig = new HashMap<>();
+    serviceConfig.put(RangerServiceModelGenerator.HTTP_PORT, "2468");
+    serviceConfig.put(RangerServiceModelGenerator.HTTPS_PORT, "1357");
+    final Map<String, String> roleConfig = new HashMap<>();
+    roleConfig.put(RangerServiceModelGenerator.SSL_ENABLED, "false");
+
+    validateServiceModel(createServiceModel(serviceConfig, roleConfig), serviceConfig, roleConfig);
+  }
+
+  @Override
+  protected String getServiceType() {
+    return RangerServiceModelGenerator.SERVICE_TYPE;
+  }
+
+  @Override
+  protected String getRoleType() {
+    return RangerServiceModelGenerator.ROLE_TYPE;
+  }
+
+  @Override
+  protected ServiceModelGenerator newGenerator() {
+    return new RangerServiceModelGenerator();
+  }
+
+}
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveOnTezServiceModelGenerator.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/ranger/RangerUIServiceModelGeneratorTest.java
similarity index 61%
copy from gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveOnTezServiceModelGenerator.java
copy to gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/ranger/RangerUIServiceModelGeneratorTest.java
index 276daec..2b13d18 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveOnTezServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/ranger/RangerUIServiceModelGeneratorTest.java
@@ -14,22 +14,25 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package org.apache.knox.gateway.topology.discovery.cm.model.hive;
+package org.apache.knox.gateway.topology.discovery.cm.model.ranger;
 
-import com.cloudera.api.swagger.model.ApiConfigList;
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator;
 
-public class HiveOnTezServiceModelGenerator extends HiveServiceModelGenerator {
+public class RangerUIServiceModelGeneratorTest extends RangerServiceModelGeneratorTest {
 
-  public static final String SERVICE_TYPE = "HIVE_ON_TEZ";
+  @Override
+  protected String getServiceType() {
+    return RangerUIServiceModelGenerator.SERVICE_TYPE;
+  }
 
   @Override
-  public String getServiceType() {
-    return SERVICE_TYPE;
+  protected String getRoleType() {
+    return RangerUIServiceModelGenerator.ROLE_TYPE;
   }
 
   @Override
-  protected boolean checkHiveServer2HTTPMode(ApiConfigList roleConfig) {
-    return TRANSPORT_MODE_HTTP.equals(getRoleConfigValue(roleConfig, "hive_server2_transport_mode"));
+  protected ServiceModelGenerator newGenerator() {
+    return new RangerUIServiceModelGenerator();
   }
 
 }
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/solr/SolrServiceModelGeneratorTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/solr/SolrServiceModelGeneratorTest.java
new file mode 100644
index 0000000..e1eac9d
--- /dev/null
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/solr/SolrServiceModelGeneratorTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.model.solr;
+
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator;
+import org.apache.knox.gateway.topology.discovery.cm.model.AbstractServiceModelGeneratorTest;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class SolrServiceModelGeneratorTest extends AbstractServiceModelGeneratorTest {
+
+  @Test
+  public void testServiceModelMetadata() {
+    final Map<String, String> serviceConfig = new HashMap<>();
+    serviceConfig.put(SolrServiceModelGenerator.USE_SSL, "false");
+    final Map<String, String> roleConfig = new HashMap<>();
+    roleConfig.put(SolrServiceModelGenerator.HTTP_PORT, "2468");
+    roleConfig.put(SolrServiceModelGenerator.HTTPS_PORT, "1357");
+
+    validateServiceModel(createServiceModel(serviceConfig, roleConfig), serviceConfig, roleConfig);
+  }
+
+  @Override
+  protected String getServiceType() {
+    return SolrServiceModelGenerator.SERVICE_TYPE;
+  }
+
+  @Override
+  protected String getRoleType() {
+    return SolrServiceModelGenerator.ROLE_TYPE;
+  }
+
+  @Override
+  protected ServiceModelGenerator newGenerator() {
+    return new SolrServiceModelGenerator();
+  }
+
+}
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/spark/SparkHistoryUIServiceModelGeneratorTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/spark/SparkHistoryUIServiceModelGeneratorTest.java
new file mode 100644
index 0000000..0c22e1d
--- /dev/null
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/spark/SparkHistoryUIServiceModelGeneratorTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.model.spark;
+
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator;
+import org.apache.knox.gateway.topology.discovery.cm.model.AbstractServiceModelGeneratorTest;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class SparkHistoryUIServiceModelGeneratorTest extends AbstractServiceModelGeneratorTest {
+
+  @Test
+  public void testServiceModelMetadata() {
+    final Map<String, String> serviceConfig = Collections.emptyMap();
+    final Map<String, String> roleConfig = new HashMap<>();
+    roleConfig.put(SparkHistoryUIServiceModelGenerator.SSL_ENABLED, "false");
+    roleConfig.put(SparkHistoryUIServiceModelGenerator.HISTORY_SERVER_PORT, "2468");
+    roleConfig.put(SparkHistoryUIServiceModelGenerator.SSL_SERVER_PORT, "1357");
+
+    validateServiceModel(createServiceModel(serviceConfig, roleConfig), serviceConfig, roleConfig);
+  }
+
+  @Override
+  protected String getServiceType() {
+    return SparkHistoryUIServiceModelGenerator.SERVICE_TYPE;
+  }
+
+  @Override
+  protected String getRoleType() {
+    return SparkHistoryUIServiceModelGenerator.ROLE_TYPE;
+  }
+
+  @Override
+  protected ServiceModelGenerator newGenerator() {
+    return new SparkHistoryUIServiceModelGenerator();
+  }
+
+}
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/zeppelin/ZeppelinServiceModelGeneratorTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/zeppelin/ZeppelinServiceModelGeneratorTest.java
new file mode 100644
index 0000000..5383a75
--- /dev/null
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/zeppelin/ZeppelinServiceModelGeneratorTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.model.zeppelin;
+
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator;
+import org.apache.knox.gateway.topology.discovery.cm.model.AbstractServiceModelGeneratorTest;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class ZeppelinServiceModelGeneratorTest extends AbstractServiceModelGeneratorTest {
+
+  @Test
+  public void testServiceModelMetadata() {
+    final Map<String, String> serviceConfig = Collections.emptyMap();
+    final Map<String, String> roleConfig = new HashMap<>();
+    roleConfig.put(ZeppelinServiceModelGenerator.SSL_ENABLED, "false");
+    roleConfig.put(ZeppelinServiceModelGenerator.SERVER_PORT, "2468");
+    roleConfig.put(ZeppelinServiceModelGenerator.SERVER_SSL_PORT, "1357");
+
+    validateServiceModel(createServiceModel(serviceConfig, roleConfig), serviceConfig, roleConfig);
+  }
+
+  @Override
+  protected String getServiceType() {
+    return ZeppelinServiceModelGenerator.SERVICE_TYPE;
+  }
+
+  @Override
+  protected String getRoleType() {
+    return ZeppelinServiceModelGenerator.ROLE_TYPE;
+  }
+
+  @Override
+  protected ServiceModelGenerator newGenerator() {
+    return new ZeppelinServiceModelGenerator();
+  }
+
+}
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveOnTezServiceModelGenerator.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/zeppelin/ZeppelinUIServiceModelGeneratorTest.java
similarity index 60%
copy from gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveOnTezServiceModelGenerator.java
copy to gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/zeppelin/ZeppelinUIServiceModelGeneratorTest.java
index 276daec..3104bf8 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveOnTezServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/zeppelin/ZeppelinUIServiceModelGeneratorTest.java
@@ -14,22 +14,24 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package org.apache.knox.gateway.topology.discovery.cm.model.hive;
+package org.apache.knox.gateway.topology.discovery.cm.model.zeppelin;
 
-import com.cloudera.api.swagger.model.ApiConfigList;
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator;
 
-public class HiveOnTezServiceModelGenerator extends HiveServiceModelGenerator {
-
-  public static final String SERVICE_TYPE = "HIVE_ON_TEZ";
+public class ZeppelinUIServiceModelGeneratorTest extends ZeppelinServiceModelGeneratorTest {
 
   @Override
-  public String getServiceType() {
-    return SERVICE_TYPE;
+  protected String getServiceType() {
+    return ZeppelinUIServiceModelGenerator.SERVICE_TYPE;
   }
 
   @Override
-  protected boolean checkHiveServer2HTTPMode(ApiConfigList roleConfig) {
-    return TRANSPORT_MODE_HTTP.equals(getRoleConfigValue(roleConfig, "hive_server2_transport_mode"));
+  protected String getRoleType() {
+    return ZeppelinUIServiceModelGenerator.ROLE_TYPE;
   }
 
+  @Override
+  protected ServiceModelGenerator newGenerator() {
+    return new ZeppelinUIServiceModelGenerator();
+  }
 }
diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveOnTezServiceModelGenerator.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/zeppelin/ZeppelinWSServiceModelGeneratorTest.java
similarity index 60%
copy from gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveOnTezServiceModelGenerator.java
copy to gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/zeppelin/ZeppelinWSServiceModelGeneratorTest.java
index 276daec..308c707 100644
--- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/model/hive/HiveOnTezServiceModelGenerator.java
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/model/zeppelin/ZeppelinWSServiceModelGeneratorTest.java
@@ -14,22 +14,25 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package org.apache.knox.gateway.topology.discovery.cm.model.hive;
+package org.apache.knox.gateway.topology.discovery.cm.model.zeppelin;
 
-import com.cloudera.api.swagger.model.ApiConfigList;
+import org.apache.knox.gateway.topology.discovery.cm.ServiceModelGenerator;
 
-public class HiveOnTezServiceModelGenerator extends HiveServiceModelGenerator {
+public class ZeppelinWSServiceModelGeneratorTest extends ZeppelinServiceModelGeneratorTest {
 
-  public static final String SERVICE_TYPE = "HIVE_ON_TEZ";
+  @Override
+  protected String getServiceType() {
+    return ZeppelinWSServiceModelGenerator.SERVICE_TYPE;
+  }
 
   @Override
-  public String getServiceType() {
-    return SERVICE_TYPE;
+  protected String getRoleType() {
+    return ZeppelinWSServiceModelGenerator.ROLE_TYPE;
   }
 
   @Override
-  protected boolean checkHiveServer2HTTPMode(ApiConfigList roleConfig) {
-    return TRANSPORT_MODE_HTTP.equals(getRoleConfigValue(roleConfig, "hive_server2_transport_mode"));
+  protected ServiceModelGenerator newGenerator() {
+    return new ZeppelinWSServiceModelGenerator();
   }
 
 }
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/monitor/AbstractConfigurationStoreTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/monitor/AbstractConfigurationStoreTest.java
new file mode 100644
index 0000000..374066d
--- /dev/null
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/monitor/AbstractConfigurationStoreTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.monitor;
+
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.Assert.fail;
+
+public abstract class AbstractConfigurationStoreTest {
+
+  @Rule
+  public TemporaryFolder TEMP_DIR = new TemporaryFolder();
+
+  protected File DATA_DIR;
+
+  @Before
+  public void createDataDir() {
+    try {
+      DATA_DIR = TEMP_DIR.newFolder("data");
+    } catch (IOException e) {
+      fail(e.getMessage());
+    }
+  }
+
+  protected GatewayConfig createGatewayConfig() {
+    GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class);
+    EasyMock.expect(config.getGatewayDataDir()).andReturn(DATA_DIR.getAbsolutePath()).anyTimes();
+    EasyMock.replay(config);
+    return config;
+  }
+
+  protected List<File> listFiles(final File dataDirectory) {
+    File[] files = (new File(dataDirectory, AbstractConfigurationStore.CLUSTERS_DATA_DIR_NAME)).listFiles();
+    return (files != null ? Arrays.asList(files) : Collections.emptyList());
+  }
+
+}
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/monitor/ClusterConfigurationFileStoreTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/monitor/ClusterConfigurationFileStoreTest.java
new file mode 100644
index 0000000..f93898f
--- /dev/null
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/monitor/ClusterConfigurationFileStoreTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.monitor;
+
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+public class ClusterConfigurationFileStoreTest extends AbstractConfigurationStoreTest {
+
+  @Test
+  public void testPersistenceWithNoConfigs() {
+    final String cmAddress = "http://myhost:1234/";
+    final String cluster   = "My Cluster";
+
+    ClusterConfigurationStore configStore = new ClusterConfigurationFileStore(createGatewayConfig());
+
+    assertEquals("Expecting empty data directory initially.", 0, listFiles(DATA_DIR).size());
+    assertEquals("Expecting no results since data directory is empty.", 0, configStore.getAll().size());
+
+    configStore.store(cmAddress, cluster, Collections.emptyMap());
+
+    assertEquals("Expecting empty data directory initially.", 1, listFiles(DATA_DIR).size());
+    assertEquals("Expecting no results since data directory is empty.", 1, configStore.getAll().size());
+
+    ServiceConfigurationRecord record = configStore.get(cmAddress, cluster);
+    assertNotNull(record);
+    Map<String, ServiceConfigurationModel> configs = record.getConfigs();
+    assertNotNull(configs);
+    assertTrue(configs.isEmpty());
+  }
+
+
+  @Test
+  public void testPersistence() {
+    ClusterConfigurationStore configStore = new ClusterConfigurationFileStore(createGatewayConfig());
+
+    assertEquals("Expecting empty data directory initially.", 0, listFiles(DATA_DIR).size());
+    assertEquals("Expecting no results since data directory is empty.", 0, configStore.getAll().size());
+
+    // Construct a configuration model
+    final String address = "http://cmhost:9765/";
+    final String cluster = "Cluster X";
+
+    Map<String, ServiceConfigurationModel> configModels = new HashMap<>();
+    ServiceConfigurationModel model = new ServiceConfigurationModel();
+    model.addServiceProperty("s_prop_1", "s_prop_1-value");
+    model.addServiceProperty("s_prop_2", "s_prop_2-value");
+    model.addServiceProperty("s_prop_3", "s_prop_3-value");
+
+    model.addRoleProperty("ROLE_1", "r_prop_1", "r_prop_1-value");
+    model.addRoleProperty("ROLE_2", "r_prop_1", "r_prop_1-value");
+    model.addRoleProperty("ROLE_2", "r_prop_2", "r_prop_2-value");
+    model.addRoleProperty("ROLE_2", "r_prop_3", "r_prop_3-value");
+    model.addRoleProperty("ROLE_3", "r_prop_1", "r_prop_1-value");
+    model.addRoleProperty("ROLE_3", "r_prop_2", "r_prop_2-value");
+    configModels.put("MY_SERVICE", model);
+
+    configStore.store(address, cluster, configModels);
+    assertEquals("Expected a new file in the data directory.", 1, listFiles(DATA_DIR).size());
+
+    // Try to get a record for an unknown cluster
+    ServiceConfigurationRecord record = configStore.get("http://unknown-host", cluster);
+    assertNull("Unexpected record for an unknown cluster.", record);
+
+    record = configStore.get(address, cluster);
+    assertNotNull(record);
+    assertEquals(address, record.getDiscoveryAddress());
+    assertEquals(cluster, record.getClusterName());
+    Map<String, ServiceConfigurationModel> reloadedConfigs = record.getConfigs();
+    assertNotNull(reloadedConfigs);
+    assertFalse(reloadedConfigs.isEmpty());
+
+    // Validate service config model
+    validateModel(model, reloadedConfigs.get("MY_SERVICE"));
+
+    Set<ServiceConfigurationRecord> allRecords = configStore.getAll();
+    assertEquals(1, allRecords.size());
+    validateModel(model, allRecords.iterator().next().getConfigs().get("MY_SERVICE"));
+
+    configStore.remove(address, cluster);
+    assertEquals("Expected no files in the data directory.", 0, listFiles(DATA_DIR).size());
+  }
+
+
+  private void validateModel(final ServiceConfigurationModel original, final ServiceConfigurationModel candidate) {
+    assertNotNull(candidate);
+
+    // Compare service properties
+    Map<String, String> reloadedServiceProps = candidate.getServiceProps();
+    assertNotNull(reloadedServiceProps);
+    assertEquals(original.getServiceProps().size(), reloadedServiceProps.size());
+    for (Map.Entry<String, String> prop : original.getServiceProps().entrySet()) {
+      assertEquals(prop.getValue(), reloadedServiceProps.get(prop.getKey()));
+    }
+
+    // Compare the role properties
+    assertEquals(original.getRoleTypes().size(), candidate.getRoleTypes().size());
+    for (Map.Entry<String, Map<String, String>> entry : original.getRoleProps().entrySet()) {
+      Map<String, String> candidateProps = candidate.getRoleProps(entry.getKey());
+      for (Map.Entry<String, String> prop : entry.getValue().entrySet()) {
+        assertEquals(prop.getValue(), candidateProps.get(prop.getKey()));
+      }
+    }
+  }
+
+}
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/monitor/DiscoveryConfigurationFileStoreTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/monitor/DiscoveryConfigurationFileStoreTest.java
new file mode 100644
index 0000000..c5e9373
--- /dev/null
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/monitor/DiscoveryConfigurationFileStoreTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.monitor;
+
+import org.apache.knox.gateway.topology.discovery.ServiceDiscoveryConfig;
+import org.junit.Test;
+
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+
+
+public class DiscoveryConfigurationFileStoreTest extends AbstractConfigurationStoreTest {
+
+  @Test
+  public void test() {
+    final DiscoveryConfigurationStore configStore = new DiscoveryConfigurationFileStore(createGatewayConfig());
+
+    final ServiceDiscoveryConfig original = createConfig("http://myhost:1234", "myCluster", "iam", "pwd.alias");
+
+    try {
+      // Test storage
+      configStore.store(original);
+
+      // Verify the file was persisted to disk
+      assertEquals(1, listFiles(DATA_DIR).size());
+
+      // Test retrieval
+      // Load the persisted config
+      Set<ServiceDiscoveryConfig> persistedConfigs = configStore.getAll();
+      assertNotNull(persistedConfigs);
+      assertFalse(persistedConfigs.isEmpty());
+      assertEquals(1, persistedConfigs.size());
+
+      ServiceDiscoveryConfig reloaded = persistedConfigs.iterator().next();
+      assertEquals(original.getAddress(), reloaded.getAddress());
+      assertEquals(original.getCluster(), reloaded.getCluster());
+      assertEquals(original.getUser(), reloaded.getUser());
+      assertEquals(original.getPasswordAlias(), reloaded.getPasswordAlias());
+    } finally {
+      configStore.remove(original.getAddress(), original.getCluster());
+
+      // Verify file is gone
+      assertEquals(0, listFiles(DATA_DIR).size());
+    }
+  }
+
+  @Test
+  public void testMultiple() {
+    final DiscoveryConfigurationStore configStore = new DiscoveryConfigurationFileStore(createGatewayConfig());
+
+    final ServiceDiscoveryConfig config1 = createConfig("http://myhost:1234", "Cluster 1", "iam1", "pwd.alias1");
+    final ServiceDiscoveryConfig config2 = createConfig("http://myhost:1234", "Cluster 2", "iam2", "pwd.alias2");
+
+    try {
+      // Test storage
+      configStore.store(config1);
+      configStore.store(config2);
+
+      // Verify the files were persisted to disk
+      assertEquals(2, listFiles(DATA_DIR).size());
+
+      // Test retrieval
+      // Load the persisted config
+      Set<ServiceDiscoveryConfig> persistedConfigs = configStore.getAll();
+      assertNotNull(persistedConfigs);
+      assertFalse(persistedConfigs.isEmpty());
+      assertEquals(2, persistedConfigs.size());
+
+      for (ServiceDiscoveryConfig reloaded : persistedConfigs) {
+        ServiceDiscoveryConfig original = ("Cluster 1".equals(reloaded.getCluster())) ? config1 : config2;
+        assertEquals(original.getAddress(), reloaded.getAddress());
+        assertEquals(original.getCluster(), reloaded.getCluster());
+        assertEquals(original.getUser(), reloaded.getUser());
+        assertEquals(original.getPasswordAlias(), reloaded.getPasswordAlias());
+      }
+    } finally {
+      configStore.remove(config1.getAddress(), config1.getCluster());
+      configStore.remove(config2.getAddress(), config2.getCluster());
+
+      // Validate the file removal
+      assertEquals(0, listFiles(DATA_DIR).size());
+    }
+  }
+
+  private ServiceDiscoveryConfig createConfig(final String address,
+                                              final String cluster,
+                                              final String user,
+                                              final String alias) {
+    return  new ServiceDiscoveryConfig() {
+      @Override
+      public String getAddress() {
+        return address;
+      }
+
+      @Override
+      public String getCluster() {
+        return cluster;
+      }
+
+      @Override
+      public String getUser() {
+        return user;
+      }
+
+      @Override
+      public String getPasswordAlias() {
+        return alias;
+      }
+    };
+  }
+
+}
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/monitor/PollingConfigurationAnalyzerTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/monitor/PollingConfigurationAnalyzerTest.java
new file mode 100644
index 0000000..b2ca5a7
--- /dev/null
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/monitor/PollingConfigurationAnalyzerTest.java
@@ -0,0 +1,251 @@
+/*
+ * 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.knox.gateway.topology.discovery.cm.monitor;
+
+import com.cloudera.api.swagger.client.ApiClient;
+import com.cloudera.api.swagger.model.ApiEvent;
+import com.cloudera.api.swagger.model.ApiEventAttribute;
+import com.cloudera.api.swagger.model.ApiEventCategory;
+import org.apache.knox.gateway.topology.discovery.ServiceDiscoveryConfig;
+import org.apache.knox.gateway.topology.discovery.cm.model.hdfs.NameNodeServiceModelGenerator;
+import org.easymock.EasyMock;
+import org.junit.Test;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.knox.gateway.topology.discovery.ClusterConfigurationMonitor.ConfigurationChangeListener;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+
+public class PollingConfigurationAnalyzerTest {
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testRestartEventWithWrongApiEventCategory() {
+    doTestRestartEvent(ApiEventCategory.LOG_EVENT);
+  }
+
+  @Test
+  public void testRestartEvent() {
+    doTestRestartEvent(ApiEventCategory.AUDIT_EVENT);
+  }
+
+  private void doTestRestartEvent(final ApiEventCategory category) {
+    final String clusterName = "My Cluster";
+    final String serviceType = NameNodeServiceModelGenerator.SERVICE_TYPE;
+    final String service     = NameNodeServiceModelGenerator.SERVICE;
+
+    List<ApiEventAttribute> apiEventAttrs = new ArrayList<>();
+    apiEventAttrs.add(createEventAttribute("CLUSTER", clusterName));
+    apiEventAttrs.add(createEventAttribute("SERVICE_TYPE", serviceType));
+    apiEventAttrs.add(createEventAttribute("SERVICE", service));
+    ApiEvent apiEvent = createApiEvent(category, apiEventAttrs);
+
+    PollingConfigurationAnalyzer.RestartEvent restartEvent = new PollingConfigurationAnalyzer.RestartEvent(apiEvent);
+    assertNotNull(restartEvent);
+    assertEquals(clusterName, restartEvent.getClusterName());
+    assertEquals(serviceType, restartEvent.getServiceType());
+    assertEquals(service, restartEvent.getService());
+    assertNotNull(restartEvent.getTimestamp());
+  }
+
+  @Test
+  public void testPollingConfigChangeNotificationForChangedPropertyValue() {
+    final String address = "http://host1:1234";
+    final String clusterName = "Cluster 5";
+
+    final String failoverPropertyName = "autofailover_enabled";
+    final String nsPropertyName = "dfs_federation_namenode_nameservice";
+    final String portPropertyName = "namenode_port";
+
+    // Mock the service discovery details
+    ServiceDiscoveryConfig sdc = EasyMock.createNiceMock(ServiceDiscoveryConfig.class);
+    EasyMock.expect(sdc.getCluster()).andReturn(clusterName).anyTimes();
+    EasyMock.expect(sdc.getAddress()).andReturn(address).anyTimes();
+    EasyMock.expect(sdc.getUser()).andReturn("u").anyTimes();
+    EasyMock.expect(sdc.getPasswordAlias()).andReturn("a").anyTimes();
+    EasyMock.replay(sdc);
+
+    final Map<String, List<String>> clusterNames = new HashMap<>();
+    clusterNames.put(address, Collections.singletonList(clusterName));
+
+    // Create the original ServiceConfigurationModel details
+    final Map<String, ServiceConfigurationModel> serviceConfigurationModels = new HashMap<>();
+    final Map<String, String> nnServiceConf = new HashMap<>();
+    final Map<String, Map<String, String>> nnRoleConf = new HashMap<>();
+    final Map<String, String> nnRoleProps = new HashMap<>();
+    nnRoleProps.put(failoverPropertyName, "false");
+    nnRoleProps.put(nsPropertyName, "");
+    nnRoleProps.put(portPropertyName, "54321");
+    nnRoleConf.put(NameNodeServiceModelGenerator.ROLE_TYPE, nnRoleProps);
+    serviceConfigurationModels.put(NameNodeServiceModelGenerator.SERVICE_TYPE + "-1", createModel(nnServiceConf, nnRoleConf));
+
+    // Mock a ClusterConfigurationCache for the monitor to use
+    ClusterConfigurationCache configCache = EasyMock.createNiceMock(ClusterConfigurationCache.class);
+    EasyMock.expect(configCache.getDiscoveryConfig(address, clusterName)).andReturn(sdc).anyTimes();
+    EasyMock.expect(configCache.getClusterNames()).andReturn(clusterNames).anyTimes();
+    EasyMock.expect(configCache.getClusterServiceConfigurations(address, clusterName))
+            .andReturn(serviceConfigurationModels)
+            .anyTimes();
+    EasyMock.replay(configCache);
+
+    // Create the monitor, registering a listener so we can verify that change notification works
+    ChangeListener listener = new ChangeListener();
+    TestablePollingConfigAnalyzer pca = new TestablePollingConfigAnalyzer(configCache, listener);
+    pca.setInterval(5);
+
+    // Create another version of the same ServiceConfigurationModel with a modified property value
+    ServiceConfigurationModel updatedNNModel = new ServiceConfigurationModel();
+    updatedNNModel.addRoleProperty(NameNodeServiceModelGenerator.ROLE_TYPE, failoverPropertyName, "false");
+    updatedNNModel.addRoleProperty(NameNodeServiceModelGenerator.ROLE_TYPE, nsPropertyName, "");
+    updatedNNModel.addRoleProperty(NameNodeServiceModelGenerator.ROLE_TYPE, portPropertyName, "12345");
+    pca.addCurrentServiceConfigModel(address, clusterName, NameNodeServiceModelGenerator.SERVICE_TYPE + "-1", updatedNNModel);
+
+    // Start the polling thread
+    ExecutorService pollingThreadExecutor = Executors.newSingleThreadExecutor();
+    pollingThreadExecutor.execute(pca);
+    pollingThreadExecutor.shutdown();
+
+    // Simulate a service restart event
+    List<ApiEventAttribute> restartEventAttrs = new ArrayList<>();
+    restartEventAttrs.add(createEventAttribute("CLUSTER", clusterName));
+    restartEventAttrs.add(createEventAttribute("SERVICE_TYPE", NameNodeServiceModelGenerator.SERVICE_TYPE));
+    restartEventAttrs.add(createEventAttribute("SERVICE", NameNodeServiceModelGenerator.SERVICE));
+    ApiEvent restartEvent = createApiEvent(ApiEventCategory.AUDIT_EVENT, restartEventAttrs);
+    pca.addRestartEvent(clusterName, restartEvent);
+
+    try {
+      pollingThreadExecutor.awaitTermination(15, TimeUnit.SECONDS);
+    } catch (InterruptedException e) {
+      //
+    }
+
+    // Stop the config analyzer thread
+    pca.stop();
+
+    assertTrue("Expected a change notification", listener.wasNotified(address, clusterName));
+  }
+
+
+  private ApiEvent createApiEvent(final ApiEventCategory category, final List<ApiEventAttribute> attrs) {
+    ApiEvent event = EasyMock.createNiceMock(ApiEvent.class);
+    EasyMock.expect(event.getTimeOccurred()).andReturn(Instant.now().toString()).anyTimes();
+    EasyMock.expect(event.getCategory()).andReturn(category).anyTimes();
+    EasyMock.expect(event.getAttributes()).andReturn(attrs).anyTimes();
+    EasyMock.replay(event);
+    return event;
+  }
+
+  private ApiEventAttribute createEventAttribute(final String name, final String value) {
+    ApiEventAttribute attr = EasyMock.createNiceMock(ApiEventAttribute.class);
+    EasyMock.expect(attr.getName()).andReturn(name).anyTimes();
+    EasyMock.expect(attr.getValues()).andReturn(Collections.singletonList(value)).anyTimes();
+    EasyMock.replay(attr);
+    return attr;
+  }
+
+  private ServiceConfigurationModel createModel(Map<String, String>              serviceConfig,
+                                                Map<String, Map<String, String>> roleConfig) {
+    ServiceConfigurationModel model = new ServiceConfigurationModel();
+
+    for (Map.Entry<String, String> entry : serviceConfig.entrySet()) {
+      model.addServiceProperty(entry.getKey(), entry.getValue());
+    }
+
+    for (Map.Entry<String, Map<String, String>> entry : roleConfig.entrySet()) {
+      String roleType = entry.getKey();
+      for (Map.Entry<String, String> prop : entry.getValue().entrySet()) {
+        model.addRoleProperty(roleType, prop.getKey(), prop.getValue());
+      }
+    }
+
+    return model;
+  }
+
+
+  /**
+   * PollingConfigurationAnalyzer extension to override CM API invocations.
+   */
+  private static class TestablePollingConfigAnalyzer extends PollingConfigurationAnalyzer {
+
+    private Map<String, List<ApiEvent>> restartEvents = new HashMap<>();
+    private Map<String, ServiceConfigurationModel> serviceConfigModels = new HashMap<>();
+
+    TestablePollingConfigAnalyzer(ClusterConfigurationCache cache) {
+      this(cache, null);
+    }
+
+    TestablePollingConfigAnalyzer(ClusterConfigurationCache   cache,
+                                  ConfigurationChangeListener listener) {
+      super(cache, null, listener);
+    }
+
+    TestablePollingConfigAnalyzer(ClusterConfigurationCache cache,
+                                  ConfigurationChangeListener listener,
+                                  int interval) {
+      super(cache, null, listener, interval);
+    }
+
+    void addRestartEvent(final String service, final ApiEvent restartEvent) {
+      restartEvents.computeIfAbsent(service, l -> new ArrayList<>()).add(restartEvent);
+    }
+
+    void addCurrentServiceConfigModel(final String address, final String clusterName, final String service, final ServiceConfigurationModel model) {
+      serviceConfigModels.put(getServiceConfigModelKey(address, clusterName, service), model);
+    }
+
+    @Override
+    protected List<ApiEvent> queryRestartEvents(ApiClient client, String clusterName, String since) {
+      return restartEvents.computeIfAbsent(clusterName, l -> new ArrayList<>());
+    }
+
+    @Override
+    protected ServiceConfigurationModel getCurrentServiceConfiguration(String address,
+                                                                       String clusterName,
+                                                                       String service) {
+      return serviceConfigModels.get(getServiceConfigModelKey(address, clusterName, service));
+    }
+
+    static String getServiceConfigModelKey(final String address, final String clusterName, final String service) {
+      return address + ":" + clusterName + ":" + service;
+    }
+  }
+
+
+  private static class ChangeListener implements ConfigurationChangeListener {
+    private final Map<String, String> notifications = new HashMap<>();
+
+    @Override
+    public void onConfigurationChange(String source, String clusterName) {
+      notifications.put(source, clusterName);
+    }
+
+    boolean wasNotified(final String source, final String clusterName) {
+      return clusterName.equals(notifications.get(source));
+    }
+  }
+
+}
diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/monitor/ServiceConfigurationModelTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/monitor/ServiceConfigurationModelTest.java
new file mode 100644
index 0000000..67d95d9
--- /dev/null
+++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/monitor/ServiceConfigurationModelTest.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.knox.gateway.topology.discovery.cm.monitor;
+
+import com.cloudera.api.swagger.model.ApiConfigList;
+import com.cloudera.api.swagger.model.ApiRole;
+import org.apache.knox.gateway.topology.discovery.cm.AbstractCMDiscoveryTest;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+public class ServiceConfigurationModelTest extends AbstractCMDiscoveryTest {
+
+  @Test
+  public void testEmptyServiceConfigurationModel() {
+    ServiceConfigurationModel model = new ServiceConfigurationModel();
+    validateServiceConfigurationModel(model, Collections.emptyMap(), Collections.emptyMap());
+    assertNull(model.getRoleProps("UNKNOWN_ROLE_TYPE"));
+  }
+
+  @Test
+  public void testServiceConfigurationModel() {
+    Map<String, String> serviceConfig = new HashMap<>();
+    serviceConfig.put("prop1", "value1");
+    serviceConfig.put("prop2", "value2");
+
+    Map<String, Map<String, String>> roleConfig = new HashMap<>();
+    Map<String, String> roleProps = new HashMap<>();
+    roleProps.put("prop1", "value1");
+    roleProps.put("prop2", "value2");
+    roleProps.put("prop3", "value3");
+    roleConfig.put("test1", roleProps);
+
+    roleProps = new HashMap<>();
+    roleProps.put("prop4", "value4");
+    roleProps.put("prop5", "value5");
+    roleProps.put("prop6", "value6");
+    roleConfig.put("test2", roleProps);
+
+    // Create the model
+    ServiceConfigurationModel model = createModel(serviceConfig, roleConfig);
+    assertNotNull(model);
+    assertFalse(model.getServiceProps().isEmpty());
+    assertFalse(model.getRoleProps().isEmpty());
+    assertFalse(model.getRoleTypes().isEmpty());
+
+    // Validate model contents
+    validateServiceConfigurationModel(model, serviceConfig, roleConfig);
+  }
+
+  @Test
+  public void testServiceConfigurationModelFromAPI() {
+    Map<String, String> serviceConfig = new HashMap<>();
+    serviceConfig.put("prop1", "value1");
+    serviceConfig.put("prop2", "value2");
+    serviceConfig.put("prop3", "value3");
+
+    Map<String, Map<String, String>> roleConfig = new HashMap<>();
+    Map<String, String> role1Props = new HashMap<>();
+    role1Props.put("prop1", "value1");
+    role1Props.put("prop2", "value2");
+    role1Props.put("prop3", "value3");
+    roleConfig.put("ROLE_1", role1Props);
+
+    Map<String, String> role2Props = new HashMap<>();
+    role2Props.put("prop4", "value4");
+    role2Props.put("prop5", "value5");
+    roleConfig.put("ROLE_2", role2Props);
+
+    Map<String, String> role3Props = new HashMap<>();
+    role3Props.put("prop6", "value6");
+    role3Props.put("prop7", "value7");
+    role3Props.put("prop8", "value8");
+    role3Props.put("prop9", "value9");
+    roleConfig.put("ROLE_3", role3Props);
+
+    Map<ApiRole, ApiConfigList> apiRoleConfigs = new HashMap<>();
+    for (Map.Entry<String, Map<String, String>> entry : roleConfig.entrySet()) {
+      apiRoleConfigs.put(createApiRoleMock(entry.getKey()), createApiConfigListMock(entry.getValue()));
+    }
+
+    // Create the model
+    ServiceConfigurationModel model =
+        new ServiceConfigurationModel(createApiServiceConfigMock(serviceConfig), apiRoleConfigs);
+    assertNotNull(model);
+    assertFalse(model.getServiceProps().isEmpty());
+    assertFalse(model.getRoleProps().isEmpty());
+    assertFalse(model.getRoleTypes().isEmpty());
+
+    // Validate model contents
+    validateServiceConfigurationModel(model, serviceConfig, roleConfig);
+  }
+
+
+  private void validateServiceConfigurationModel(final ServiceConfigurationModel        model,
+                                                 final Map<String, String>              expectedServiceConfig,
+                                                 final Map<String, Map<String, String>> expectedRoleConfig) {
+    assertNotNull(model);
+
+    Map<String, String> modelServiceProps = model.getServiceProps();
+    assertEquals(expectedServiceConfig.size(), modelServiceProps.size());
+    for (Map.Entry<String, String> entry : expectedServiceConfig.entrySet()) {
+      assertEquals(entry.getValue(), modelServiceProps.get(entry.getKey()));
+    }
+
+    assertEquals(expectedRoleConfig.size(), model.getRoleProps().size());
+    for (Map.Entry<String, Map<String, String>> entry : expectedRoleConfig.entrySet()) {
+      String roleType = entry.getKey();
+      assertEquals(expectedRoleConfig.get(roleType).size(), entry.getValue().size());
+      for (Map.Entry<String, String> prop : entry.getValue().entrySet()) {
+        assertEquals(prop.getValue(), expectedRoleConfig.get(roleType).get(prop.getKey()));
+      }
+    }
+  }
+
+
+  private ServiceConfigurationModel createModel(Map<String, String>              serviceConfig,
+                                                Map<String, Map<String, String>> roleConfig) {
+    ServiceConfigurationModel model = new ServiceConfigurationModel();
+
+    for (Map.Entry<String, String> entry : serviceConfig.entrySet()) {
+      model.addServiceProperty(entry.getKey(), entry.getValue());
+    }
+
+    for (Map.Entry<String, Map<String, String>> entry : roleConfig.entrySet()) {
+      String roleType = entry.getKey();
+      for (Map.Entry<String, String> prop : entry.getValue().entrySet()) {
+        model.addRoleProperty(roleType, prop.getKey(), prop.getValue());
+      }
+    }
+
+    return model;
+  }
+
+}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/topology/impl/DefaultTopologyService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/topology/impl/DefaultTopologyService.java
index e904daa..894d976 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/topology/impl/DefaultTopologyService.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/topology/impl/DefaultTopologyService.java
@@ -37,6 +37,7 @@ import org.apache.knox.gateway.config.GatewayConfig;
 import org.apache.knox.gateway.i18n.messages.MessagesFactory;
 import org.apache.knox.gateway.service.definition.ServiceDefinition;
 import org.apache.knox.gateway.service.definition.ServiceDefinitionChangeListener;
+import org.apache.knox.gateway.services.GatewayServices;
 import org.apache.knox.gateway.services.ServiceType;
 import org.apache.knox.gateway.services.ServiceLifecycleException;
 import org.apache.knox.gateway.services.config.client.RemoteConfigurationRegistryClient;
@@ -604,10 +605,7 @@ public class DefaultTopologyService extends FileAlterationListenerAdaptor implem
 
   @Override
   public void start() {
-    // Register a cluster configuration monitor listener for change notifications
-    ClusterConfigurationMonitorService ccms =
-                  GatewayServer.getGatewayServices().getService(ServiceType.CLUSTER_CONFIGURATION_MONITOR_SERVICE);
-    ccms.addListener(new TopologyDiscoveryTrigger(this, ccms));
+
   }
 
   @Override
@@ -619,6 +617,16 @@ public class DefaultTopologyService extends FileAlterationListenerAdaptor implem
       System.setProperty(ServiceDiscovery.CONFIG_DIR_PROPERTY, gatewayConfDir);
     }
 
+    // Register a cluster configuration monitor listener for change notifications.
+    // The cluster monitor service will start before this service, so the listener must be registered
+    // beforehand or we risk the possibility of missing configuration change notifications.
+    GatewayServices gwServices = GatewayServer.getGatewayServices();
+    if (gwServices != null) {
+      ClusterConfigurationMonitorService ccms =
+              gwServices.getService(ServiceType.CLUSTER_CONFIGURATION_MONITOR_SERVICE);
+      ccms.addListener(new TopologyDiscoveryTrigger(this, ccms));
+    }
+
     try {
       listeners  = new HashSet<>();
       topologies = new HashMap<>();
@@ -1002,7 +1010,7 @@ public class DefaultTopologyService extends FileAlterationListenerAdaptor implem
         }
 
         if (!affectedDescriptors) {
-          // If not descriptors are affected by this configuration, then clear the cache to prevent future notifications
+          // If no descriptors are affected by this configuration, then clear the cache to prevent future notifications
           ccms.clearCache(source, clusterName);
         }
       } catch (Exception e) {
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/topology/discovery/ServiceDiscoveryConfig.java b/gateway-spi/src/main/java/org/apache/knox/gateway/topology/discovery/ServiceDiscoveryConfig.java
index 7d4250f..34e96e2 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/topology/discovery/ServiceDiscoveryConfig.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/topology/discovery/ServiceDiscoveryConfig.java
@@ -29,6 +29,12 @@ public interface ServiceDiscoveryConfig {
 
     /**
      *
+     * @return The name of the cluster.
+     */
+    String getCluster();
+
+    /**
+     *
      * @return The username configured for interactions with the discovery source.
      */
     String getUser();
diff --git a/gateway-topology-simple/src/main/java/org/apache/knox/gateway/topology/discovery/DefaultServiceDiscoveryConfig.java b/gateway-topology-simple/src/main/java/org/apache/knox/gateway/topology/discovery/DefaultServiceDiscoveryConfig.java
index 52913e8..f17696d 100644
--- a/gateway-topology-simple/src/main/java/org/apache/knox/gateway/topology/discovery/DefaultServiceDiscoveryConfig.java
+++ b/gateway-topology-simple/src/main/java/org/apache/knox/gateway/topology/discovery/DefaultServiceDiscoveryConfig.java
@@ -18,6 +18,7 @@ package org.apache.knox.gateway.topology.discovery;
 
 public class DefaultServiceDiscoveryConfig implements ServiceDiscoveryConfig {
     private String address;
+    private String cluster;
     private String user;
     private String pwdAlias;
 
@@ -38,6 +39,15 @@ public class DefaultServiceDiscoveryConfig implements ServiceDiscoveryConfig {
         return address;
     }
 
+    public void setCluster(String cluster) {
+        this.cluster = cluster;
+    }
+
+    @Override
+    public String getCluster() {
+        return cluster;
+    }
+
     @Override
     public String getUser() {
         return user;
diff --git a/gateway-topology-simple/src/main/java/org/apache/knox/gateway/topology/simple/SimpleDescriptorHandler.java b/gateway-topology-simple/src/main/java/org/apache/knox/gateway/topology/simple/SimpleDescriptorHandler.java
index 5d68313..8e44005 100644
--- a/gateway-topology-simple/src/main/java/org/apache/knox/gateway/topology/simple/SimpleDescriptorHandler.java
+++ b/gateway-topology-simple/src/main/java/org/apache/knox/gateway/topology/simple/SimpleDescriptorHandler.java
@@ -199,6 +199,7 @@ public class SimpleDescriptorHandler {
 
     private static ServiceDiscovery.Cluster performDiscovery(GatewayConfig config, SimpleDescriptor desc, Service...gatewayServices) {
         DefaultServiceDiscoveryConfig sdc = new DefaultServiceDiscoveryConfig(desc.getDiscoveryAddress());
+        sdc.setCluster(desc.getCluster());
         sdc.setUser(desc.getDiscoveryUser());
         sdc.setPasswordAlias(desc.getDiscoveryPasswordAlias());
 


Mime
View raw message