You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sentry.apache.org by sp...@apache.org on 2018/05/31 03:32:34 UTC

[56/86] sentry git commit: SENTRY-2208: Refactor out Sentry service into own module from sentry-provider-db (Anthony Young-Garner, reviewed by Sergio Pena, Steve Moist, Na Li)

http://git-wip-us.apache.org/repos/asf/sentry/blob/7db84b2f/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/JaasConfiguration.java
----------------------------------------------------------------------
diff --git a/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/JaasConfiguration.java b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/JaasConfiguration.java
new file mode 100644
index 0000000..a79ce5f
--- /dev/null
+++ b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/JaasConfiguration.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.sentry.service.thrift;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
+
+/**
+ * Creates a programmatic version of a jaas.conf file.  This can be used instead of writing a jaas.conf file and setting
+ * the system property, "java.security.auth.login.config", to point to that file.  It is meant to be used for connecting to
+ * ZooKeeper.
+ * <p>
+ * example usage:
+ * JaasConfiguration.addEntry("Client", principal, keytabFile);
+ * javax.security.auth.login.Configuration.setConfiguration(JaasConfiguration.getInstance());
+ */
+public final class JaasConfiguration extends Configuration {
+  private static Map<String, AppConfigurationEntry> entries = new HashMap<String, AppConfigurationEntry>();
+  private static JaasConfiguration me = null;
+  private static final String krb5LoginModuleName;
+
+  static  {
+    if (System.getProperty("java.vendor").contains("IBM")) {
+      krb5LoginModuleName = "com.ibm.security.auth.module.Krb5LoginModule";
+    }
+    else {
+      krb5LoginModuleName = "com.sun.security.auth.module.Krb5LoginModule";
+    }
+  }
+
+  private JaasConfiguration() {
+    // don't need to do anything here but we want to make it private
+  }
+
+  /**
+   * Return the singleton.  You'd typically use it only to do this:
+   * <p>
+   * javax.security.auth.login.Configuration.setConfiguration(JaasConfiguration.getInstance());
+   *
+   * @return
+   */
+  public static Configuration getInstance() {
+    if (me == null) {
+      me = new JaasConfiguration();
+    }
+    return me;
+  }
+
+  /**
+   * Add an entry to the jaas configuration with the passed in name, principal, and keytab.  The other necessary options will be
+   * set for you.
+   *
+   * @param name The name of the entry (e.g. "Client")
+   * @param principal The principal of the user
+   * @param keytab The location of the keytab
+   */
+  public static void addEntryForKeytab(String name, String principal, String keytab) {
+    Map<String, String> options = new HashMap<String, String>();
+    options.put("keyTab", keytab);
+    options.put("principal", principal);
+    options.put("useKeyTab", "true");
+    options.put("storeKey", "true");
+    options.put("useTicketCache", "false");
+    AppConfigurationEntry entry = new AppConfigurationEntry(krb5LoginModuleName,
+        AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options);
+    entries.put(name, entry);
+  }
+
+  /**
+   * Add an entry to the jaas configuration with the passed in name. The other
+   * necessary options will be set for you.
+   *
+   * @param name The name of the entry (e.g. "Client")
+   */
+  public static void addEntryForTicketCache(String sectionName) {
+    Map<String, String> options = new HashMap<String, String>();
+    options.put("useKeyTab", "false");
+    options.put("storeKey", "false");
+    options.put("useTicketCache", "true");
+    AppConfigurationEntry entry = new AppConfigurationEntry(krb5LoginModuleName,
+        AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options);
+    entries.put(sectionName, entry);
+  }
+
+  /**
+   * Removes the specified entry.
+   *
+   * @param name  The name of the entry to remove
+   */
+  public static void removeEntry(String name) {
+    entries.remove(name);
+  }
+
+  /**
+   * Clears all entries.
+   */
+  public static void clearEntries() {
+    entries.clear();
+  }
+
+  /**
+   * Returns the entries map.
+   *
+   * @return the entries map
+   */
+  public static Map<String, AppConfigurationEntry> getEntries() {
+    return entries;
+  }
+
+  @Override
+  public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
+    return new AppConfigurationEntry[]{entries.get(name)};
+  }
+}
+

http://git-wip-us.apache.org/repos/asf/sentry/blob/7db84b2f/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/KerberosConfiguration.java
----------------------------------------------------------------------
diff --git a/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/KerberosConfiguration.java b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/KerberosConfiguration.java
new file mode 100644
index 0000000..41e4fe4
--- /dev/null
+++ b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/KerberosConfiguration.java
@@ -0,0 +1,107 @@
+/**
+ * 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.sentry.service.thrift;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.security.auth.login.AppConfigurationEntry;
+
+public class KerberosConfiguration extends javax.security.auth.login.Configuration {
+  private String principal;
+  private String keytab;
+  private boolean isInitiator;
+  private static final boolean IBM_JAVA =  System.getProperty("java.vendor").contains("IBM");
+
+  private KerberosConfiguration(String principal, File keytab,
+      boolean client) {
+    this.principal = principal;
+    this.keytab = keytab.getAbsolutePath();
+    this.isInitiator = client;
+  }
+
+  public static javax.security.auth.login.Configuration createClientConfig(String principal,
+      File keytab) {
+    return new KerberosConfiguration(principal, keytab, true);
+  }
+
+  public static javax.security.auth.login.Configuration createServerConfig(String principal,
+      File keytab) {
+    return new KerberosConfiguration(principal, keytab, false);
+  }
+
+  private static String getKrb5LoginModuleName() {
+    return (IBM_JAVA ? "com.ibm.security.auth.module.Krb5LoginModule"
+            : "com.sun.security.auth.module.Krb5LoginModule");
+  }
+
+  @Override
+  public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
+    Map<String, String> options = new HashMap<String, String>();
+
+    if (IBM_JAVA) {
+      // IBM JAVA's UseKeytab covers both keyTab and useKeyTab options
+      options.put("useKeytab",keytab.startsWith("file://") ? keytab : "file://" + keytab);
+
+      options.put("principal", principal);
+      options.put("refreshKrb5Config", "true");
+
+      // Both "initiator" and "acceptor"
+      options.put("credsType", "both");
+    } else {
+      options.put("keyTab", keytab);
+      options.put("principal", principal);
+      options.put("useKeyTab", "true");
+      options.put("storeKey", "true");
+      options.put("doNotPrompt", "true");
+      options.put("useTicketCache", "true");
+      options.put("renewTGT", "true");
+      options.put("refreshKrb5Config", "true");
+      options.put("isInitiator", Boolean.toString(isInitiator));
+    }
+
+    String ticketCache = System.getenv("KRB5CCNAME");
+    if (IBM_JAVA) {
+      // If cache is specified via env variable, it takes priority
+      if (ticketCache != null) {
+        // IBM JAVA only respects system property so copy ticket cache to system property
+        // The first value searched when "useDefaultCcache" is true.
+        System.setProperty("KRB5CCNAME", ticketCache);
+      } else {
+    	ticketCache = System.getProperty("KRB5CCNAME");
+      }
+
+      if (ticketCache != null) {
+        options.put("useDefaultCcache", "true");
+        options.put("renewTGT", "true");
+      }
+    } else {
+      if (ticketCache != null) {
+        options.put("ticketCache", ticketCache);
+      }
+    }
+    options.put("debug", "true");
+
+    return new AppConfigurationEntry[]{
+        new AppConfigurationEntry(getKrb5LoginModuleName(),
+            AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
+            options)};
+  }
+}
+

http://git-wip-us.apache.org/repos/asf/sentry/blob/7db84b2f/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/ProcessorFactory.java
----------------------------------------------------------------------
diff --git a/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/ProcessorFactory.java b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/ProcessorFactory.java
new file mode 100644
index 0000000..2a48c63
--- /dev/null
+++ b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/ProcessorFactory.java
@@ -0,0 +1,40 @@
+/**
+ * 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.sentry.service.thrift;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.sentry.provider.db.service.persistent.SentryStore;
+import org.apache.thrift.TMultiplexedProcessor;
+
+public abstract class ProcessorFactory {
+  protected final Configuration conf;
+
+  public ProcessorFactory(Configuration conf) {
+    this.conf = conf;
+  }
+
+  /**
+   * Register a Thrift processor with SentryStore.
+   * @param processor a thrift processor.
+   * @param sentryStore a {@link SentryStore}
+   * @return true if success.
+   * @throws Exception
+   */
+  public abstract boolean register(TMultiplexedProcessor processor,
+                                   SentryStore sentryStore) throws Exception;
+}

