brooklyn-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From henev...@apache.org
Subject [3/7] incubator-brooklyn git commit: javascript checks up on start, shows caution
Date Thu, 07 May 2015 10:57:18 GMT
javascript checks up on start, shows caution

popup now appears while server is starting, or if there are errors, and comes back if server changes to standby.
changed to plug in *before* the routes are evaluated, so we don't load the wrong data.

expands rest api to return several pieces of information to simplify gui checks.

simplifies code in rest filters to block requests not compatible with server state, including checking for upness there.


Project: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/commit/e629602b
Tree: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/tree/e629602b
Diff: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/diff/e629602b

Branch: refs/heads/master
Commit: e629602b41613f1068c9fa4b319e99dafc5eeeeb
Parents: ddce439
Author: Alex Heneveld <alex.heneveld@cloudsoftcorp.com>
Authored: Thu Apr 23 16:31:30 2015 +0100
Committer: Alex Heneveld <alex.heneveld@cloudsoftcorp.com>
Committed: Mon Apr 27 08:37:04 2015 +0100

----------------------------------------------------------------------
 .../brooklyn/entity/rebind/RebindManager.java   |   5 +
 .../entity/rebind/RebindManagerImpl.java        |  11 +
 .../ha/HighAvailabilityManagerImpl.java         |   5 +-
 .../NonDeploymentManagementContext.java         |   6 +-
 usage/jsgui/src/main/webapp/assets/js/config.js |  10 +-
 .../jsgui/src/main/webapp/assets/js/model/ha.js |  62 ------
 .../assets/js/model/server-extended-status.js   |  87 ++++++++
 usage/jsgui/src/main/webapp/assets/js/router.js | 205 ++++++++++---------
 .../main/webapp/assets/js/view/ha-summary.js    |  17 +-
 .../src/main/webapp/assets/js/view/home.js      |   4 +-
 .../webapp/assets/tpl/home/server-caution.html  |  94 +++++++++
 .../assets/tpl/home/server-not-ha-master.html   |  35 ----
 usage/jsgui/src/main/webapp/index.html          |   2 +-
 .../src/test/javascript/specs/model/ha-spec.js  |  41 ----
 .../src/test/javascript/specs/router-spec.js    |   3 -
 .../main/java/brooklyn/rest/api/ServerApi.java  |   5 +
 .../rest/filter/HaHotCheckResourceFilter.java   |  74 +++++--
 .../rest/filter/HaMasterCheckFilter.java        |  58 ++++--
 .../rest/filter/ServerStatusCheckFilter.java    | 120 +++++++++++
 .../brooklyn/rest/resources/ServerResource.java |  15 +-
 .../brooklyn/rest/BrooklynRestApiLauncher.java  |   1 +
 .../test/java/brooklyn/rest/HaHotCheckTest.java |  55 ++---
 .../testing/mocks/ManagementContextMock.java    |  18 +-
 23 files changed, 600 insertions(+), 333 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e629602b/api/src/main/java/brooklyn/entity/rebind/RebindManager.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/brooklyn/entity/rebind/RebindManager.java b/api/src/main/java/brooklyn/entity/rebind/RebindManager.java
index 556c6bc..501430d 100644
--- a/api/src/main/java/brooklyn/entity/rebind/RebindManager.java
+++ b/api/src/main/java/brooklyn/entity/rebind/RebindManager.java
@@ -125,6 +125,11 @@ public interface RebindManager {
      * setting if the process fails after the clear!) */
     @VisibleForTesting
     public void forcePersistNow(boolean full, @Nullable PersistenceExceptionHandler exceptionHandler);
+    
+    /** Whether the management state has changed to a state where a rebind is needed
+     * but we are still awaiting the first run; 
+     * ie state is master or hot, but list of apps is not yet accurate */
+    public boolean isAwaitingInitialRebind();
 
     /** Metrics about rebind, last success, etc. */
     public Map<String,Object> getMetrics();

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e629602b/core/src/main/java/brooklyn/entity/rebind/RebindManagerImpl.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/rebind/RebindManagerImpl.java b/core/src/main/java/brooklyn/entity/rebind/RebindManagerImpl.java
index 5d155cb..a31c1de 100644
--- a/core/src/main/java/brooklyn/entity/rebind/RebindManagerImpl.java
+++ b/core/src/main/java/brooklyn/entity/rebind/RebindManagerImpl.java
@@ -132,6 +132,8 @@ public class RebindManagerImpl implements RebindManager {
     private RebindFailureMode loadPolicyFailureMode;
     private QuorumCheck danglingRefsQuorumRequiredHealthy;
     
+    private boolean isAwaitingInitialRebind;
+    
     private PersistenceActivityMetrics rebindMetrics = new PersistenceActivityMetrics();
     private PersistenceActivityMetrics persistMetrics = new PersistenceActivityMetrics();
 
@@ -547,6 +549,7 @@ public class RebindManagerImpl implements RebindManager {
             firstRebindEntityCount = iteration.getRebindContext().getEntities().size();
             firstRebindItemCount = iteration.getRebindContext().getAllBrooklynObjects().size();
         }
+        isAwaitingInitialRebind = false;
 
         return iteration.getApplications();
     }
@@ -578,6 +581,14 @@ public class RebindManagerImpl implements RebindManager {
         return result;
     }
 