http://git-wip-us.apache.org/repos/asf/sentry/blob/7db84b2f/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/SentryHMSClient.java
----------------------------------------------------------------------
diff --git a/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/SentryHMSClient.java b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/SentryHMSClient.java
new file mode 100644
index 0000000..b9a0563
--- /dev/null
+++ b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/SentryHMSClient.java
@@ -0,0 +1,250 @@
+/*
+  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
+  <p>
+  http://www.apache.org/licenses/LICENSE-2.0
+  <p>
+  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.sentry.service.thrift;
+
+import com.codahale.metrics.Counter;
+import com.codahale.metrics.Timer;
+import com.codahale.metrics.Timer.Context;
+import com.google.common.annotations.VisibleForTesting;
+import org.apache.commons.lang.exception.ExceptionUtils;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hive.metastore.HiveMetaStoreClient;
+import org.apache.hadoop.hive.metastore.api.CurrentNotificationEventId;
+import org.apache.hadoop.hive.metastore.api.MetaException;
+import org.apache.hadoop.hive.metastore.api.NotificationEvent;
+import org.apache.hadoop.hive.metastore.api.NotificationEventResponse;
+import org.apache.hadoop.hive.metastore.messaging.MessageDeserializer;
+import org.apache.sentry.binding.metastore.messaging.json.SentryJSONMessageDeserializer;
+import org.apache.sentry.provider.db.service.persistent.PathsImage;
+import org.apache.sentry.provider.db.service.persistent.SentryStore;
+import org.apache.sentry.api.service.thrift.SentryMetrics;
+import org.apache.thrift.TException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+
+import static com.codahale.metrics.MetricRegistry.name;
+import static java.util.Collections.emptyMap;
+
+/**
+ * Wrapper class for <Code>HiveMetaStoreClient</Code>
+ *
+ * <p>Abstracts communication with HMS and exposes APi's to connect/disconnect to HMS and to
+ * request HMS snapshots and also for new notifications.
+ */
+public class SentryHMSClient implements AutoCloseable {
+
+  private static final Logger LOGGER = LoggerFactory.getLogger(SentryHMSClient.class);
+  private static final String NOT_CONNECTED_MSG = "Client is not connected to HMS";
+
+  private final Configuration conf;
+  private HiveMetaStoreClient client = null;
+  private HiveConnectionFactory hiveConnectionFactory;
+
+  private static final String SNAPSHOT = "snapshot";
+  /** Measures time to get full snapshot. */
+  private final Timer updateTimer = SentryMetrics.getInstance()
+      .getTimer(name(FullUpdateInitializer.class, SNAPSHOT));
+  /** Number of times update failed. */
+  private final Counter failedSnapshotsCount = SentryMetrics.getInstance()
+      .getCounter(name(FullUpdateInitializer.class, "failed"));
+
+  public SentryHMSClient(Configuration conf, HiveConnectionFactory hiveConnectionFactory) {
+    this.conf = conf;
+    this.hiveConnectionFactory = hiveConnectionFactory;
+  }
+
+  /**
+   * Used only for testing purposes.
+   *x
+   * @param client HiveMetaStoreClient to be initialized
+   */
+  @VisibleForTesting
+  void setClient(HiveMetaStoreClient client) {
+    this.client = client;
+  }
+
+  /**
+   * Used to know if the client is connected to HMS
+   *
+   * @return true if the client is connected to HMS false, if client is not connected.
+   */
+  boolean isConnected() {
+    return client != null;
+  }
+
+  /**
+   * Connects to HMS by creating HiveMetaStoreClient.
+   *
+   * @throws IOException          if could not establish connection
+   * @throws InterruptedException if connection was interrupted
+   * @throws MetaException        if other errors happened
+   */
+  public void connect()
+      throws IOException, InterruptedException, MetaException {
+    if (client != null) {
+      return;
+    }
+    client = hiveConnectionFactory.connect().getClient();
+  }
+
+  /**
+   * Disconnects the HMS client.
+   */
+  public void disconnect() throws Exception {
+    try {
+      if (client != null) {
+        LOGGER.info("Closing the HMS client connection");
+        client.close();
+      }
+    } catch (Exception e) {
+      LOGGER.error("failed to close Hive Connection Factory", e);
+    } finally {
+      client = null;
+    }
+  }
+
+  /**
+   * Closes the HMS client.
+   *
+   * <p>This is similar to disconnect. As this class implements AutoClosable, close should be
+   * implemented.
+   */
+  public void close() throws Exception {
+    disconnect();
+  }
+
+  /**
+   * Creates HMS full snapshot.
+   *
+   * @return Full path snapshot and the last notification id on success
+   */
+  public PathsImage getFullSnapshot() {
+    if (client == null) {
+      LOGGER.error(NOT_CONNECTED_MSG);
+      return new PathsImage(Collections.<String, Collection<String>>emptyMap(),
+          SentryStore.EMPTY_NOTIFICATION_ID, SentryStore.EMPTY_PATHS_SNAPSHOT_ID);
+    }
+
+    try {
+      CurrentNotificationEventId eventIdBefore = client.getCurrentNotificationEventId();
+      Map<String, Collection<String>> pathsFullSnapshot = fetchFullUpdate();
+      if (pathsFullSnapshot.isEmpty()) {
+        LOGGER.info("Received empty paths when getting full snapshot. NotificationID Before Snapshot: {}", eventIdBefore.getEventId());
+        return new PathsImage(pathsFullSnapshot, SentryStore.EMPTY_NOTIFICATION_ID,
+            SentryStore.EMPTY_PATHS_SNAPSHOT_ID);
+      }
+
+      CurrentNotificationEventId eventIdAfter = client.getCurrentNotificationEventId();
+      LOGGER.info("NotificationID, Before Snapshot: {}, After Snapshot {}",
+          eventIdBefore.getEventId(), eventIdAfter.getEventId());
+
+      if (eventIdAfter.equals(eventIdBefore)) {
+        LOGGER.info("Successfully fetched hive full snapshot, Current NotificationID: {}.",
+                eventIdAfter);
+        // As eventIDAfter is the last event that was processed, eventIDAfter is used to update
+        // lastProcessedNotificationID instead of getting it from persistent store.
+        return new PathsImage(pathsFullSnapshot, eventIdAfter.getEventId(),
+                SentryStore.EMPTY_PATHS_SNAPSHOT_ID);
+      }
+
+      LOGGER.info("Reconciling full snapshot - applying {} changes",
+              eventIdAfter.getEventId() - eventIdBefore.getEventId());
+
+      // While we were taking snapshot, HMS made some changes, so now we need to apply all
+      // extra events to the snapshot
+      long currentEventId = eventIdBefore.getEventId();
+      MessageDeserializer deserializer = new SentryJSONMessageDeserializer();
+
+      while (currentEventId < eventIdAfter.getEventId()) {
+        NotificationEventResponse response =
+                client.getNextNotification(currentEventId, Integer.MAX_VALUE, null);
+        if (response == null || !response.isSetEvents() || response.getEvents().isEmpty()) {
+          LOGGER.error("Snapshot discarded, updates to HMS data while shapshot is being taken."
+                  + "ID Before: {}. ID After: {}", eventIdBefore.getEventId(), eventIdAfter.getEventId());
+          return new PathsImage(Collections.<String, Collection<String>>emptyMap(),
+                  SentryStore.EMPTY_NOTIFICATION_ID, SentryStore.EMPTY_PATHS_SNAPSHOT_ID);
+        }
+
+        for (NotificationEvent event : response.getEvents()) {
+          LOGGER.info("Received event = {} currentEventId = {}, eventIdAfter = {}", event.getEventId(), currentEventId, eventIdAfter);
+          if (event.getEventId() <= eventIdBefore.getEventId()) {
+            LOGGER.error("Received stray event with eventId {} which is less then {}",
+                    event.getEventId(), eventIdBefore);
+            continue;
+          }
+          if (event.getEventId() > eventIdAfter.getEventId()) {
+            // Enough events processed
+            LOGGER.debug("Received eventId = {} is greater than eventIdAfter = {}", event.getEventId(), eventIdAfter);
+            break;
+          }
+          try {
+            FullUpdateModifier.applyEvent(pathsFullSnapshot, event, deserializer);
+          } catch (Exception e) {
+            LOGGER.warn("Failed to apply operation", e);
+          }
+
+          //Log warning message if event id increments are not sequential
+          if( event.getEventId() != (currentEventId + 1) ) {
+            LOGGER.warn("Received non-sequential event. currentEventId = {} received eventId = {} ", currentEventId, event.getEventId());
+          }
+          currentEventId = event.getEventId();
+        }
+      }
+
+      LOGGER.info("Successfully fetched hive full snapshot, Current NotificationID: {}.",
+          currentEventId);
+      // As eventIDAfter is the last event that was processed, eventIDAfter is used to update
+      // lastProcessedNotificationID instead of getting it from persistent store.
+      return new PathsImage(pathsFullSnapshot, currentEventId,
+          SentryStore.EMPTY_PATHS_SNAPSHOT_ID);
+    } catch (TException failure) {
+      LOGGER.error("Fetching a new HMS snapshot cannot continue because an error occurred during "
+          + "the HMS communication: ", failure);
+      LOGGER.error("Root Exception", ExceptionUtils.getRootCause(failure));
+      return new PathsImage(Collections.<String, Collection<String>>emptyMap(),
+          SentryStore.EMPTY_NOTIFICATION_ID, SentryStore.EMPTY_PATHS_SNAPSHOT_ID);
+    }
+  }
+
+  /**
+   * Retrieve a Hive full snapshot from HMS.
+   *
+   * @return HMS snapshot. Snapshot consists of a mapping from auth object name to the set of paths
+   *     corresponding to that name.
+   */
+  private Map<String, Collection<String>> fetchFullUpdate() {
+    LOGGER.info("Request full HMS snapshot");
+    try (FullUpdateInitializer updateInitializer =
+             new FullUpdateInitializer(hiveConnectionFactory, conf);
+         Context context = updateTimer.time()) {
+      Map<String, Collection<String>> pathsUpdate = updateInitializer.getFullHMSSnapshot();
+      LOGGER.info("Obtained full HMS snapshot");
+      return pathsUpdate;
+    } catch (Exception ignored) {
+      failedSnapshotsCount.inc();
+      LOGGER.error("Snapshot created failed ", ignored);
+      return emptyMap();
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/sentry/blob/7db84b2f/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/SentryKerberosContext.java
----------------------------------------------------------------------
diff --git a/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/SentryKerberosContext.java b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/SentryKerberosContext.java
new file mode 100644
index 0000000..efb8ae6
--- /dev/null
+++ b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/SentryKerberosContext.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.sentry.service.thrift;
+
+import java.io.File;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.ThreadFactory;
+
+import javax.security.auth.Subject;
+import javax.security.auth.kerberos.KerberosPrincipal;
+import javax.security.auth.kerberos.KerberosTicket;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.Sets;
+
+public class SentryKerberosContext implements Runnable {
+
+  private static final String KERBEROS_RENEWER_THREAD_NAME = "kerberos-renewer-%d";
+  private static final float TICKET_RENEW_WINDOW = 0.80f;
+  private static final Logger LOGGER = LoggerFactory
+      .getLogger(SentryKerberosContext.class);
+  private LoginContext loginContext;
+  private Subject subject;
+  private final javax.security.auth.login.Configuration kerberosConfig;
+  private Thread renewerThread;
+  private boolean shutDownRenewer = false;
+
+  public SentryKerberosContext(String principal, String keyTab, boolean server)
+      throws LoginException {
+    subject = new Subject(false, Sets.newHashSet(new KerberosPrincipal(principal)),
+          new HashSet<Object>(), new HashSet<Object>());
+    if(server) {
+      kerberosConfig = KerberosConfiguration.createServerConfig(principal, new File(keyTab));
+    } else {
+      kerberosConfig = KerberosConfiguration.createClientConfig(principal, new File(keyTab));
+    }
+    loginWithNewContext();
+    if (!server) {
+      startRenewerThread();
+    }
+  }
+
+  private void loginWithNewContext() throws LoginException {
+    LOGGER.info("Logging in with new Context");
+    logoutSubject();
+    loginContext = new LoginContext("", subject, null, kerberosConfig);
+    loginContext.login();
+    subject = loginContext.getSubject();
+  }
+  
+  private void logoutSubject() {
+    if (loginContext != null) {
+      try {
+        loginContext.logout();
+      } catch (LoginException e) {
+        LOGGER.warn("Error logging out the subject", e);
+      }        
+    }
+    loginContext = null;
+  }
+  
+  public Subject getSubject() {
+    return subject;
+  }
+
+  /**
+   * Get the Kerberos TGT
+   * @return the user's TGT or null if none was found
+   */
+  @Deprecated
+  private KerberosTicket getTGT() {
+    Set<KerberosTicket> tickets = subject.getPrivateCredentials(KerberosTicket.class);
+    for(KerberosTicket ticket: tickets) {
+      KerberosPrincipal server = ticket.getServer();
+      if (server.getName().equals("krbtgt/" + server.getRealm() +
+          "@" + server.getRealm())) {
+        return ticket;
+      }
+    }
+    return null;
+  }
+
+  private long getRefreshTime(KerberosTicket tgt) {
+    long start = tgt.getStartTime().getTime();
+    long end = tgt.getEndTime().getTime();
+    LOGGER.debug("Ticket start time: {}, end time: {}", start, end);
+    return start + (long) ((end - start) * TICKET_RENEW_WINDOW);
+  }
+
+  /***
+   * Ticket renewer thread
+   * wait till 80% time interval left on the ticket and then renew it
+   */
+  @Override
+  public void run() {
+    try {
+      LOGGER.info("Sentry Ticket renewer thread started");
+      while (!shutDownRenewer) {
+        KerberosTicket tgt = getTGT();
+        if (tgt == null) {
+          LOGGER.warn("No ticket found in the cache");
+          return;
+        }
+        long nextRefresh = getRefreshTime(tgt);
+        while (System.currentTimeMillis() < nextRefresh) {
+          Thread.sleep(1000);
+          if (shutDownRenewer) {
+            return;
+          }
+        }
+        loginWithNewContext();
+        LOGGER.debug("Renewed ticket");
+      }
+    } catch (InterruptedException e1) {
+      LOGGER.warn("Sentry Ticket renewer thread interrupted", e1);
+      return;
+    } catch (LoginException e) {
+      LOGGER.warn("Failed to renew ticket", e);
+    } finally {
+      logoutSubject();
+      LOGGER.info("Sentry Ticket renewer thread finished");
+    }
+  }
+
+  public void startRenewerThread() {
+    ThreadFactory renewerThreadFactory = new ThreadFactoryBuilder()
+        .setNameFormat(KERBEROS_RENEWER_THREAD_NAME)
+        .build();
+    renewerThread = renewerThreadFactory.newThread(this);
+    renewerThread.start();
+  }
+
+  public void shutDown() throws LoginException {
+    if (renewerThread != null) {
+      shutDownRenewer = true;
+    } else {
+      logoutSubject();
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/sentry/blob/7db84b2f/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/SentryService.java
----------------------------------------------------------------------
diff --git a/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/SentryService.java b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/SentryService.java
new file mode 100644
index 0000000..d92ec21
--- /dev/null
+++ b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/SentryService.java
@@ -0,0 +1,658 @@
+/*
+ * 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.sentry.service.thrift;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.net.InetSocketAddress;
+import java.net.MalformedURLException;
+import java.net.ServerSocket;
+import java.security.PrivilegedExceptionAction;
+import java.util.ArrayList;
+import java.util.EventListener;
+import java.util.List;
+import java.util.concurrent.*;
+
+import javax.security.auth.Subject;
+
+import com.codahale.metrics.Gauge;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.GnuParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Options;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hive.conf.HiveConf;
+import org.apache.hadoop.net.NetUtils;
+import org.apache.hadoop.security.SaslRpcServer;
+import org.apache.hadoop.security.SaslRpcServer.AuthMethod;
+import org.apache.hadoop.security.SecurityUtil;
+import org.apache.sentry.Command;
+import org.apache.sentry.api.common.SentryServiceUtil;
+import org.apache.sentry.core.common.utils.SigUtils;
+import org.apache.sentry.provider.db.service.persistent.HMSFollower;
+import org.apache.sentry.provider.db.service.persistent.LeaderStatusMonitor;
+import org.apache.sentry.provider.db.service.persistent.SentryStore;
+import org.apache.sentry.api.service.thrift.SentryHealthCheckServletContextListener;
+import org.apache.sentry.api.service.thrift.SentryMetrics;
+import org.apache.sentry.api.service.thrift.SentryMetricsServletContextListener;
+import org.apache.sentry.api.service.thrift.SentryWebServer;
+import org.apache.sentry.service.common.ServiceConstants;
+import org.apache.sentry.service.common.ServiceConstants.ConfUtilties;
+import org.apache.sentry.service.common.ServiceConstants.ServerConfig;
+import org.apache.thrift.TMultiplexedProcessor;
+import org.apache.thrift.protocol.TBinaryProtocol;
+import org.apache.thrift.server.TServer;
+import org.apache.thrift.server.TServerEventHandler;
+import org.apache.thrift.server.TThreadPoolServer;
+import org.apache.thrift.transport.TSaslServerTransport;
+import org.apache.thrift.transport.TServerSocket;
+import org.apache.thrift.transport.TServerTransport;
+import org.apache.thrift.transport.TTransportFactory;
+import org.eclipse.jetty.util.MultiException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Preconditions;
+
+import static org.apache.sentry.core.common.utils.SigUtils.registerSigListener;
+
+// Enable signal handler for HA leader/follower status if configured
+public class SentryService implements Callable, SigUtils.SigListener {
+
+  private static final Logger LOGGER = LoggerFactory.getLogger(SentryService.class);
+  private HiveSimpleConnectionFactory hiveConnectionFactory;
+
+  private static final String SENTRY_SERVICE_THREAD_NAME = "sentry-service";
+  private static final String HMSFOLLOWER_THREAD_NAME = "hms-follower";
+  private static final String STORE_CLEANER_THREAD_NAME = "store-cleaner";
+  private static final String SERVICE_SHUTDOWN_THREAD_NAME = "service-shutdown";
+
+  private enum Status {
+    NOT_STARTED,
+    STARTED,
+  }
+
+  private final Configuration conf;
+  private final InetSocketAddress address;
+  private final int maxThreads;
+  private final int minThreads;
+  private final boolean kerberos;
+  private final String principal;
+  private final String[] principalParts;
+  private final String keytab;
+  private final ExecutorService serviceExecutor;
+  private ScheduledExecutorService hmsFollowerExecutor = null;
+  private HMSFollower hmsFollower = null;
+  private Future serviceStatus;
+  private TServer thriftServer;
+  private Status status;
+  private final int webServerPort;
+  private SentryWebServer sentryWebServer;
+  private final long maxMessageSize;
+  /*
+    sentryStore provides the data access for sentry data. It is the singleton instance shared
+    between various {@link SentryPolicyService}, i.e., {@link SentryPolicyStoreProcessor} and
+    {@link HMSFollower}.
+   */
+  private final SentryStore sentryStore;
+  private ScheduledExecutorService sentryStoreCleanService;
+  private final LeaderStatusMonitor leaderMonitor;
+
+  public SentryService(Configuration conf) throws Exception {
+    this.conf = conf;
+    int port = conf
+        .getInt(ServerConfig.RPC_PORT, ServerConfig.RPC_PORT_DEFAULT);
+    if (port == 0) {
+      port = findFreePort();
+      conf.setInt(ServerConfig.RPC_PORT, port);
+    }
+    this.address = NetUtils.createSocketAddr(
+        conf.get(ServerConfig.RPC_ADDRESS, ServerConfig.RPC_ADDRESS_DEFAULT),
+        port);
+    LOGGER.info("Configured on address {}", address);
+    kerberos = ServerConfig.SECURITY_MODE_KERBEROS.equalsIgnoreCase(
+        conf.get(ServerConfig.SECURITY_MODE, ServerConfig.SECURITY_MODE_KERBEROS).trim());
+    maxThreads = conf.getInt(ServerConfig.RPC_MAX_THREADS,
+        ServerConfig.RPC_MAX_THREADS_DEFAULT);
+    minThreads = conf.getInt(ServerConfig.RPC_MIN_THREADS,
+        ServerConfig.RPC_MIN_THREADS_DEFAULT);
+    maxMessageSize = conf.getLong(ServerConfig.SENTRY_POLICY_SERVER_THRIFT_MAX_MESSAGE_SIZE,
+        ServerConfig.SENTRY_POLICY_SERVER_THRIFT_MAX_MESSAGE_SIZE_DEFAULT);
+    if (kerberos) {
+      // Use Hadoop libraries to translate the _HOST placeholder with actual hostname
+      try {
+        String rawPrincipal = Preconditions.checkNotNull(conf.get(ServerConfig.PRINCIPAL), ServerConfig.PRINCIPAL + " is required");
+        principal = SecurityUtil.getServerPrincipal(rawPrincipal, address.getAddress());
+      } catch(IOException io) {
+        throw new RuntimeException("Can't translate kerberos principal'", io);
+      }
+      LOGGER.info("Using kerberos principal: {}", principal);
+
+      principalParts = SaslRpcServer.splitKerberosName(principal);
+      Preconditions.checkArgument(principalParts.length == 3,
+          "Kerberos principal should have 3 parts: " + principal);
+      keytab = Preconditions.checkNotNull(conf.get(ServerConfig.KEY_TAB),
+          ServerConfig.KEY_TAB + " is required");
+      File keytabFile = new File(keytab);
+      Preconditions.checkState(keytabFile.isFile() && keytabFile.canRead(),
+          "Keytab %s does not exist or is not readable.", keytab);
+    } else {
+      principal = null;
+      principalParts = null;
+      keytab = null;
+    }
+    ThreadFactory sentryServiceThreadFactory = new ThreadFactoryBuilder()
+        .setNameFormat(SENTRY_SERVICE_THREAD_NAME)
+        .build();
+    serviceExecutor = Executors.newSingleThreadExecutor(sentryServiceThreadFactory);
+    this.sentryStore = new SentryStore(conf);
+    sentryStore.setPersistUpdateDeltas(SentryServiceUtil.isHDFSSyncEnabled(conf));
+    this.leaderMonitor = LeaderStatusMonitor.getLeaderStatusMonitor(conf);
+    webServerPort = conf.getInt(ServerConfig.SENTRY_WEB_PORT, ServerConfig.SENTRY_WEB_PORT_DEFAULT);
+
+    status = Status.NOT_STARTED;
+
+    // Enable signal handler for HA leader/follower status if configured
+    String sigName = conf.get(ServerConfig.SERVER_HA_STANDBY_SIG);
+    if ((sigName != null) && !sigName.isEmpty()) {
+      LOGGER.info("Registering signal handler {} for HA", sigName);
+      try {
+        registerSigListener(sigName, this);
+      } catch (Exception e) {
+        LOGGER.error("Failed to register signal", e);
+      }
+    }
+  }
+
+  @Override
+  public String call() throws Exception {
+    SentryKerberosContext kerberosContext = null;
+    try {
+      status = Status.STARTED;
+      if (kerberos) {
+        kerberosContext = new SentryKerberosContext(principal, keytab, true);
+        Subject.doAs(kerberosContext.getSubject(), new PrivilegedExceptionAction<Void>() {
+          @Override
+          public Void run() throws Exception {
+            runServer();
+            return null;
+          }
+        });
+      } else {
+        runServer();
+      }
+    } catch (Exception t) {
+      LOGGER.error("Error starting server", t);
+      throw new Exception("Error starting server", t);
+    } finally {
+      if (kerberosContext != null) {
+        kerberosContext.shutDown();
+      }
+      status = Status.NOT_STARTED;
+    }
+    return null;
+  }
+
+  private void runServer() throws Exception {
+
+    startSentryStoreCleaner(conf);
+    startHMSFollower(conf);
+
+    Iterable<String> processorFactories = ConfUtilties.CLASS_SPLITTER
+        .split(conf.get(ServerConfig.PROCESSOR_FACTORIES,
+            ServerConfig.PROCESSOR_FACTORIES_DEFAULT).trim());
+    TMultiplexedProcessor processor = new TMultiplexedProcessor();
+    boolean registeredProcessor = false;
+    for (String processorFactory : processorFactories) {
+      Class<?> clazz = conf.getClassByName(processorFactory);
+      if (!ProcessorFactory.class.isAssignableFrom(clazz)) {
+        throw new IllegalArgumentException("Processor Factory "
+            + processorFactory + " is not a "
+            + ProcessorFactory.class.getName());
+      }
+      try {
+        Constructor<?> constructor = clazz
+            .getConstructor(Configuration.class);
+        LOGGER.info("ProcessorFactory being used: " + clazz.getCanonicalName());
+        ProcessorFactory factory = (ProcessorFactory) constructor
+            .newInstance(conf);
+        boolean registerStatus = factory.register(processor, sentryStore);
+        if (!registerStatus) {
+          LOGGER.error("Failed to register " + clazz.getCanonicalName());
+        }
+        registeredProcessor = registerStatus || registeredProcessor;
+      } catch (Exception e) {
+        throw new IllegalStateException("Could not create "
+            + processorFactory, e);
+      }
+    }
+    if (!registeredProcessor) {
+      throw new IllegalStateException(
+          "Failed to register any processors from " + processorFactories);
+    }
+    addSentryServiceGauge();
+    TServerTransport serverTransport = new TServerSocket(address);
+    TTransportFactory transportFactory = null;
+    if (kerberos) {
+      TSaslServerTransport.Factory saslTransportFactory = new TSaslServerTransport.Factory();
+      saslTransportFactory.addServerDefinition(AuthMethod.KERBEROS
+          .getMechanismName(), principalParts[0], principalParts[1],
+              ServerConfig.SASL_PROPERTIES, new GSSCallback(conf));
+      transportFactory = saslTransportFactory;
+    } else {
+      transportFactory = new TTransportFactory();
+    }
+    TThreadPoolServer.Args args = new TThreadPoolServer.Args(
+        serverTransport).processor(processor)
+        .transportFactory(transportFactory)
+        .protocolFactory(new TBinaryProtocol.Factory(true, true, maxMessageSize, maxMessageSize))
+        .minWorkerThreads(minThreads).maxWorkerThreads(maxThreads);
+    thriftServer = new TThreadPoolServer(args);
+    LOGGER.info("Serving on {}", address);
+    startSentryWebServer();
+
+    // thriftServer.serve() does not return until thriftServer is stopped. Need to log before
+    // calling thriftServer.serve()
+    LOGGER.info("Sentry service is ready to serve client requests");
+
+    // Allow clients/users watching the console to know when sentry is ready
+    System.out.println("Sentry service is ready to serve client requests");
+    SentryStateBank.enableState(SentryServiceState.COMPONENT, SentryServiceState.SERVICE_RUNNING);
+    thriftServer.serve();
+  }
+
+  private void startHMSFollower(Configuration conf) throws Exception {
+    boolean syncPolicyStore = SentryServiceUtil.isSyncPolicyStoreEnabled(conf);
+
+    if ((!SentryServiceUtil.isHDFSSyncEnabled(conf)) && (!syncPolicyStore)) {
+      LOGGER.info("HMS follower is not started because HDFS sync is disabled and perm sync is disabled");
+      return;
+    }
+
+    String metastoreURI = SentryServiceUtil.getHiveMetastoreURI();
+    if (metastoreURI == null) {
+      LOGGER.info("Metastore uri is not configured. Do not start HMSFollower");
+      return;
+    }
+
+    LOGGER.info("Starting HMSFollower to HMS {}", metastoreURI);
+
+    Preconditions.checkState(hmsFollower == null);
+    Preconditions.checkState(hmsFollowerExecutor == null);
+    Preconditions.checkState(hiveConnectionFactory == null);
+
+    hiveConnectionFactory = new HiveSimpleConnectionFactory(conf, new HiveConf());
+    hiveConnectionFactory.init();
+    hmsFollower = new HMSFollower(conf, sentryStore, leaderMonitor, hiveConnectionFactory);
+    long initDelay = conf.getLong(ServerConfig.SENTRY_HMSFOLLOWER_INIT_DELAY_MILLS,
+            ServerConfig.SENTRY_HMSFOLLOWER_INIT_DELAY_MILLS_DEFAULT);
+    long period = conf.getLong(ServerConfig.SENTRY_HMSFOLLOWER_INTERVAL_MILLS,
+            ServerConfig.SENTRY_HMSFOLLOWER_INTERVAL_MILLS_DEFAULT);
+    try {
+      ThreadFactory hmsFollowerThreadFactory = new ThreadFactoryBuilder()
+          .setNameFormat(HMSFOLLOWER_THREAD_NAME)
+          .build();
+      hmsFollowerExecutor = Executors.newScheduledThreadPool(1, hmsFollowerThreadFactory);
+      hmsFollowerExecutor.scheduleAtFixedRate(hmsFollower,
+              initDelay, period, TimeUnit.MILLISECONDS);
+    } catch (IllegalArgumentException e) {
+      LOGGER.error(String.format("Could not start HMSFollower due to illegal argument. period is %s ms",
+              period), e);
+      throw e;
+    }
+  }
+
+  private void stopHMSFollower(Configuration conf) {
+    if ((hmsFollowerExecutor == null) || (hmsFollower == null)) {
+        Preconditions.checkState(hmsFollower == null);
+        Preconditions.checkState(hmsFollowerExecutor == null);
+
+        LOGGER.debug("Skip shuting down hmsFollowerExecutor and closing hmsFollower because they are not created");
+        return;
+    }
+
+    Preconditions.checkNotNull(hmsFollowerExecutor);
+    Preconditions.checkNotNull(hmsFollower);
+    Preconditions.checkNotNull(hiveConnectionFactory);
+
+    // use follower scheduling interval as timeout for shutting down its executor as
+    // such scheduling interval should be an upper bound of how long the task normally takes to finish
+    long timeoutValue = conf.getLong(ServerConfig.SENTRY_HMSFOLLOWER_INTERVAL_MILLS,
+            ServerConfig.SENTRY_HMSFOLLOWER_INTERVAL_MILLS_DEFAULT);
+    try {
+      SentryServiceUtil.shutdownAndAwaitTermination(hmsFollowerExecutor, "hmsFollowerExecutor",
+              timeoutValue, TimeUnit.MILLISECONDS, LOGGER);
+    } finally {
+      try {
+        hiveConnectionFactory.close();
+      } catch (Exception e) {
+        LOGGER.error("Can't close HiveConnectionFactory", e);
+      }
+      hmsFollowerExecutor = null;
+      hiveConnectionFactory = null;
+      try {
+        // close connections
+        hmsFollower.close();
+      } catch (Exception ex) {
+        LOGGER.error("HMSFollower.close() failed", ex);
+      } finally {
+        hmsFollower = null;
+      }
+    }
+  }
+
+  private void startSentryStoreCleaner(Configuration conf) {
+    Preconditions.checkState(sentryStoreCleanService == null);
+
+    // If SENTRY_STORE_CLEAN_PERIOD_SECONDS is set to positive, the background SentryStore cleaning
+    // thread is enabled. Currently, it only purges the delta changes {@link MSentryChange} in
+    // the sentry store.
+    long storeCleanPeriodSecs = conf.getLong(
+            ServerConfig.SENTRY_STORE_CLEAN_PERIOD_SECONDS,
+            ServerConfig.SENTRY_STORE_CLEAN_PERIOD_SECONDS_DEFAULT);
+    if (storeCleanPeriodSecs <= 0) {
+      return;
+    }
+
+    try {
+      Runnable storeCleaner = new Runnable() {
+        @Override
+        public void run() {
+          if (leaderMonitor.isLeader()) {
+            sentryStore.purgeDeltaChangeTables();
+            sentryStore.purgeNotificationIdTable();
+          }
+        }
+      };
+
+      ThreadFactory sentryStoreCleanerThreadFactory = new ThreadFactoryBuilder()
+          .setNameFormat(STORE_CLEANER_THREAD_NAME)
+          .build();
+      sentryStoreCleanService = Executors.newSingleThreadScheduledExecutor(sentryStoreCleanerThreadFactory);
+      sentryStoreCleanService.scheduleWithFixedDelay(
+              storeCleaner, 0, storeCleanPeriodSecs, TimeUnit.SECONDS);
+
+      LOGGER.info("sentry store cleaner is scheduled with interval {} seconds", storeCleanPeriodSecs);
+    }
+    catch(IllegalArgumentException e){
+      LOGGER.error("Could not start SentryStoreCleaner due to illegal argument", e);
+      sentryStoreCleanService = null;
+    }
+  }
+
+  private void stopSentryStoreCleaner() {
+    Preconditions.checkNotNull(sentryStoreCleanService);
+
+    try {
+      SentryServiceUtil.shutdownAndAwaitTermination(sentryStoreCleanService, "sentryStoreCleanService",
+              10, TimeUnit.SECONDS, LOGGER);
+    }
+    finally {
+      sentryStoreCleanService = null;
+    }
+  }
+
+  private void addSentryServiceGauge() {
+    SentryMetrics.getInstance().addSentryServiceGauges(this);
+  }
+
+  private void startSentryWebServer() throws Exception{
+    Boolean sentryReportingEnable = conf.getBoolean(ServerConfig.SENTRY_WEB_ENABLE,
+        ServerConfig.SENTRY_WEB_ENABLE_DEFAULT);
+    if(sentryReportingEnable) {
+      List<EventListener> listenerList = new ArrayList<>();
+      listenerList.add(new SentryHealthCheckServletContextListener());
+      listenerList.add(new SentryMetricsServletContextListener());
+      sentryWebServer = new SentryWebServer(listenerList, webServerPort, conf);
+      sentryWebServer.start();
+    }
+  }
+
+  private void stopSentryWebServer() throws Exception{
+    if( sentryWebServer != null) {
+      sentryWebServer.stop();
+      sentryWebServer = null;
+    }
+  }
+
+  public InetSocketAddress getAddress() {
+    return address;
+  }
+
+  public synchronized boolean isRunning() {
+    return status == Status.STARTED && thriftServer != null
+        && thriftServer.isServing();
+  }
+
+  public synchronized void start() throws Exception{
+    if (status != Status.NOT_STARTED) {
+      throw new IllegalStateException("Cannot start when " + status);
+    }
+    LOGGER.info("Attempting to start...");
+    serviceStatus = serviceExecutor.submit(this);
+  }
+
+  public synchronized void stop() throws Exception{
+    MultiException exception = null;
+    LOGGER.info("Attempting to stop...");
+    leaderMonitor.close();
+    if (isRunning()) {
+      LOGGER.info("Attempting to stop sentry thrift service...");
+      try {
+        thriftServer.stop();
+        thriftServer = null;
+        status = Status.NOT_STARTED;
+      } catch (Exception e) {
+        LOGGER.error("Error while stopping sentry thrift service", e);
+        exception = addMultiException(exception,e);
+      }
+    } else {
+      thriftServer = null;
+      status = Status.NOT_STARTED;
+      LOGGER.info("Sentry thrift service is already stopped...");
+    }
+    if (isWebServerRunning()) {
+      try {
+        LOGGER.info("Attempting to stop sentry web service...");
+        stopSentryWebServer();
+      } catch (Exception e) {
+        LOGGER.error("Error while stopping sentry web service", e);
+        exception = addMultiException(exception,e);
+      }
+    } else {
+      LOGGER.info("Sentry web service is already stopped...");
+    }
+
+    stopHMSFollower(conf);
+    stopSentryStoreCleaner();
+
+    if (exception != null) {
+      exception.ifExceptionThrow();
+    }
+    SentryStateBank.disableState(SentryServiceState.COMPONENT,SentryServiceState.SERVICE_RUNNING);
+    LOGGER.info("Stopped...");
+  }
+
+  /**
+   * If the current daemon is active, make it standby.
+   * Here 'active' means it is the only daemon that can fetch snapshots from HMA and write
+   * to the backend DB.
+   */
+  @VisibleForTesting
+  public synchronized void becomeStandby() {
+    leaderMonitor.deactivate();
+  }
+
+  private MultiException addMultiException(MultiException exception, Exception e) {
+    MultiException newException = exception;
+    if (newException == null) {
+      newException = new MultiException();
+    }
+    newException.add(e);
+    return newException;
+  }
+
+  private boolean isWebServerRunning() {
+    return sentryWebServer != null
+        && sentryWebServer.isAlive();
+  }
+
+  private static int findFreePort() {
+    int attempts = 0;
+    while (attempts++ <= 1000) {
+      try {
+        ServerSocket s = new ServerSocket(0);
+        int port = s.getLocalPort();
+        s.close();
+        return port;
+      } catch (IOException e) {
+        // ignore and retry
+      }
+    }
+    throw new IllegalStateException("Unable to find a port after 1000 attempts");
+  }
+
+  public static Configuration loadConfig(String configFileName)
+      throws MalformedURLException {
+    File configFile = null;
+    if (configFileName == null) {
+      throw new IllegalArgumentException("Usage: "
+          + ServiceConstants.ServiceArgs.CONFIG_FILE_LONG
+          + " path/to/sentry-service.xml");
+    } else if (!((configFile = new File(configFileName)).isFile() && configFile
+        .canRead())) {
+      throw new IllegalArgumentException("Cannot read configuration file "
+          + configFile);
+    }
+    Configuration conf = new Configuration(false);
+    conf.addResource(configFile.toURI().toURL(), true);
+    return conf;
+  }
+
+  public static class CommandImpl implements Command {
+    @Override
+    public void run(String[] args) throws Exception {
+      CommandLineParser parser = new GnuParser();
+      Options options = new Options();
+      options.addOption(ServiceConstants.ServiceArgs.CONFIG_FILE_SHORT,
+          ServiceConstants.ServiceArgs.CONFIG_FILE_LONG,
+          true, "Sentry Service configuration file");
+      CommandLine commandLine = parser.parse(options, args);
+      String configFileName = commandLine.getOptionValue(ServiceConstants.
+          ServiceArgs.CONFIG_FILE_LONG);
+      File configFile = null;
+      if (configFileName == null || commandLine.hasOption("h") || commandLine.hasOption("help")) {
+        // print usage
+        HelpFormatter formatter = new HelpFormatter();
+        formatter.printHelp("sentry --command service", options);
+        System.exit(-1);
+      } else if(!((configFile = new File(configFileName)).isFile() && configFile.canRead())) {
+        throw new IllegalArgumentException("Cannot read configuration file " + configFile);
+      }
+      Configuration serverConf = loadConfig(configFileName);
+      final SentryService server = new SentryService(serverConf);
+      server.start();
+
+      ThreadFactory serviceShutdownThreadFactory = new ThreadFactoryBuilder()
+          .setNameFormat(SERVICE_SHUTDOWN_THREAD_NAME)
+          .build();
+      Runtime.getRuntime().addShutdownHook(serviceShutdownThreadFactory.newThread(new Runnable() {
+        @Override
+        public void run() {
+          LOGGER.info("ShutdownHook shutting down server");
+          try {
+            server.stop();
+          } catch (Throwable t) {
+            LOGGER.error("Error stopping SentryService", t);
+            System.exit(1);
+          }
+        }
+      }));
+
+      // Let's wait on the service to stop
+      try {
+        // Wait for the service thread to finish
+        server.serviceStatus.get();
+      } finally {
+        server.serviceExecutor.shutdown();
+      }
+    }
+  }
+
+  public Configuration getConf() {
+    return conf;
+  }
+
+  /**
+   * Add Thrift event handler to underlying thrift threadpool server
+   * @param eventHandler
+   */
+  public void setThriftEventHandler(TServerEventHandler eventHandler) throws IllegalStateException {
+    if (thriftServer == null) {
+      throw new IllegalStateException("Server is not initialized or stopped");
+    }
+    thriftServer.setServerEventHandler(eventHandler);
+  }
+
+  public TServerEventHandler getThriftEventHandler() throws IllegalStateException {
+    if (thriftServer == null) {
+      throw new IllegalStateException("Server is not initialized or stopped");
+    }
+    return thriftServer.getEventHandler();
+  }
+
+  public Gauge<Boolean> getIsActiveGauge() {
+    return new Gauge<Boolean>() {
+      @Override
+      public Boolean getValue() {
+        return leaderMonitor.isLeader();
+      }
+    };
+  }
+
+  public Gauge<Long> getBecomeActiveCount() {
+    return new Gauge<Long>() {
+      @Override
+      public Long getValue() {
+        return leaderMonitor.getLeaderCount();
+      }
+    };
+  }
+
+  @Override
+  public void onSignal(String signalName) {
+    // Become follower
+    leaderMonitor.deactivate();
+  }
+
+  /**
+   * Restart HMSFollower with new configuration
+   * @param newConf Configuration
+   * @throws Exception
+   */
+  @VisibleForTesting
+  public void restartHMSFollower(Configuration newConf) throws Exception{
+    stopHMSFollower(conf);
+    startHMSFollower(newConf);
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/sentry/blob/7db84b2f/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/SentryServiceFactory.java
----------------------------------------------------------------------
diff --git a/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/SentryServiceFactory.java b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/SentryServiceFactory.java
new file mode 100644
index 0000000..c1d81ed
--- /dev/null
+++ b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/SentryServiceFactory.java
@@ -0,0 +1,27 @@
+/**
+ * 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.sentry.service.thrift;
+
+import org.apache.hadoop.conf.Configuration;
+
+public class SentryServiceFactory {
+  public static SentryService create(Configuration conf) throws Exception {
+    return new SentryService(conf);
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/sentry/blob/7db84b2f/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/SentryServiceState.java
----------------------------------------------------------------------
diff --git a/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/SentryServiceState.java b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/SentryServiceState.java
new file mode 100644
index 0000000..4219adc
--- /dev/null
+++ b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/SentryServiceState.java
@@ -0,0 +1,44 @@
+/**
+ * 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.sentry.service.thrift;
+
+/**
+ * States for the SentryService
+ */
+public enum SentryServiceState implements SentryState {
+  /**
+   * The SentryService is running all of its threads and services.  This include the store cleaner,
+   * the web interface, the HMS poller, and the Thrift Server
+   */
+  SERVICE_RUNNING,
+
+  /**
+   * A full update of data from the HMS is running by the thread handling the update.
+   */
+  FULL_UPDATE_RUNNING;
+
+  /**
+   * The component name this state is for.
+   */
+  public static final String COMPONENT = "SentryService";
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public long getValue() {
+    return 1 << this.ordinal();
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/sentry/blob/7db84b2f/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/SentryState.java
----------------------------------------------------------------------
diff --git a/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/SentryState.java b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/SentryState.java
new file mode 100644
index 0000000..040d82a
--- /dev/null
+++ b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/SentryState.java
@@ -0,0 +1,27 @@
+/**
+ * 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.sentry.service.thrift;
+
+/**
+ * Interface for SentryState enums.
+ */
+public interface SentryState {
+
+  /**
+   * This gets the Bitmask value associated with the state.
+   */
+  long getValue();
+}

http://git-wip-us.apache.org/repos/asf/sentry/blob/7db84b2f/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/SentryStateBank.java
----------------------------------------------------------------------
diff --git a/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/SentryStateBank.java b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/SentryStateBank.java
new file mode 100644
index 0000000..2c05d49
--- /dev/null
+++ b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/service/thrift/SentryStateBank.java
@@ -0,0 +1,159 @@
+/**
+ * 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.sentry.service.thrift;
+
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.util.concurrent.AtomicLongMap;
+import java.util.Set;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import javax.annotation.concurrent.ThreadSafe;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * <p>SentryStateBank is a state visibility manager to allow components to communicate state to other
+ * parts of the application.</p>
+ *
+ * <p>It allows you to provide multiple boolean states for a component and expose those states to
+ * other parts of the application without having references to the actual instances of the classes
+ * setting those states.</p>
+ *
+ * <p>SentryStateBank uses a bitmasked long in order to store the states, so its very compact and
+ * efficient.</p>
+ *
+ * <p>States are defined using an enum that implements the {@link SentryState} interface.  The
+ * {@link SentryState} implementation can provide up to 64 states per components.  The {@link SentryState#getValue()}
+ * implementation should return a bitshift of the oridinal of the enum value.  This gives the bitmask
+ * location to be checking for the state.</p>
+ *
+ * <p>The following is an example of a simple {@link SentryState} enum implementation</p>
+ *
+ * <pre>
+ * {@code
+ *
+ * public enum ExampleState implements SentryState {
+ *  FIRST_STATE,
+ *  SECOND_STATE;
+ *
+ *  public static final String COMPONENT = "ExampleState";
+ *
+ *  @Override
+ *  public long getValue() {
+ *    return 1 << this.ordinal();
+ *  }
+ * }
+ * }
+ * </pre>
+ *
+ * <p>This class is thread safe.  It uses a {@link ReentrantReadWriteLock} to wrap accesses and changes
+ * to the state.</p>
+ */
+@ThreadSafe
+public final class SentryStateBank {
+
+  private static final Logger LOGGER = LoggerFactory.getLogger(SentryStateBank.class);
+  private static final AtomicLongMap<String> states = AtomicLongMap.create();
+  private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
+
+  protected SentryStateBank() {
+  }
+
+  @VisibleForTesting
+  static void clearAllStates() {
+    states.clear();
+    LOGGER.debug("All states have been cleared.");
+  }
+
+  @VisibleForTesting
+  static void resetComponentState(String component) {
+    states.remove(component);
+    LOGGER.debug("All states have been cleared for component {}", component);
+  }
+
+  /**
+   * Enables a state
+   *
+   * @param component the component for the state
+   * @param state the state to disable
+   */
+  public static void enableState(String component, SentryState state) {
+    lock.writeLock().lock();
+    try {
+      states.put(component, states.get(component) | state.getValue());
+      if (LOGGER.isDebugEnabled()) {
+        LOGGER.debug("{} entered state {}", component, state.toString());
+      }
+    } finally {
+      lock.writeLock().unlock();
+    }
+  }
+
+  /**
+   * Disables a state for a component
+   *
+   * @param component the component for the state
+   * @param state the state to disable
+   */
+  public static void disableState(String component, SentryState state) {
+    lock.writeLock().lock();
+    try {
+      states.put(component, states.get(component) & (~state.getValue()));
+      if (LOGGER.isDebugEnabled()) {
+        LOGGER.debug("{} exited state {}", component, state.toString());
+      }
+    } finally {
+      lock.writeLock().unlock();
+    }
+  }
+
+  /**
+   * Returns if a state is enabled or not
+   *
+   * @param component The component for the state
+   * @param state the SentryState to check
+   * @return true if the state for the component is enabled
+   */
+  public static boolean isEnabled(String component, SentryState state) {
+    lock.readLock().lock();
+    try {
+      return (states.get(component) & state.getValue()) == state.getValue();
+    } finally {
+      lock.readLock().unlock();
+    }
+
+  }
+
+  /**
+   * Checks if all of the states passed in are enabled
+   *
+   * @param component The component for the states
+   * @param passedStates the SentryStates to check
+   */
+  public static boolean hasStatesEnabled(String component, Set<SentryState> passedStates) {
+    lock.readLock().lock();
+    try {
+      long value = 0L;
+
+      for (SentryState state : passedStates) {
+          value += state.getValue();
+      }
+      return (states.get(component) & value) == value;
+    } finally {
+      lock.readLock().unlock();
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/sentry/blob/7db84b2f/sentry-service/sentry-service-server/src/main/webapp/SentryService.html
----------------------------------------------------------------------
diff --git a/sentry-service/sentry-service-server/src/main/webapp/SentryService.html b/sentry-service/sentry-service-server/src/main/webapp/SentryService.html
new file mode 100644
index 0000000..9f52a8e
--- /dev/null
+++ b/sentry-service/sentry-service-server/src/main/webapp/SentryService.html
@@ -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.
+-->
+<!DOCTYPE HTML>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>Sentry Service</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta name="description" content="">
+    <link href="css/bootstrap.min.css" rel="stylesheet">
+    <link href="css/bootstrap-theme.min.css" rel="stylesheet">
+    <link href="css/sentry.css" rel="stylesheet">
+  </head>
+
+  <body>
+    <nav class="navbar navbar-default navbar-fixed-top">
+      <div class="container">
+        <div class="navbar-header">
+          <a class="navbar-brand" href="#"><img src="sentry.png" alt="Sentry Logo"/></a>
+        </div>
+        <div class="collapse navbar-collapse">
+          <ul class="nav navbar-nav">
+            <li class="active"><a href="#">Home</a></li>
+            <li><a href="/metrics?pretty=true">Metrics</a></li>
+            <li><a href="/threads">Threads</a></li>
+            <li><a href="/conf">Configuration</a></li>
+          </ul>
+        </div>
+      </div>
+    </nav>
+
+    <div class="container">
+      <div class="page-header"><h2>Sentry Service</h2></div>
+      <ul>
+        <li><a href="/metrics?pretty=true">Metrics</a></li>
+        <li><a href="/threads">Threads</a></li>
+        <li><a href="/conf">Configuration</a></li>
+      </ul>
+    </div>
+
+    <footer class="footer">
+      <div class="container">
+        <p class="text-muted">SENTRY 2.0.0-SNAPSHOT</p>
+      </div>
+    </footer>
+  </body>
+</html>

http://git-wip-us.apache.org/repos/asf/sentry/blob/7db84b2f/sentry-service/sentry-service-server/src/main/webapp/css/bootstrap-theme.min.css
----------------------------------------------------------------------
diff --git a/sentry-service/sentry-service-server/src/main/webapp/css/bootstrap-theme.min.css b/sentry-service/sentry-service-server/src/main/webapp/css/bootstrap-theme.min.css
new file mode 100644
index 0000000..c31428b
--- /dev/null
+++ b/sentry-service/sentry-service-server/src/main/webapp/css/bootstrap-theme.min.css
@@ -0,0 +1,10 @@
+/*!
+ * Bootstrap v3.0.0
+ *
+ * Copyright 2013 Twitter, Inc
+ * Licensed under the Apache License v2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Designed and built with all the love in the world by @mdo and @fat.
+ */
+.btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn:active,.btn.active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left 0,left 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,0%,#e6e6e6,100%);background-image:-moz-linear-gradient(top,#fff 0,#e6e6e6 100%);background-image:linear-gradient(to bottom,#fff 0,#e6e6e6 100%);background-repeat:repeat-x;border-co
 lor:#e0e0e0;border-color:#ccc;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe6e6e6',GradientType=0)}.btn-default:active,.btn-default.active{background-color:#e6e6e6;border-color:#e0e0e0}.btn-primary{background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#3071a9));background-image:-webkit-linear-gradient(top,#428bca,0%,#3071a9,100%);background-image:-moz-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:linear-gradient(to bottom,#428bca 0,#3071a9 100%);background-repeat:repeat-x;border-color:#2d6ca2;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff3071a9',GradientType=0)}.btn-primary:active,.btn-primary.active{background-color:#3071a9;border-color:#2d6ca2}.btn-success{background-image:-webkit-gradient(linear,left 0,left 100%,from(#5cb85c),to(#449d44));background-image:-webkit-linear-gradient(top,#5cb85c,0%,#449d44,100%);background-image:-moz-linear-gradient(top,#5cb
 85c 0,#449d44 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);background-repeat:repeat-x;border-color:#419641;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c',endColorstr='#ff449d44',GradientType=0)}.btn-success:active,.btn-success.active{background-color:#449d44;border-color:#419641}.btn-warning{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f0ad4e),to(#ec971f));background-image:-webkit-linear-gradient(top,#f0ad4e,0%,#ec971f,100%);background-image:-moz-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);background-repeat:repeat-x;border-color:#eb9316;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e',endColorstr='#ffec971f',GradientType=0)}.btn-warning:active,.btn-warning.active{background-color:#ec971f;border-color:#eb9316}.btn-danger{background-image:-webkit-gradient(linear,left 0,left 100%,from(#d9534f),to(#c9302c));background-i
 mage:-webkit-linear-gradient(top,#d9534f,0%,#c9302c,100%);background-image:-moz-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);background-repeat:repeat-x;border-color:#c12e2a;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f',endColorstr='#ffc9302c',GradientType=0)}.btn-danger:active,.btn-danger.active{background-color:#c9302c;border-color:#c12e2a}.btn-info{background-image:-webkit-gradient(linear,left 0,left 100%,from(#5bc0de),to(#31b0d5));background-image:-webkit-linear-gradient(top,#5bc0de,0%,#31b0d5,100%);background-image:-moz-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);background-repeat:repeat-x;border-color:#2aabd2;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff31b0d5',GradientType=0)}.btn-info:active,.btn-info.active{background-color:#31b0d5;border-color:#2aabd2}.thumbnail,.img-
 thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus,.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-color:#357ebd;background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#357ebd));background-image:-webkit-linear-gradient(top,#428bca,0%,#357ebd,100%);background-image:-moz-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff357ebd',GradientType=0)}.navbar{background-image:-webkit-gradient(linear,left 0,left 100%,from(#fff),to(#f8f8f8));background-image:-webkit-linear-gradient(top,#fff,0%,#f8f8f8,100%);background-image:-moz-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);background-repeat:repe
 at-x;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#fff8f8f8',GradientType=0);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075)}.navbar .navbar-nav>.active>a{background-color:#f8f8f8}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,0.25)}.navbar-inverse{background-image:-webkit-gradient(linear,left 0,left 100%,from(#3c3c3c),to(#222));background-image:-webkit-linear-gradient(top,#3c3c3c,0%,#222,100%);background-image:-moz-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c',endColorstr='#ff222222',GradientType=0)}.navbar-inverse .navbar-nav>.active>a{background-color:#222}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow
 :0 -1px 0 rgba(0,0,0,0.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}.alert{text-shadow:0 1px 0 rgba(255,255,255,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05)}.alert-success{background-image:-webkit-gradient(linear,left 0,left 100%,from(#dff0d8),to(#c8e5bc));background-image:-webkit-linear-gradient(top,#dff0d8,0%,#c8e5bc,100%);background-image:-moz-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);background-repeat:repeat-x;border-color:#b2dba1;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8',endColorstr='#ffc8e5bc',GradientType=0)}.alert-info{background-image:-webkit-gradient(linear,left 0,left 100%,from(#d9edf7),to(#b9def0));background-image:-webkit-linear-gradient(top,#d9edf7,0%,#b9def0,100%);background-image:-moz-linear-gradient(top,#d9edf7 0,#b9
 def0 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);background-repeat:repeat-x;border-color:#9acfea;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7',endColorstr='#ffb9def0',GradientType=0)}.alert-warning{background-image:-webkit-gradient(linear,left 0,left 100%,from(#fcf8e3),to(#f8efc0));background-image:-webkit-linear-gradient(top,#fcf8e3,0%,#f8efc0,100%);background-image:-moz-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);background-repeat:repeat-x;border-color:#f5e79e;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3',endColorstr='#fff8efc0',GradientType=0)}.alert-danger{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f2dede),to(#e7c3c3));background-image:-webkit-linear-gradient(top,#f2dede,0%,#e7c3c3,100%);background-image:-moz-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:linear-gradient(to bottom,#f2dede 0,
 #e7c3c3 100%);background-repeat:repeat-x;border-color:#dca7a7;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede',endColorstr='#ffe7c3c3',GradientType=0)}.progress{background-image:-webkit-gradient(linear,left 0,left 100%,from(#ebebeb),to(#f5f5f5));background-image:-webkit-linear-gradient(top,#ebebeb,0%,#f5f5f5,100%);background-image:-moz-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb',endColorstr='#fff5f5f5',GradientType=0)}.progress-bar{background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#3071a9));background-image:-webkit-linear-gradient(top,#428bca,0%,#3071a9,100%);background-image:-moz-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:linear-gradient(to bottom,#428bca 0,#3071a9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient
 (startColorstr='#ff428bca',endColorstr='#ff3071a9',GradientType=0)}.progress-bar-success{background-image:-webkit-gradient(linear,left 0,left 100%,from(#5cb85c),to(#449d44));background-image:-webkit-linear-gradient(top,#5cb85c,0%,#449d44,100%);background-image:-moz-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c',endColorstr='#ff449d44',GradientType=0)}.progress-bar-info{background-image:-webkit-gradient(linear,left 0,left 100%,from(#5bc0de),to(#31b0d5));background-image:-webkit-linear-gradient(top,#5bc0de,0%,#31b0d5,100%);background-image:-moz-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff31b0d5',GradientType=0)}.progress-bar-warning{backg
 round-image:-webkit-gradient(linear,left 0,left 100%,from(#f0ad4e),to(#ec971f));background-image:-webkit-linear-gradient(top,#f0ad4e,0%,#ec971f,100%);background-image:-moz-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e',endColorstr='#ffec971f',GradientType=0)}.progress-bar-danger{background-image:-webkit-gradient(linear,left 0,left 100%,from(#d9534f),to(#c9302c));background-image:-webkit-linear-gradient(top,#d9534f,0%,#c9302c,100%);background-image:-moz-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f',endColorstr='#ffc9302c',GradientType=0)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.li
 st-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #3071a9;background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#3278b3));background-image:-webkit-linear-gradient(top,#428bca,0%,#3278b3,100%);background-image:-moz-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:linear-gradient(to bottom,#428bca 0,#3278b3 100%);background-repeat:repeat-x;border-color:#3278b3;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff3278b3',GradientType=0)}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.panel-default>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f5f5f5),to(#e8e8e8));background-image:-webkit-linear-gradient(top,#f5f5f5,0%,#e8e8e8,100%);background-image:-moz-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x
 ;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#ffe8e8e8',GradientType=0)}.panel-primary>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#357ebd));background-image:-webkit-linear-gradient(top,#428bca,0%,#357ebd,100%);background-image:-moz-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff357ebd',GradientType=0)}.panel-success>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#dff0d8),to(#d0e9c6));background-image:-webkit-linear-gradient(top,#dff0d8,0%,#d0e9c6,100%);background-image:-moz-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8
 ',endColorstr='#ffd0e9c6',GradientType=0)}.panel-info>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#d9edf7),to(#c4e3f3));background-image:-webkit-linear-gradient(top,#d9edf7,0%,#c4e3f3,100%);background-image:-moz-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7',endColorstr='#ffc4e3f3',GradientType=0)}.panel-warning>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#fcf8e3),to(#faf2cc));background-image:-webkit-linear-gradient(top,#fcf8e3,0%,#faf2cc,100%);background-image:-moz-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3',endColorstr='#fffaf2cc',GradientType=0)}.panel-danger>.panel-heading{backgro
 und-image:-webkit-gradient(linear,left 0,left 100%,from(#f2dede),to(#ebcccc));background-image:-webkit-linear-gradient(top,#f2dede,0%,#ebcccc,100%);background-image:-moz-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede',endColorstr='#ffebcccc',GradientType=0)}.well{background-image:-webkit-gradient(linear,left 0,left 100%,from(#e8e8e8),to(#f5f5f5));background-image:-webkit-linear-gradient(top,#e8e8e8,0%,#f5f5f5,100%);background-image:-moz-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);background-repeat:repeat-x;border-color:#dcdcdc;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8',endColorstr='#fff5f5f5',GradientType=0);-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 3px rgba(0
 ,0,0,0.05),0 1px 0 rgba(255,255,255,0.1)}
\ No newline at end of file