+    public boolean isAwaitingInitialRebind() {
+        return isAwaitingInitialRebind;
+    }
+
+    public void setAwaitingInitialRebind(boolean isAwaitingInitialRebind) {
+        this.isAwaitingInitialRebind = isAwaitingInitialRebind;
+    }
+    
     /**
      * Wraps a ChangeListener, to log and never propagate any exceptions that it throws.
      * 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e629602b/core/src/main/java/brooklyn/management/ha/HighAvailabilityManagerImpl.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/ha/HighAvailabilityManagerImpl.java b/core/src/main/java/brooklyn/management/ha/HighAvailabilityManagerImpl.java
index ec844a3..b004f6b 100644
--- a/core/src/main/java/brooklyn/management/ha/HighAvailabilityManagerImpl.java
+++ b/core/src/main/java/brooklyn/management/ha/HighAvailabilityManagerImpl.java
@@ -43,6 +43,7 @@ import brooklyn.entity.basic.BrooklynTaskTags;
 import brooklyn.entity.basic.ConfigKeys;
 import brooklyn.entity.basic.EntityInternal;
 import brooklyn.entity.rebind.RebindManager;
+import brooklyn.entity.rebind.RebindManagerImpl;
 import brooklyn.entity.rebind.persister.BrooklynPersistenceUtils;
 import brooklyn.entity.rebind.persister.BrooklynPersistenceUtils.CreateBackupMode;
 import brooklyn.entity.rebind.persister.PersistenceActivityMetrics;
@@ -151,7 +152,7 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
     private volatile PersistenceActivityMetrics managementStateWritePersistenceMetrics = new PersistenceActivityMetrics();
     private volatile PersistenceActivityMetrics managementStateReadPersistenceMetrics = new PersistenceActivityMetrics();
     private final long startTimeUtc;
-    
+
     public HighAvailabilityManagerImpl(ManagementContextInternal managementContext) {
         this.managementContext = managementContext;
         startTimeUtc = localTickerUtc.read();
@@ -493,6 +494,8 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
                     nodeStateHistory.remove(nodeStateHistory.size()-1);
                 }
             }
+            ((RebindManagerImpl)managementContext.getRebindManager()).setAwaitingInitialRebind(running &&
+                (ManagementNodeState.isHotProxy(newState) || newState==ManagementNodeState.MASTER));
             this.nodeState = newState;
         }
         

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e629602b/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java b/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
index 7f158d5..d5fcf75 100644
--- a/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
+++ b/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
@@ -544,7 +544,11 @@ public class NonDeploymentManagementContext implements ManagementContextInternal
         public BrooklynMementoRawData retrieveMementoRawData() {
             throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
         }
-
+        @Override
+        public boolean isAwaitingInitialRebind() {
+            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
+        }
+        
         @Override
         public Map<String, Object> getMetrics() {
             throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e629602b/usage/jsgui/src/main/webapp/assets/js/config.js
----------------------------------------------------------------------
diff --git a/usage/jsgui/src/main/webapp/assets/js/config.js b/usage/jsgui/src/main/webapp/assets/js/config.js
index 918a7ca..4fa477d 100644
--- a/usage/jsgui/src/main/webapp/assets/js/config.js
+++ b/usage/jsgui/src/main/webapp/assets/js/config.js
@@ -76,13 +76,9 @@ require.config({
 
 /*
  * Main application entry point.
- *
- * Inclusion of brooklyn module sets up logging.
  */
 require([
-    "backbone", "brooklyn", "router", "model/ha"
-], function (Backbone, Brooklyn, Router, ha) {
-    ha.autoUpdate();
-    var router = new Router();
-    Backbone.history.start();
+    "router"
+], function (Router) {
+    new Router().startBrooklynGui();
 });

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e629602b/usage/jsgui/src/main/webapp/assets/js/model/ha.js
----------------------------------------------------------------------
diff --git a/usage/jsgui/src/main/webapp/assets/js/model/ha.js b/usage/jsgui/src/main/webapp/assets/js/model/ha.js
deleted file mode 100644
index 8ece989..0000000
--- a/usage/jsgui/src/main/webapp/assets/js/model/ha.js
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * 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.
-*/
-define(["backbone", "brooklyn", "view/viewutils"], function (Backbone, Brooklyn, ViewUtils) {
-
-    var HAStatus = Backbone.Model.extend({
-        callbacks: [],
-        loaded: false,
-        url: "/v1/server/highAvailability",
-        isMaster: function() {
-            return this.get("masterId") == this.get("ownId");
-        },
-        getMasterUri: function() {
-            // Might be undefined if first fetch hasn't completed
-            var nodes = this.get("nodes") || {};
-            var master = nodes[this.get("masterId")];
-            // defensive, if this happens something more serious has gone wrong!
-            if (!master) {
-                return null;
-            } else {
-                return master.nodeUri;
-            }
-        },
-        onLoad: function(f) {
-            if (this.loaded) {
-                f();
-            } else {
-                this.callbacks.push(f);
-            }
-        },
-        autoUpdate: function() {
-            ViewUtils.fetchModelRepeatedlyWithDelay(this, { doitnow: true });
-        }
-    });
-
-    var haStatus = new HAStatus();
-    haStatus.once("sync", function() {
-        haStatus.loaded = true;
-        _.invoke(haStatus.callbacks, "apply");
-        haStatus.callbacks = undefined;
-    });
-
-    // Will returning the instance rather than the object be confusing?
-    // It breaks the pattern used by all the other models.
-    return haStatus;
-
-});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e629602b/usage/jsgui/src/main/webapp/assets/js/model/server-extended-status.js
----------------------------------------------------------------------
diff --git a/usage/jsgui/src/main/webapp/assets/js/model/server-extended-status.js b/usage/jsgui/src/main/webapp/assets/js/model/server-extended-status.js
new file mode 100644
index 0000000..88e1ef2
--- /dev/null
+++ b/usage/jsgui/src/main/webapp/assets/js/model/server-extended-status.js
@@ -0,0 +1,87 @@
+/*
+ * 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.
+*/
+define(["backbone", "brooklyn", "view/viewutils"], function (Backbone, Brooklyn, ViewUtils) {
+
+    var ServerExtendedStatus = Backbone.Model.extend({
+        callbacks: [],
+        loaded: false,
+        url: "/v1/server/up/extended",
+        whenUp: function(f) {
+            var that = this;
+            if (this.isUp()) {
+                f();
+            } else {
+                this.addCallback(function() { that.whenUp(f); });
+            }
+        },
+        onLoad: function(f) {
+            if (this.loaded) {
+                f();
+            } else {
+                this.addCallback(f);
+            }
+        },
+        addCallback: function(f) {
+            this.callbacks.push(f);
+        },
+        autoUpdate: function() {
+            var that = this;
+            // to debug:
+//            serverExtendedStatus.onLoad(function() { log("loaded server status:"); log(that.attributes); })
+            ViewUtils.fetchModelRepeatedlyWithDelay(this, { doitnow: true });
+        },
+
+        isUp: function() { return this.get("up") },
+        isHealthy: function() { return this.get("healthy") },
+        isMaster: function() {
+            ha = this.get("ha") || {};
+            ownId = ha.ownId;
+            if (!ownId) return null;
+            return ha.masterId == ownId;
+        },
+        getMasterUri: function() {
+            // Might be undefined if first fetch hasn't completed
+            ha = this.get("ha") || {};
+            states = ha.states || {};
+            if (!states) return null;
+            
+            var nodes = this.get("nodes") || {};
+            var master = nodes[this.get("masterId")];
+            // defensive, if this happens something more serious has gone wrong!
+            if (!master) {
+                return null;
+            } else {
+                return master.nodeUri;
+            }
+        },
+    });
+
+    var serverExtendedStatus = new ServerExtendedStatus();
+    serverExtendedStatus.on("sync", function() {
+        serverExtendedStatus.loaded = true;
+        var currentCallbacks = serverExtendedStatus.callbacks;
+        serverExtendedStatus.callbacks = [];
+        _.invoke(currentCallbacks, "apply");
+    });
+
+    // Will returning the instance rather than the object be confusing?
+    // It breaks the pattern used by all the other models.
+    return serverExtendedStatus;
+
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e629602b/usage/jsgui/src/main/webapp/assets/js/router.js
----------------------------------------------------------------------
diff --git a/usage/jsgui/src/main/webapp/assets/js/router.js b/usage/jsgui/src/main/webapp/assets/js/router.js
index 186dd72..4c7689a 100644
--- a/usage/jsgui/src/main/webapp/assets/js/router.js
+++ b/usage/jsgui/src/main/webapp/assets/js/router.js
@@ -18,46 +18,95 @@
 */
 define([
     "brooklyn", "underscore", "jquery", "backbone",
-    "model/application", "model/app-tree", "model/location", "model/ha",
+    "model/application", "model/app-tree", "model/location", 
+    "model/server-extended-status",
     "view/home", "view/application-explorer", "view/catalog", "view/apidoc", "view/script-groovy",
-    "text!tpl/help/page.html","text!tpl/labs/page.html", "text!tpl/home/server-not-ha-master.html"
+    "text!tpl/help/page.html","text!tpl/labs/page.html", "text!tpl/home/server-caution.html"
 ], function (Brooklyn, _, $, Backbone,
-        Application, AppTree, Location, ha,
-        HomeView, ExplorerView, CatalogView, ApidocView, ScriptGroovyView,
-        HelpHtml, LabsHtml, ServerNotMasterHtml) {
+        Application, AppTree, Location, 
+        serverStatus,
+        HomeView, ExplorerView, CatalogView, ApidocView, ScriptGroovyView, 
+        HelpHtml, LabsHtml, ServerCautionHtml) {
 
-    /**
-     * @returns {jquery.Deferred}
-     *      A promise that resolves when the high availability status has been
-     *      loaded. Actions to be taken on the view after it has loaded should
-     *      be registered with calls to .done()
-     */
-    // Not just defined as a function on Router because the delay if the HA status
-    // hasn't loaded requires a reference to the function, which we lose if we use
-    // 'this.showView'.
-    function showViewImpl(router, selector, view) {
-        // Don't do anything until the HA status has loaded.
-        var promise = $.Deferred()
-            .done(function () {
-                // close the previous view - does binding clean-up and avoids memory leaks
-                if (router.currentView) {
-                    router.currentView.close();
-                }
-                // render the view inside the selector element
-                $(selector).html(view.render().el);
-                router.currentView = view;
-                return view
+    var ServerCautionOverlay = Backbone.View.extend({
+        template: _.template(ServerCautionHtml),
+        scheduledRedirect: false,
+        initialize: function() {
+            var that = this;
+            this.carryOnRegardless = false;
+            _.bindAll(this);
+            serverStatus.addCallback(this.renderAndAddCallback);
+        },
+        renderAndAddCallback: function() {
+            this.renderOnUpdate();
+            serverStatus.addCallback(this.renderAndAddCallback);
+        },
+        renderOnUpdate: function() {
+            var that = this;
+            if (this.carryOnRegardless) return this.renderEmpty();
+            
+            var state = {
+                    loaded: serverStatus.loaded,
+                    up: serverStatus.isUp(),
+                    healthy: serverStatus.isHealthy(),
+                    master: serverStatus.isMaster(),
+                    masterUri: serverStatus.getMasterUri(),
+                };
+            if (state.loaded && state.up && state.healthy && state.master) return this.renderEmpty();
+            
+            this.warningActive = true;
+            this.$el.html(this.template(state));
+                
+            $("#dismiss-standby-warning", this.$el).click(function() {
+                that.carryOnRegardless = true;
+                if (that.redirectPending) {
+                    log("Cancelling redirect, using this non-master instance");
+                    clearTimeout(that.redirectPending);
+                    that.redirectPending = null;
+                }       
+                that.renderOnUpdate();
             });
-        (function isComplete() {
-            if (ha.loaded) {
-                promise.resolve();
-            } else {
-                _.defer(isComplete, 100);
+            
+            if (!state.master && state.masterUri) {
+                if (!this.scheduledRedirect && !this.redirectPending) {
+                    log("Not master; will redirect shortly to: "+state.masterUri);
+                    var destination = state.masterUri + "#" + Backbone.history.fragment;
+                    var time = 10;
+                    this.scheduledRedirect = true;
+                    log("Redirecting to " + destination + " in " + time + " seconds");
+                    this.redirectPending = setTimeout(function () {
+                        // re-check, in case the server's status changed in the wait
+                        if (!serverStatus.isMaster()) {
+                            if (that.redirectPending) {
+                                window.location.href = destination;
+                            } else {
+                                log("Cancelled redirect, using this non-master instance");
+                            }
+                        } else {
+                            log("Cancelled redirect, this instance is now master");
+                        }
+                    }, time * 1000);
+                }
             }
-        })();
-        return promise;
-    }
-
+            return this;
+        },
+        renderEmpty: function() {
+            this.warningActive = false;
+            this.$el.empty();
+            return this;
+        },
+        beforeClose: function() {
+            this.stopListening();
+        },
+        warnIfNotLoaded: function() {
+            if (!this.loaded)
+                this.renderOnUpdate();
+        }
+    });
+    // look for ha-standby-overlay for compatibility with older index.html copies
+    var serverCautionOverlay = new ServerCautionOverlay({ el: $("#server-caution-overlay").length ? $("#server-caution-overlay") : $("#ha-standby-overlay")});
+    serverCautionOverlay.render();
+    
     var Router = Backbone.Router.extend({
         routes:{
             'v1/home/*trail':'homePage',
@@ -75,7 +124,14 @@ define([
         },
 
         showView: function(selector, view) {
-            return showViewImpl(this, selector, view);
+            // close the previous view - does binding clean-up and avoids memory leaks
+            if (this.currentView) {
+                this.currentView.close();
+            }
+            // render the view inside the selector element
+            $(selector).html(view.render().el);
+            this.currentView = view;
+            return view;
         },
 
         defaultRoute: function() {
@@ -94,6 +150,7 @@ define([
                 homeView = new HomeView({
                     collection:that.applications,
                     locations:that.locations,
+                    cautionOverlay:serverCautionOverlay,
                     appRouter:that
                 });
                 veryFirstViewLoad = !that.currentView;
@@ -102,13 +159,10 @@ define([
             this.applications.fetch({success:function () {
                 render();
                 // show add application wizard if none already exist and this is the first page load
-                if ((veryFirstViewLoad && trail=='auto' && that.applications.isEmpty()) ||
-                     (trail=='add_application') ) {
-                    ha.onLoad(function() {
-                        if (ha.isMaster()) {
-                            homeView.createApplication();
-                        }
-                    });
+                if ((veryFirstViewLoad && trail=='auto' && that.applications.isEmpty()) || (trail=='add_application') ) {
+                    if (serverStatus.isMaster()) {
+                        homeView.createApplication();
+                    }
                 }
             }, error: render});
         },
@@ -156,64 +210,17 @@ define([
         labsPage:function () {
             $("#application-content").html(_.template(LabsHtml, {}))
             $(".nav1").removeClass("active")
-        }
-    })
-
-    var HaStandbyOverlay = Backbone.View.extend({
-        template: _.template(ServerNotMasterHtml),
-        scheduledRedirect: false,
-        initialize: function() {
-            this.carryOnRegardless = false;
-            this.listenTo(ha, "change", this.render);
         },
-        render: function() {
-            var that = this;
-            
-            if (!ha.isMaster() && !this.carryOnRegardless) {
-                var masterUri = ha.getMasterUri();
-                this.$el.html(this.template({"masterUri": masterUri}));
-                
-                log("render, redirect = "+this.redirectPending);
-                
-                $("#dismiss-standby-warning", this.$el).click(function() {
-                    that.carryOnRegardless = true;
-                    if (that.redirectPending) {
-                        console.log("Cancelling redirect, using this non-master instance");
-                        clearTimeout(that.redirectPending);
-                        that.redirectPending = null;
-                    }       
-                    that.render();
-                });
-                
-                if (masterUri && !this.scheduledRedirect && !this.redirectPending) {
-                    var destination = masterUri + "#" + Backbone.history.fragment;
-                    var time = 10;
-                    this.scheduledRedirect = true;
-                    console.log("Redirecting to " + destination + " in " + time + " seconds");
-                    this.redirectPending = setTimeout(function () {
-                        // re-check, in case the server's status changed in the wait
-                        if (!ha.isMaster()) {
-                            if (that.redirectPending) {
-                                window.location.href = destination;
-                            } else {
-                                console.log("Cancelled redirect, using this non-master instance");
-                            }
-                        } else {
-                            console.log("Cancelled redirect, this instance is now master");
-                        }
-                    }, time * 1000);
-                }
-            } else {
-                this.$el.empty();
-            }
-            return this;
-        },
-        beforeClose: function() {
-            this.stopListening();
+
+        /** Triggers the Backbone.Router process which drives this GUI through Backbone.history,
+         *  after starting background server health checks and waiting for confirmation of health
+         *  (or user click-through). */
+        startBrooklynGui: function() {
+            serverStatus.whenUp(function() { Backbone.history.start(); });
+            serverStatus.autoUpdate();
+            _.delay(serverCautionOverlay.warnIfNotLoaded, 2*1000)
         }
     });
-    new HaStandbyOverlay({ el: $("#ha-standby-overlay") }).render();
-
 
     $.ajax({
         type: "GET",
@@ -221,7 +228,7 @@ define([
         dataType: "text"
     }).done(function (data) {
         if (data != null) {
-            $("#user").html(data);
+            $("#user").html(_.escape(data));
         }
     });
 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e629602b/usage/jsgui/src/main/webapp/assets/js/view/ha-summary.js
----------------------------------------------------------------------
diff --git a/usage/jsgui/src/main/webapp/assets/js/view/ha-summary.js b/usage/jsgui/src/main/webapp/assets/js/view/ha-summary.js
index 62b5c3e..2032d1d 100644
--- a/usage/jsgui/src/main/webapp/assets/js/view/ha-summary.js
+++ b/usage/jsgui/src/main/webapp/assets/js/view/ha-summary.js
@@ -18,9 +18,9 @@
 */
 define([
     "jquery", "underscore", "backbone", "moment", "view/viewutils",
-    "model/ha",
+    "model/server-extended-status",
     "text!tpl/home/ha-summary.html"
-], function ($, _, Backbone, moment, ViewUtils, ha, HASummaryHtml) {
+], function ($, _, Backbone, moment, ViewUtils, serverStatus, HASummaryHtml) {
 
     var template = _.template(HASummaryHtml);
     var nodeRowTemplate = _.template(
@@ -38,26 +38,27 @@ define([
         initialize: function() {
             _.bindAll(this);
             this.updateTimestampCallback = setInterval(this.updateTimestamps, 1000);
-            this.listenTo(ha, "change", this.renderNodeStatus);
+            this.listenTo(serverStatus, "change", this.renderNodeStatus);
         },
         beforeClose: function() {
             clearInterval(this.updateTimestampCallback);
             this.stopListening();
         },
         updateNow: function() {
-            ha.fetch();
+            serverStatus.fetch();
         },
         render: function() {
             this.$el.html(template());
-            if (ha.loaded) {
+            if (serverStatus.loaded) {
                 this.renderNodeStatus();
             }
             return this;
         },
         renderNodeStatus: function() {
-            var master = ha.get("masterId"),
-                self = ha.get("ownId"),
-                nodes = ha.get("nodes"),
+            var serverHa = serverStatus.get("ha") || {};
+            var master = serverHa.masterId,
+                self = serverHa.ownId,
+                nodes = serverHa.nodes,
                 $target = this.$(".ha-summary-table-body");
             $target.empty();
             // undefined check just in case server returns something odd

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e629602b/usage/jsgui/src/main/webapp/assets/js/view/home.js
----------------------------------------------------------------------
diff --git a/usage/jsgui/src/main/webapp/assets/js/view/home.js b/usage/jsgui/src/main/webapp/assets/js/view/home.js
index de9ce69..ae2ba3a 100644
--- a/usage/jsgui/src/main/webapp/assets/js/view/home.js
+++ b/usage/jsgui/src/main/webapp/assets/js/view/home.js
@@ -139,7 +139,9 @@ define([
                 this._modal.close()
             }
             var that = this;
-            if (!this.options.offline) {
+            if (this.options.offline || (this.options.cautionOverlay && this.options.cautionOverlay.warningActive)) {
+                // don't show wizard
+            } else {
                 var wizard = new AppAddWizard({appRouter:this.options.appRouter})
                 this._modal = wizard
                 this.$(".add-app #modal-container").html(wizard.render().el)

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e629602b/usage/jsgui/src/main/webapp/assets/tpl/home/server-caution.html
----------------------------------------------------------------------
diff --git a/usage/jsgui/src/main/webapp/assets/tpl/home/server-caution.html b/usage/jsgui/src/main/webapp/assets/tpl/home/server-caution.html
new file mode 100644
index 0000000..0f767c0
--- /dev/null
+++ b/usage/jsgui/src/main/webapp/assets/tpl/home/server-caution.html
@@ -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.
+-->
+
+<div class="server-caution-overlay overlay-with-opacity"></div>
+<div class="server-caution-overlay">
+    <div class="overlay-content">
+        <h1 class="lead">Warning!</h1>
+
+<% if (!loaded) { %>
+        <p><strong>
+            Connecting to server...</strong></p>
+
+            <center><img src="/assets/images/throbber.gif"/></center>        
+        <p>
+            No response from server (yet). 
+            Please check your network connection, or if you are on a slow connection simply be patient!
+            This dialog will disappear if and when the server responds.</p>
+        
+        <p><i>
+            If you understand the risks, that information may be unavailable and operations may fail, 
+            and wish to proceed regardless, click <a id="dismiss-standby-warning">here</a>.</i></p>
+
+<% } else if (!up) { %>
+            <div style="float: right; position: relative; top: -45px; height: 0; overflow: visible;">
+                <img src="/assets/img/icon-status-starting.gif" width="60"/></div>
+            
+        <p><strong>
+            Starting up...</strong></p>
+                    
+        <p>
+            The Brooklyn server is starting.
+            It is recommended to wait before proceeding.
+            This dialog will disappear when the server is ready.</p>
+        
+        <p><i>
+            If you understand the risks, that information may be unavailable and operations may fail, 
+            and wish to proceed regardless, click <a id="dismiss-standby-warning">here</a>.</i></p>
+
+<% } else if (healthy && !master) { %>
+        <p><strong>
+            This Brooklyn server is not the high availability master.</strong></p>
+        
+        <p>
+            It is recommended not to use this server directly.</p>
+            
+    <% if (masterUri) { %>
+        <p>Redirecting to the master server at <a href="<%= masterUri %>"><%= masterUri %></a> shortly.</p>
+    <% } else { %>
+        <p>The address of the master Brooklyn server is not currently known.</p>
+    <% } %>
+
+        <p><i>
+            If you understand the risks, that write operations will likely fail, 
+            and wish to proceed regardless, click <a id="dismiss-standby-warning">here</a>.</i></p>
+          
+<% } else { %>
+        <p><strong>
+            This Brooklyn server has errors.</strong></p>
+        
+        <p>
+            Please check with your system administrator.</p>
+
+    <% if (healthy) { /* this page should not be displayed in this case */ %>
+        <p>
+            The misconfiguration may be in Javascript as the server is reporting it is healthy.</p><% } %>
+        
+        <p><i>
+          If you would like to debug the server, click <a id="dismiss-standby-warning">here</a>
+          to dismiss this warning until you reload this page. 
+          (You should reload the page once you believe the errors are fixed
+          to confirm that this dialog does not return.)
+          </i></p>
+
+<% } %>
+
+    </div>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e629602b/usage/jsgui/src/main/webapp/assets/tpl/home/server-not-ha-master.html
----------------------------------------------------------------------
diff --git a/usage/jsgui/src/main/webapp/assets/tpl/home/server-not-ha-master.html b/usage/jsgui/src/main/webapp/assets/tpl/home/server-not-ha-master.html
deleted file mode 100644
index b9b8b73..0000000
--- a/usage/jsgui/src/main/webapp/assets/tpl/home/server-not-ha-master.html
+++ /dev/null
@@ -1,35 +0,0 @@
-
-<!--
-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.
--->
-
-<div class="ha-standby-overlay overlay-with-opacity"></div>
-<div class="ha-standby-overlay">
-    <div class="overlay-content">
-        <h1 class="lead">Warning!</h1>
-        <p><strong>This Brooklyn server is not the high availability master.</strong></p>
-        <p>It is recommended not to use this server directly.</p>
-    <% if (masterUri) { %>
-        <p>Redirecting to the master server at <a href="<%= masterUri %>"><%= masterUri %></a> shortly.</p>
-    <% } else { %>
-        <p>The address of the master Brooklyn server is not currently known.</p>
-    <% } %>
-        <p><i>If you understand the risks, that write operations will likely fail, 
-          and wish to proceed regardless, click <a id="dismiss-standby-warning">here</a>.</i>
-    </div>
-</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e629602b/usage/jsgui/src/main/webapp/index.html
----------------------------------------------------------------------
diff --git a/usage/jsgui/src/main/webapp/index.html b/usage/jsgui/src/main/webapp/index.html
index 856830e..fda3514 100644
--- a/usage/jsgui/src/main/webapp/index.html
+++ b/usage/jsgui/src/main/webapp/index.html
@@ -67,7 +67,7 @@ under the License.
 
 <div id="main-content">
     <div id="application-content"></div>
-    <div id="ha-standby-overlay"></div>
+    <div id="server-caution-overlay"></div>
 </div>
 
 <!-- load our code with require.js. data-main is replaced with optimised file at build -->

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e629602b/usage/jsgui/src/test/javascript/specs/model/ha-spec.js
----------------------------------------------------------------------
diff --git a/usage/jsgui/src/test/javascript/specs/model/ha-spec.js b/usage/jsgui/src/test/javascript/specs/model/ha-spec.js
deleted file mode 100644
index 0e51ef0..0000000
--- a/usage/jsgui/src/test/javascript/specs/model/ha-spec.js
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * 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.
-*/
-define([
-    "model/ha"
-], function (ha) {
-
-    describe("model/ha", function() {
-
-        beforeEach(function () {
-            ha.url = "fixtures/ha-summary.json";
-            ha.fetch();
-        });
-
-        it("should determine the master's URI", function() {
-            expect(ha.getMasterUri()).toBe("http://10.30.40.50:8081/");
-        });
-
-        it("should determine isMaster", function() {
-            expect(ha.isMaster()).toBe(true);
-            ha.set("masterId", "something else");
-            expect(ha.isMaster()).toBe(false);
-        });
-
-    });
-});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e629602b/usage/jsgui/src/test/javascript/specs/router-spec.js
----------------------------------------------------------------------
diff --git a/usage/jsgui/src/test/javascript/specs/router-spec.js b/usage/jsgui/src/test/javascript/specs/router-spec.js
index bd30eca..0041373 100644
--- a/usage/jsgui/src/test/javascript/specs/router-spec.js
+++ b/usage/jsgui/src/test/javascript/specs/router-spec.js
@@ -20,9 +20,6 @@ define([
     "brooklyn", "router", "model/ha"
 ], function (Brooklyn, Router, ha) {
 
-    ha.url = "fixtures/ha-summary.json";
-    ha.loaded = true;
-
     var View = Backbone.View.extend({
         render:function () {
             this.$el.html("<p>fake view</p>")

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e629602b/usage/rest-api/src/main/java/brooklyn/rest/api/ServerApi.java
----------------------------------------------------------------------
diff --git a/usage/rest-api/src/main/java/brooklyn/rest/api/ServerApi.java b/usage/rest-api/src/main/java/brooklyn/rest/api/ServerApi.java
index 7ee371f..a1ac60c 100644
--- a/usage/rest-api/src/main/java/brooklyn/rest/api/ServerApi.java
+++ b/usage/rest-api/src/main/java/brooklyn/rest/api/ServerApi.java
@@ -101,6 +101,11 @@ public interface ServerApi {
     public String getStatus();
 
     @GET
+    @Path("/up/extended")
+    @ApiOperation(value = "Returns extended server-up information, a map including up (/up), healthy (/healthy), and ha (/ha/states) (qv)")
+    public Map<String,Object> getUpExtended();
+
+    @GET
     @Path("/config/{configKey}")
     @ApiOperation(value = "Get the value of the specified config key from brooklyn properties")
     @ApiErrors(value = {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e629602b/usage/rest-server/src/main/java/brooklyn/rest/filter/HaHotCheckResourceFilter.java
----------------------------------------------------------------------
diff --git a/usage/rest-server/src/main/java/brooklyn/rest/filter/HaHotCheckResourceFilter.java b/usage/rest-server/src/main/java/brooklyn/rest/filter/HaHotCheckResourceFilter.java
index c219de2..dc2e954 100644
--- a/usage/rest-server/src/main/java/brooklyn/rest/filter/HaHotCheckResourceFilter.java
+++ b/usage/rest-server/src/main/java/brooklyn/rest/filter/HaHotCheckResourceFilter.java
@@ -29,12 +29,12 @@ import javax.ws.rs.core.Response;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import brooklyn.entity.rebind.RebindManagerImpl.RebindTracker;
 import brooklyn.management.ManagementContext;
 import brooklyn.management.ha.ManagementNodeState;
 import brooklyn.rest.domain.ApiError;
-import brooklyn.util.time.Duration;
+import brooklyn.util.text.Strings;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableSet;
 import com.sun.jersey.api.model.AbstractMethod;
 import com.sun.jersey.spi.container.ContainerRequest;
@@ -43,17 +43,32 @@ import com.sun.jersey.spi.container.ContainerResponseFilter;
 import com.sun.jersey.spi.container.ResourceFilter;
 import com.sun.jersey.spi.container.ResourceFilterFactory;
 
+/** 
+ * Checks that if the method or resource class corresponding to a request
+ * has a {@link HaHotStateRequired} annotation,
+ * that the server is in that state (and up). 
+ * Requests with {@link #SKIP_CHECK_HEADER} set as a header skip this check.
+ * <p>
+ * This follows a different pattern to {@link HaMasterCheckFilter} 
+ * as this needs to know the method being invoked. 
+ */
 public class HaHotCheckResourceFilter implements ResourceFilterFactory {
     
     private static final Logger log = LoggerFactory.getLogger(HaHotCheckResourceFilter.class);
     
     private static final Set<ManagementNodeState> HOT_STATES = ImmutableSet.of(
             ManagementNodeState.MASTER, ManagementNodeState.HOT_STANDBY, ManagementNodeState.HOT_BACKUP);
-    private static final long STATE_CHANGE_SETTLE_OFFSET = Duration.seconds(10).toMilliseconds();
 
     @Context
     private ManagementContext mgmt;
 
+    public HaHotCheckResourceFilter() {}
+    
+    @VisibleForTesting
+    public HaHotCheckResourceFilter(ManagementContext mgmt) {
+        this.mgmt = mgmt;
+    }
+    
     private static class MethodFilter implements ResourceFilter, ContainerRequestFilter {
 
         private AbstractMethod am;
@@ -74,39 +89,54 @@ public class HaHotCheckResourceFilter implements ResourceFilterFactory {
             return null;
         }
 
+        private String lookForProblem(ContainerRequest request) {
+            if (isSkipCheckHeaderSet(request)) 
+                return null;
+            
+            if (!isHaHotStateRequired(request))
+                return null;
+            
+            String problem = HaMasterCheckFilter.lookForProblemIfServerNotRunning(mgmt);
+            if (Strings.isNonBlank(problem)) 
+                return problem;
+            
+            if (!isHaHotStatus())
+                return "server not in required HA hot state";
+            if (isStateNotYetValid())
+                return "server not yet completed loading data for required HA hot state";
+            
+            return null;
+        }
+        
         @Override
         public ContainerRequest filter(ContainerRequest request) {
-            if (!isStateLoaded() && isUnsafe(request)) {
-                log.warn("Disallowed request to standby brooklyn: "+request+"/"+am+" (caller should set '"+HaMasterCheckFilter.SKIP_CHECK_HEADER+"' to force)");
+            String problem = lookForProblem(request);
+            if (Strings.isNonBlank(problem)) {
+                log.warn("Disallowing request as "+problem+": "+request+"/"+am+" (caller should set '"+HaMasterCheckFilter.SKIP_CHECK_HEADER+"' to force)");
                 throw new WebApplicationException(ApiError.builder()
-                    .message("This request is not permitted against a standby Brooklyn server")
+                    .message("This request is only permitted against an active hot Brooklyn server")
                     .errorCode(Response.Status.FORBIDDEN).build().asJsonResponse());
             }
             return request;
         }
 
-        private boolean isStateLoaded() {
-            return isHaHotStatusOrDisabled() && !RebindTracker.isRebinding() && !recentlySwitchedState();
+        // Maybe there should be a separate state to indicate that we have switched state
+        // but still haven't finished rebinding. (Previously there was a time delay and an
+        // isRebinding check, but introducing RebindManager#isAwaitingInitialRebind() seems cleaner.)
+        private boolean isStateNotYetValid() {
+            return mgmt.getRebindManager().isAwaitingInitialRebind();
         }
 
-        // Ideally there will be a separate state to indicate that we switched state
-        // but still haven't finished rebinding. There's a gap between changing the state
-        // and starting rebind so add a time offset just to be sure.
-        private boolean recentlySwitchedState() {
-            long lastStateChange = mgmt.getHighAvailabilityManager().getLastStateChange();
-            if (lastStateChange == -1) return false;
-            return System.currentTimeMillis() - lastStateChange < STATE_CHANGE_SETTLE_OFFSET;
+        private boolean isHaHotStateRequired(ContainerRequest request) {
+            return (am.getAnnotation(HaHotStateRequired.class) != null ||
+                    am.getResource().getAnnotation(HaHotStateRequired.class) != null);
         }
 
-        private boolean isUnsafe(ContainerRequest request) {
-            boolean isOverriden = "true".equalsIgnoreCase(request.getHeaderValue(HaMasterCheckFilter.SKIP_CHECK_HEADER));
-            return !isOverriden &&
-                    (am.getAnnotation(HaHotStateRequired.class) != null ||
-                    am.getResource().getAnnotation(HaHotStateRequired.class) != null);
+        private boolean isSkipCheckHeaderSet(ContainerRequest request) {
+            return "true".equalsIgnoreCase(request.getHeaderValue(HaMasterCheckFilter.SKIP_CHECK_HEADER));
         }
 
-        private boolean isHaHotStatusOrDisabled() {
-            if (!mgmt.getHighAvailabilityManager().isRunning()) return true;
+        private boolean isHaHotStatus() {
             ManagementNodeState state = mgmt.getHighAvailabilityManager().getNodeState();
             return HOT_STATES.contains(state);
         }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e629602b/usage/rest-server/src/main/java/brooklyn/rest/filter/HaMasterCheckFilter.java
----------------------------------------------------------------------
diff --git a/usage/rest-server/src/main/java/brooklyn/rest/filter/HaMasterCheckFilter.java b/usage/rest-server/src/main/java/brooklyn/rest/filter/HaMasterCheckFilter.java
index bfb2cf4..9cca507 100644
--- a/usage/rest-server/src/main/java/brooklyn/rest/filter/HaMasterCheckFilter.java
+++ b/usage/rest-server/src/main/java/brooklyn/rest/filter/HaMasterCheckFilter.java
@@ -40,13 +40,15 @@ import brooklyn.management.ManagementContext;
 import brooklyn.management.ha.ManagementNodeState;
 import brooklyn.rest.domain.ApiError;
 import brooklyn.rest.util.WebResourceUtils;
+import brooklyn.util.text.Strings;
 
 import com.google.common.collect.Sets;
 
 /**
- * Checks that the request is appropriate given the high availability status of the server.
- *
- * @see brooklyn.management.ha.ManagementNodeState
+ * Checks that for requests that want HA master state, the server is up and in that state.
+ * <p>
+ * Post POSTs and PUTs are assumed to need master state, with the exception of shutdown.
+ * Requests with {@link #SKIP_CHECK_HEADER} set as a header skip this check.
  */
 public class HaMasterCheckFilter implements Filter {
 
@@ -64,12 +66,37 @@ public class HaMasterCheckFilter implements Filter {
         mgmt = (ManagementContext) servletContext.getAttribute(BrooklynServiceAttributes.BROOKLYN_MANAGEMENT_CONTEXT);
     }
 
+    static String lookForProblemIfServerNotRunning(ManagementContext mgmt) {
+        if (mgmt==null) return "no management context available";
+        if (!mgmt.isRunning()) return "server no longer running";
+        if (!mgmt.isStartupComplete()) return "server not in required startup-completed state";
+        return null;
+    }
+    
+    private String lookForProblem(ServletRequest request) {
+        if (isSkipCheckHeaderSet(request)) 
+            return null;
+        
+        if (!isMasterRequiredForRequest(request))
+            return null;
+        
+        String problem = lookForProblemIfServerNotRunning(mgmt);
+        if (Strings.isNonBlank(problem)) 
+            return problem;
+        
+        if (!isMaster()) 
+            return "server not in required HA master state";
+        
+        return null;
+    }
+    
     @Override
     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
-        if (!isMaster() && !isRequestAllowedForNonMaster(request)) {
-            log.warn("Disallowed request to non-HA-master brooklyn: "+request+"/"+request.getParameterMap()+" (caller should set '"+SKIP_CHECK_HEADER+"' to force)");
+        String problem = lookForProblem(request);
+        if (problem!=null) {
+            log.warn("Disallowing request as "+problem+": "+request.getParameterMap()+" (caller should set '"+SKIP_CHECK_HEADER+"' to force)");
             WebResourceUtils.applyJsonResponse(servletContext, ApiError.builder()
-                .message("This request is only permitted against a master Brooklyn server")
+                .message("This request is only permitted against an active master Brooklyn server")
                 .errorCode(Response.Status.FORBIDDEN).build().asJsonResponse(), (HttpServletResponse)response);
         } else {
             chain.doFilter(request, response);
@@ -84,23 +111,30 @@ public class HaMasterCheckFilter implements Filter {
         return ManagementNodeState.MASTER.equals(mgmt.getHighAvailabilityManager().getNodeState());
     }
 
-    private boolean isRequestAllowedForNonMaster(ServletRequest request) {
+    private boolean isMasterRequiredForRequest(ServletRequest request) {
         if (request instanceof HttpServletRequest) {
             HttpServletRequest httpRequest = (HttpServletRequest) request;
-            String checkOverridden = httpRequest.getHeader(SKIP_CHECK_HEADER);
-            if ("true".equalsIgnoreCase(checkOverridden)) return true;
             
             String method = httpRequest.getMethod().toUpperCase();
-            if (SAFE_STANDBY_METHODS.contains(method)) return true;
+            // gets usually okay
+            if (SAFE_STANDBY_METHODS.contains(method)) return false;
             
             // explicitly allow calls to shutdown
             // (if stopAllApps is specified, the method itself will fail; but we do not want to consume parameters here, that breaks things!)
             // TODO combine with HaHotCheckResourceFilter and use an annotation HaAnyStateAllowed or similar
-            if ("/v1/server/shutdown".equals(httpRequest.getRequestURI())) return true;
+            if ("/v1/server/shutdown".equals(httpRequest.getRequestURI())) return false;
             
-            return false;
+            // master required for everything else
+            return true;
         }
         // previously non-HttpServletRequests were allowed but I don't think they should be
+        return true;
+    }
+
+    private boolean isSkipCheckHeaderSet(ServletRequest httpRequest) {
+        if (httpRequest instanceof HttpServletRequest)
+            return "true".equalsIgnoreCase(((HttpServletRequest)httpRequest).getHeader(SKIP_CHECK_HEADER));
         return false;
     }
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e629602b/usage/rest-server/src/main/java/brooklyn/rest/filter/ServerStatusCheckFilter.java
----------------------------------------------------------------------
diff --git a/usage/rest-server/src/main/java/brooklyn/rest/filter/ServerStatusCheckFilter.java b/usage/rest-server/src/main/java/brooklyn/rest/filter/ServerStatusCheckFilter.java
new file mode 100644
index 0000000..d5c30a3
--- /dev/null
+++ b/usage/rest-server/src/main/java/brooklyn/rest/filter/ServerStatusCheckFilter.java
@@ -0,0 +1,120 @@
+/*
+ * 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 brooklyn.rest.filter;
+
+import java.io.IOException;
+import java.util.Set;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.core.Response;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.config.BrooklynServiceAttributes;
+import brooklyn.management.ManagementContext;
+import brooklyn.management.ha.ManagementNodeState;
+import brooklyn.rest.domain.ApiError;
+import brooklyn.rest.util.WebResourceUtils;
+
+import com.google.common.collect.Sets;
+
+/**
+ * Checks that the request is appropriate given the high availability status of the server.
+ *
+ * @see brooklyn.management.ha.ManagementNodeState
+ */
+public class ServerStatusCheckFilter implements Filter {
+
+    private static final Logger log = LoggerFactory.getLogger(ServerStatusCheckFilter.class);
+    
+    public static final String SKIP_CHECK_HEADER = "Brooklyn-Allow-Non-Master-Access";
+    private static final Set<String> SAFE_STANDBY_METHODS = Sets.newHashSet("GET", "HEAD");
+
+    protected ServletContext servletContext;
+    protected ManagementContext mgmt;
+
+    @Override
+    public void init(FilterConfig config) throws ServletException {
+        servletContext = config.getServletContext();
+        mgmt = (ManagementContext) servletContext.getAttribute(BrooklynServiceAttributes.BROOKLYN_MANAGEMENT_CONTEXT);
+    }
+
+    private String lookForProblem(ServletRequest request) {
+        if (isMasterRequiredForRequest(request)) {
+            if (mgmt==null) return "no management context available";
+            if (!mgmt.isRunning()) return "server no longer running";
+            if (!mgmt.isStartupComplete()) return "server not in required startup-completed state";
+            if (!isMaster()) return "server not in required HA master state";
+        }
+        return null;
+    }
+    
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+        String problem = lookForProblem(request);
+        if (problem!=null) {
+            log.warn("Disallowing request as "+problem+"/"+request.getParameterMap()+" (caller should set '"+SKIP_CHECK_HEADER+"' to force)");
+            WebResourceUtils.applyJsonResponse(servletContext, ApiError.builder()
+                .message("This request is only permitted against an active master Brooklyn server")
+                .errorCode(Response.Status.FORBIDDEN).build().asJsonResponse(), (HttpServletResponse)response);
+        } else {
+            chain.doFilter(request, response);
+        }
+    }
+
+    @Override
+    public void destroy() {
+    }
+
+    private boolean isMaster() {
+        return ManagementNodeState.MASTER.equals(mgmt.getHighAvailabilityManager().getNodeState());
+    }
+
+    private boolean isMasterRequiredForRequest(ServletRequest request) {
+        if (request instanceof HttpServletRequest) {
+            HttpServletRequest httpRequest = (HttpServletRequest) request;
+            String checkOverridden = httpRequest.getHeader(SKIP_CHECK_HEADER);
+            if ("true".equalsIgnoreCase(checkOverridden)) return false;
+            
+            String method = httpRequest.getMethod().toUpperCase();
+            // gets usually okay
+            if (SAFE_STANDBY_METHODS.contains(method)) return false;
+            
+            // explicitly allow calls to shutdown
+            // (if stopAllApps is specified, the method itself will fail; but we do not want to consume parameters here, that breaks things!)
+            // TODO combine with HaHotCheckResourceFilter and use an annotation HaAnyStateAllowed or similar
+            if ("/v1/server/shutdown".equals(httpRequest.getRequestURI())) return false;
+            
+            // master required for everything else
+            return true;
+        }
+        // previously non-HttpServletRequests were allowed but I don't think they should be
+        return true;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e629602b/usage/rest-server/src/main/java/brooklyn/rest/resources/ServerResource.java
----------------------------------------------------------------------
diff --git a/usage/rest-server/src/main/java/brooklyn/rest/resources/ServerResource.java b/usage/rest-server/src/main/java/brooklyn/rest/resources/ServerResource.java
index eac52f6..ecefd4c 100644
--- a/usage/rest-server/src/main/java/brooklyn/rest/resources/ServerResource.java
+++ b/usage/rest-server/src/main/java/brooklyn/rest/resources/ServerResource.java
@@ -33,14 +33,13 @@ import java.util.concurrent.atomic.AtomicBoolean;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 
-import brooklyn.config.ConfigKey;
-import brooklyn.entity.basic.ConfigKeys;
-
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import brooklyn.BrooklynVersion;
+import brooklyn.config.ConfigKey;
 import brooklyn.entity.Application;
+import brooklyn.entity.basic.ConfigKeys;
 import brooklyn.entity.basic.Entities;
 import brooklyn.entity.basic.EntityLocal;
 import brooklyn.entity.basic.StartableApplication;
@@ -63,6 +62,7 @@ import brooklyn.rest.domain.VersionSummary;
 import brooklyn.rest.transform.HighAvailabilityTransformer;
 import brooklyn.rest.util.WebResourceUtils;
 import brooklyn.util.ResourceUtils;
+import brooklyn.util.collections.MutableMap;
 import brooklyn.util.exceptions.Exceptions;
 import brooklyn.util.file.ArchiveBuilder;
 import brooklyn.util.flags.TypeCoercions;
@@ -283,6 +283,15 @@ public class ServerResource extends AbstractBrooklynRestResource implements Serv
         return true;
     }
     
+    @Override
+    public Map<String,Object> getUpExtended() {
+        return MutableMap.<String,Object>of(
+            "up", isUp(),
+            "healthy", isHealthy(),
+            "ha", getHighAvailabilityPlaneStates());
+    }
+    
+    
     @Deprecated
     @Override
     public String getStatus() {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e629602b/usage/rest-server/src/test/java/brooklyn/rest/BrooklynRestApiLauncher.java
----------------------------------------------------------------------
diff --git a/usage/rest-server/src/test/java/brooklyn/rest/BrooklynRestApiLauncher.java b/usage/rest-server/src/test/java/brooklyn/rest/BrooklynRestApiLauncher.java
index 5643614..9941466 100644
--- a/usage/rest-server/src/test/java/brooklyn/rest/BrooklynRestApiLauncher.java
+++ b/usage/rest-server/src/test/java/brooklyn/rest/BrooklynRestApiLauncher.java
@@ -165,6 +165,7 @@ public class BrooklynRestApiLauncher {
         BrooklynCampPlatformLauncherAbstract platform = new BrooklynCampPlatformLauncherNoServer()
                 .useManagementContext(mgmt)
                 .launch();
+        ((LocalManagementContext)mgmt).noteStartupComplete();
         log.debug("started "+platform);
 
         ContextHandler context;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e629602b/usage/rest-server/src/test/java/brooklyn/rest/HaHotCheckTest.java
----------------------------------------------------------------------
diff --git a/usage/rest-server/src/test/java/brooklyn/rest/HaHotCheckTest.java b/usage/rest-server/src/test/java/brooklyn/rest/HaHotCheckTest.java
index 58a9e6b..e6ca1d1 100644
--- a/usage/rest-server/src/test/java/brooklyn/rest/HaHotCheckTest.java
+++ b/usage/rest-server/src/test/java/brooklyn/rest/HaHotCheckTest.java
@@ -22,20 +22,20 @@ import static org.testng.Assert.assertEquals;
 
 import javax.ws.rs.core.MediaType;
 
-import org.testng.annotations.AfterClass;
-import org.testng.annotations.BeforeClass;
+import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 import brooklyn.management.ha.HighAvailabilityManager;
+import brooklyn.management.ha.HighAvailabilityMode;
 import brooklyn.management.ha.ManagementNodeState;
+import brooklyn.management.internal.LocalManagementContext;
+import brooklyn.management.internal.ManagementContextInternal;
 import brooklyn.rest.filter.HaHotCheckResourceFilter;
 import brooklyn.rest.filter.HaMasterCheckFilter;
 import brooklyn.rest.testing.BrooklynRestResourceTest;
-import brooklyn.rest.testing.mocks.ManagementContextMock;
 import brooklyn.rest.util.HaHotStateCheckClassResource;
 import brooklyn.rest.util.HaHotStateCheckResource;
-import brooklyn.rest.util.ManagementContextProvider;
 
 import com.sun.jersey.api.client.ClientResponse;
 import com.sun.jersey.api.client.WebResource.Builder;
@@ -43,75 +43,61 @@ import com.sun.jersey.api.core.ResourceConfig;
 
 public class HaHotCheckTest extends BrooklynRestResourceTest {
 
-    private ManagementContextMock mgmtMock;
-
-    @Override
-    @BeforeClass(alwaysRun = true)
-    public void setUp() throws Exception {
-        mgmtMock = new ManagementContextMock();
-        super.setUp();
-    }
-
-    @Override
-    @AfterClass(alwaysRun = true)
-    public void tearDown() throws Exception {
-        super.tearDown();
-    }
-
     @BeforeMethod(alwaysRun = true)
-    public void setUpMethod() {
-        mgmtMock.setState(ManagementNodeState.MASTER);
-    }
+    public void setUp() throws Exception { super.setUp(); }
 
+    @AfterMethod(alwaysRun = false)
+    public void tearDown() throws Exception { super.tearDown(); }
+    
     @Override
     protected void addBrooklynResources() {
-        config.getSingletons().add(new ManagementContextProvider(mgmtMock));
-        config.getProperties().put(ResourceConfig.PROPERTY_RESOURCE_FILTER_FACTORIES, HaHotCheckResourceFilter.class.getName());
+        config.getProperties().put(ResourceConfig.PROPERTY_RESOURCE_FILTER_FACTORIES, 
+            new HaHotCheckResourceFilter(getManagementContext()));
         addResource(new HaHotStateCheckResource());
         addResource(new HaHotStateCheckClassResource());
+        
+        ((LocalManagementContext)getManagementContext()).noteStartupComplete();
     }
 
     @Test
     public void testHaCheck() {
-        HighAvailabilityManager ha = mgmtMock.getHighAvailabilityManager();
+        HighAvailabilityManager ha = getManagementContext().getHighAvailabilityManager();
         assertEquals(ha.getNodeState(), ManagementNodeState.MASTER);
         testResourceFetch("/ha/method/ok", 200);
         testResourceFetch("/ha/method/fail", 200);
         testResourceFetch("/ha/class/fail", 200);
 
-        mgmtMock.setState(ManagementNodeState.STANDBY);
+        getManagementContext().getHighAvailabilityManager().changeMode(HighAvailabilityMode.STANDBY);
         assertEquals(ha.getNodeState(), ManagementNodeState.STANDBY);
 
         testResourceFetch("/ha/method/ok", 200);
         testResourceFetch("/ha/method/fail", 403);
         testResourceFetch("/ha/class/fail", 403);
 
-        //forces isRunning = false
-        mgmtMock.setState(ManagementNodeState.TERMINATED);
+        ((ManagementContextInternal)getManagementContext()).terminate();
         assertEquals(ha.getNodeState(), ManagementNodeState.TERMINATED);
 
         testResourceFetch("/ha/method/ok", 200);
-        testResourceFetch("/ha/method/fail", 200);
-        testResourceFetch("/ha/class/fail", 200);
+        testResourceFetch("/ha/method/fail", 403);
+        testResourceFetch("/ha/class/fail", 403);
     }
 
     @Test
     public void testHaCheckForce() {
-        HighAvailabilityManager ha = mgmtMock.getHighAvailabilityManager();
+        HighAvailabilityManager ha = getManagementContext().getHighAvailabilityManager();
         assertEquals(ha.getNodeState(), ManagementNodeState.MASTER);
         testResourceForcedFetch("/ha/method/ok", 200);
         testResourceForcedFetch("/ha/method/fail", 200);
         testResourceForcedFetch("/ha/class/fail", 200);
 
-        mgmtMock.setState(ManagementNodeState.STANDBY);
+        getManagementContext().getHighAvailabilityManager().changeMode(HighAvailabilityMode.STANDBY);
         assertEquals(ha.getNodeState(), ManagementNodeState.STANDBY);
 
         testResourceForcedFetch("/ha/method/ok", 200);
         testResourceForcedFetch("/ha/method/fail", 200);
         testResourceForcedFetch("/ha/class/fail", 200);
 
-        //forces isRunning = false
-        mgmtMock.setState(ManagementNodeState.TERMINATED);
+        ((ManagementContextInternal)getManagementContext()).terminate();
         assertEquals(ha.getNodeState(), ManagementNodeState.TERMINATED);
 
         testResourceForcedFetch("/ha/method/ok", 200);
@@ -119,6 +105,7 @@ public class HaHotCheckTest extends BrooklynRestResourceTest {
         testResourceForcedFetch("/ha/class/fail", 200);
     }
 
+
     private void testResourceFetch(String resourcePath, int code) {
         testResourceFetch(resourcePath, false, code);
     }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e629602b/usage/rest-server/src/test/java/brooklyn/rest/testing/mocks/ManagementContextMock.java
----------------------------------------------------------------------
diff --git a/usage/rest-server/src/test/java/brooklyn/rest/testing/mocks/ManagementContextMock.java b/usage/rest-server/src/test/java/brooklyn/rest/testing/mocks/ManagementContextMock.java
index d7c461e..423743f 100644
--- a/usage/rest-server/src/test/java/brooklyn/rest/testing/mocks/ManagementContextMock.java
+++ b/usage/rest-server/src/test/java/brooklyn/rest/testing/mocks/ManagementContextMock.java
@@ -42,16 +42,26 @@ import brooklyn.management.entitlement.EntitlementManager;
 import brooklyn.management.ha.HighAvailabilityManager;
 import brooklyn.management.ha.ManagementNodeState;
 import brooklyn.util.guava.Maybe;
+import brooklyn.util.javalang.JavaClassNames;
 
 public class ManagementContextMock implements ManagementContext {
+    private Boolean running;
+    private Boolean startupComplete;
     private HighAvailabilityManagerStub haMock = new HighAvailabilityManagerStub();
 
+    public void setRunning(Boolean running) {
+        this.running = running;
+    }
+    public void setStartupComplete(Boolean startupComplete) {
+        this.startupComplete = startupComplete;
+    }
+    
     public void setState(ManagementNodeState state) {
         haMock.setState(state);
     }
 
     private static RuntimeException fail() {
-        throw new UnsupportedOperationException("Mocked method not implemented");
+        throw new UnsupportedOperationException("Mocked method not implemented - "+JavaClassNames.callerNiceClassAndMethod(1));
     }
 
     @Override
@@ -131,12 +141,14 @@ public class ManagementContextMock implements ManagementContext {
 
     @Override
     public boolean isRunning() {
-        throw fail();
+        if (running==null) throw fail();
+        return running;
     }
 
     @Override
     public boolean isStartupComplete() {
-        throw fail();
+        if (startupComplete==null) throw fail();
+        return startupComplete;
     }
     
     @Override


Mime
View raw message