You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@qpid.apache.org by ea...@apache.org on 2017/10/23 21:47:41 UTC

[39/45] qpid-dispatch git commit: DISPATCH-834 Now with ability to deploy with sudo password passed in

DISPATCH-834 Now with ability to deploy with sudo password passed in


Project: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/repo
Commit: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/commit/2f5ec0d8
Tree: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/tree/2f5ec0d8
Diff: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/diff/2f5ec0d8

Branch: refs/heads/master
Commit: 2f5ec0d8948d35c09deb4bcc7a02cfeb0a8341c4
Parents: 06f5f12
Author: Ernest Allen <ea...@redhat.com>
Authored: Fri Oct 13 14:52:35 2017 -0400
Committer: Ernest Allen <ea...@redhat.com>
Committed: Fri Oct 13 14:52:35 2017 -0400

----------------------------------------------------------------------
 console/config/config.py                        |  34 +-
 console/config/css/dispatch.css                 |   1 -
 console/config/css/mock.css                     |  29 +-
 .../config/deployments/install_dispatch.yaml    |  25 +-
 console/config/deployments/run_dispatch.yaml    |  18 +
 console/config/html/qdrTopology.html            |  54 +-
 console/config/js/qdrNewNode.js                 |  16 -
 console/config/js/qdrService.js                 |  49 +-
 console/config/js/qdrTopology.js                | 672 ++++++++++++-------
 9 files changed, 572 insertions(+), 326 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/2f5ec0d8/console/config/config.py
----------------------------------------------------------------------
diff --git a/console/config/config.py b/console/config/config.py
index d17bc34..bae1240 100755
--- a/console/config/config.py
+++ b/console/config/config.py
@@ -32,13 +32,8 @@ import cStringIO
 import yaml
 import threading
 import subprocess
-
-import pdb
 from distutils.spawn import find_executable
 
-def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
-    return ''.join(random.choice(chars) for _ in range(size))
-
 get_class = lambda x: globals()[x]
 sectionKeys = {"log": "module", "sslProfile": "name", "connector": "port", "listener": "port"}
 
@@ -101,6 +96,7 @@ class Manager(object):
         self.verbose = verbose
         self.topo_base = "topologies/"
         self.deploy_base = "deployments/"
+        self.deploy_file = self.deploy_base + "deploy.txt"
         self.state = None
 
     def operation(self, op, request):
@@ -135,6 +131,8 @@ class Manager(object):
     def DEPLOY(self, request):
         nodes = request["nodes"]
         topology = request["topology"]
+        inventory_file = self.deploy_base + "inventory.yml"
+        ansible_become_pass = "ansible_become_pass"
 
         self.PUBLISH(request, deploy=True)
 
@@ -145,7 +143,6 @@ class Manager(object):
         }
         hosts = inventory['deploy_routers']['hosts']
 
-        #pdb.set_trace()
         for node in nodes:
             if node['cls'] == 'router':
                 host = node['host']
@@ -154,37 +151,44 @@ class Manager(object):
                 # if any of the nodes for this host has a console, set create_console for this host to true
                 hosts[host]['create_console'] = (hosts[host]['create_console'] or self.has_console(node))
                 hosts[host]['nodes'].append(node['name'])
+                # pass in the password for eash host if provided
+                if request.get(ansible_become_pass + "_" + host):
+                    hosts[host][ansible_become_pass] = request.get(ansible_become_pass + "_" + host)
                 # local hosts need to be marked as such
                 if host in ('0.0.0.0', 'localhost', '127.0.0.1'):
                     hosts[host]['ansible_connection'] = 'local'
 
-        with open(self.deploy_base + 'inventory.yml', 'w') as n:
+        with open(inventory_file, 'w') as n:
             yaml.safe_dump(inventory, n, default_flow_style=False)
 
-        # start ansible-playbook in separate thread and callback when done
+        # start ansible-playbook in separate thread so we don't have to wait and can still get a callback when done
         def popenCallback(callback, args):
             def popen(callback, args):
                 # send all output to deploy.txt so we can send it to the console in DEPLOY_STATUS
-                with open('deploy.txt', 'w') as fout:
+                with open(self.deploy_file, 'w') as fout:
                     proc = subprocess.Popen(args, stdout=fout, stderr=fout)
                     proc.wait()
-                    callback()
+                    callback(proc.returncode)
                 return
             thread = threading.Thread(target=popen, args=(callback, args))
             thread.start()
 
-        def ansible_done():
+        def ansible_done(returncode):
+            os.remove(inventory_file)
             if self.verbose:
-                print "-------------- DEPLOYMENT DONE ----------------"
-            self.state = "DONE"
+                print "-------------- DEPLOYMENT DONE with return code", returncode, "------------"
+            if returncode:
+                self.state = returncode
+            else:
+                self.state = "DONE"
 
         self.state = "DEPLOYING"
-        popenCallback(ansible_done, ['ansible-playbook', self.deploy_base + 'install_dispatch.yaml', '-i', self.deploy_base + 'inventory.yml'])
+        popenCallback(ansible_done, ['ansible-playbook', self.deploy_base + 'install_dispatch.yaml', '-i', inventory_file])
 
         return "deployment started"
 
     def DEPLOY_STATUS(self, request):
-        with open('deploy.txt', 'r') as fin:
+        with open(self.deploy_file, 'r') as fin:
             content = fin.readlines()
 
         # remove leading blank line

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/2f5ec0d8/console/config/css/dispatch.css
----------------------------------------------------------------------
diff --git a/console/config/css/dispatch.css b/console/config/css/dispatch.css
index f018be1..d5b71d3 100644
--- a/console/config/css/dispatch.css
+++ b/console/config/css/dispatch.css
@@ -94,7 +94,6 @@ circle.node.reflexive {
 circle.node.selected {
   stroke: #6F6 !important;
   stroke-width: 2px;
-  fill: #e0e0ff !important;
 }
 circle.node.highlighted {
   stroke: #6F6;

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/2f5ec0d8/console/config/css/mock.css
----------------------------------------------------------------------
diff --git a/console/config/css/mock.css b/console/config/css/mock.css
index 6ae1156..3faa3f5 100644
--- a/console/config/css/mock.css
+++ b/console/config/css/mock.css
@@ -135,4 +135,31 @@ pre.tail {
   max-height: 15em;
   min-height: 15em;
   overflow-y: scroll;
-}
\ No newline at end of file
+}
+
+circle.node {
+  opacity: 0.5;
+}
+circle.node.inter-router.highlighted {
+  opacity: 1;
+}
+circle.node {
+    stroke-width: 0;
+}
+
+.host_color, .host_name {
+  display: inline-block;
+}
+
+.host_color {
+  width: 1em;
+  height: 1em;
+  border: 0;
+}
+.host_name {
+  font-weight: bold;
+}
+
+.host_pass {
+  float: right;
+}

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/2f5ec0d8/console/config/deployments/install_dispatch.yaml
----------------------------------------------------------------------
diff --git a/console/config/deployments/install_dispatch.yaml b/console/config/deployments/install_dispatch.yaml
index 0ff4107..4c51057 100644
--- a/console/config/deployments/install_dispatch.yaml
+++ b/console/config/deployments/install_dispatch.yaml
@@ -18,13 +18,18 @@
               file:
                 path: /usr/local/share/qpid-dispatch
                 state: directory
-            - name: install console
-              synchronize:
-                src: ../../stand-alone
+
+            - name: archive the console
+              local_action:
+                module: archive
+                dest: standalone.zip
+                path: "{{playbook_dir}}/../../stand-alone"
+                format: zip
+
+            - unarchive:
+                src: standalone.zip
                 dest: /usr/local/share/qpid-dispatch
-                archive: no
-                recursive: yes
-                delete: yes
+
           when: create_console
 
         - name: create directories
@@ -44,9 +49,11 @@
       with_items: '{{ nodes }}'
 
     - name: stop running routers
-      action: shell pkill -f qdrouterd
+      shell: "ps ax | grep qpid-dispatch/{{item}}.conf | grep -v 'grep' | awk -F ' ' '{print $1}' | xargs kill -9"
+      #action: shell pkill -f qdrouterd
       ignore_errors: True
+      with_items: '{{ nodes }}'
 
-    - name: start routers
-      shell: "sleep 1 ; qdrouterd --config /usr/local/etc/qpid-dispatch/{{ item }}.conf -d ; sleep 1"
+    - include_tasks: run_dispatch.yaml
       with_items: "{{ nodes }}"
+

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/2f5ec0d8/console/config/deployments/run_dispatch.yaml
----------------------------------------------------------------------
diff --git a/console/config/deployments/run_dispatch.yaml b/console/config/deployments/run_dispatch.yaml
new file mode 100644
index 0000000..6257ef0
--- /dev/null
+++ b/console/config/deployments/run_dispatch.yaml
@@ -0,0 +1,18 @@
+  - name: 'start router for {{item}}.conf'
+    shell: "qdrouterd --config /usr/local/etc/qpid-dispatch/{{ item }}.conf -d"
+    ignore_errors: True
+    register: qdrouterd
+
+  - name: 'force stop of {{item}}.conf'
+    shell: "ps ax | grep qpid-dispatch/{{item}}.conf | grep -v 'grep' | awk -F ' ' '{print $1}' | xargs kill -9"
+    ignore_errors: True
+    when: qdrouterd.rc == 1
+
+  - name: 'verify router for {{item}}.conf'
+    shell: "ps aux | grep qpid-dispatch/{{item}}.conf | grep -v 'grep'"
+    register: psresult
+    ignore_errors: True
+
+  - name: 'retry {{item}}.conf'
+    shell: "qdrouterd --config /usr/local/etc/qpid-dispatch/{{ item }}.conf -d"
+    when: psresult.rc == 1

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/2f5ec0d8/console/config/html/qdrTopology.html
----------------------------------------------------------------------
diff --git a/console/config/html/qdrTopology.html b/console/config/html/qdrTopology.html
index 7b6f794..0788d4b 100644
--- a/console/config/html/qdrTopology.html
+++ b/console/config/html/qdrTopology.html
@@ -23,11 +23,13 @@ under the License.
             <button class="btn btn-primary" type="button" ng-click="Publish()" title="Save this topology">Publish</button>
             <button class="btn btn-primary" type="button" ng-click="Clear()" title="Remove all routers">Clear</button>
             <button class="btn btn-primary" type="button" ng-click="Deploy()" ng-disabled="!canDeploy()" ng-if="ansible" title="Deploy this topology">Deploy</button>
+            <button class="btn btn-primary pull-right" type="button" ng-click="doHelp()" title="Show help">?</button>
             <button class="btn btn-primary pull-right" type="button" ng-click="doSettings()" title="Show global settings">Settings</button>
             <button class="btn btn-primary pull-right" type="button" ng-click="showNewDlg()" title="Enter a new topology name">New Topology</button>
             <div class="selected-node pull-right">
                 <button class="btn btn-primary" type="button" ng-click="addAnotherNode(true)"><b class="plus caret"></b> Add new router</button>
-                <button id="action_button" class="btn btn-primary" type="button" ng-disabled="!selected_node" ng-click="showActions($event)" title="Show actions on selected router">Actions <b class="down caret"></b></button>
+                <!-- <button id="action_button" class="btn btn-primary" type="button" ng-disabled="!selected_node" ng-click="showActions($event)" title="Show actions on selected router">Actions <b class="down caret"></b></button> -->
+                <button id="multiple_action_button" class="btn btn-primary" type="button" ng-disabled="!anySelectedNodes()" ng-click="showMultiActions($event)" title="Show actions on selected routers">Actions for selected routers<b class="down caret"></b></button>
             </div>
         </div>
         <div id="topology"><!-- d3 toplogy here --></div>
@@ -48,10 +50,10 @@ under the License.
     <div id="action_menu" class="contextMenu">
         <ul>
             <li ng-click="editSection(selected_node, 'router')">Edit router info</li>
-            <li ng-click="setRouterHost(selected_node)">Set router host</li>
+            <li ng-click="setRouterHost()">Set router host</li>
 
             <li class="context-separator"></li>
-            <li ng-click="editSection(selected_node, 'log', 'new')">Add new log section</li>
+            <li ng-click="editSection(false, 'log', 'new')">Add new log section</li>
             <li ng-repeat="log in getSectionList(selected_node, 'log')" ng-click="editSection(selected_node, 'log', log)">Edit/Delete {{log}} log section</li>
 
             <li class="context-separator"></li>
@@ -71,13 +73,24 @@ under the License.
             <!-- <li ng-repeat="listener in getSectionList(selected_node, 'listener')" ng-click="editSection(selected_node, 'listener', listener)">Edit/Delete listener on port {{listener}} </li> -->
 
             <li class="context-separator"></li>
-            <li ng-click="delNode(selected_node)">Delete this node</li>
+            <li ng-click="deleteNode(false)">Delete this node</li>
             <li ng-click="showConfig(selected_node)">Show generated config</li>
         </ul>
     </div>
+    <div id="multiple_action_menu" class="contextMenu">
+        <ul>
+            <li ng-click="setRouterHost(true)">Set host for selected routers</li>
+
+            <li class="context-separator"></li>
+            <li ng-click="editSection(true, 'log', 'new')">Add log section for selected routers</li>
+
+            <li class="context-separator"></li>
+            <li ng-click="deleteNode(true)">Delete selected nodes</li>
+        </ul>
+    </div>
     <div id="client_context_menu" class="contextMenu">
         <ul>
-            <li ng-click="delNode(selected_node)">Delete</li>
+            <li ng-click="deleteNode(false)">Delete</li>
             <li class="context-separator"></li>
             <li ng-click="editThisSection(selected_node)">Edit</li>
         </ul>
@@ -227,14 +240,33 @@ under the License.
         <div class="modal-header">
             <h3 class="modal-title">{{state}}</h3>
         </div>
-        <div class="modal-body">
-            <pre id="deploy_status" class="tail">{{status}}</pre>
-            <div ng-hide="polling" id="message">Deployed.
-                <div ng-show="hasConsole()">Browse to <a ng-href="{{address}}" target="_blank">here</a> to manage the router.</div>
+        <div ng-hide="deploy_state == 'deploying'">
+            <div class="modal-body">
+                <div ng-repeat="host in hosts">
+                    <div class="host_color circle node" ng-class="host.color"></div>
+                    <div class="host_name">{{host.name}}</div>
+                    <button class="btn btn-basic host_pass" ng-click="show_pass(host)" ng-hide="showing_pass == host.name">Sudo Password</button>
+                    <div class="host_pass" ng-show="showing_pass == host.name"><input ng-keyup="$event.keyCode == 13 ? show_pass() : null" focus-when="showing_pass == host.name" ng-model="host.pass" type="password" /> <button class="btn btn-basic" ng-click="show_pass()">OK</button></div>
+                    <ul class="host_nodes">
+                        <li ng-repeat="node in host.nodes">{{node}}</li>
+                    </ul>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button class="btn btn-primary" type="button" ng-click="deploy()">Deploy</button>
+                <button ng-class="close_button_class" class="btn" type="button" ng-click="cancel()">Cancel</button>
             </div>
         </div>
-        <div class="modal-footer">
-            <button class="btn btn-warning" type="button" ng-click="cancel()">Close</button>
+        <div ng-show="deploy_state == 'deploying'">
+            <div class="modal-body">
+                <pre id="deploy_output" class="tail">{{status}}</pre>
+                <div ng-show="done()" id="message">Deployed.
+                    <div ng-show="hasConsole()">Browse to <a ng-href="{{address}}" target="_blank">here</a> to manage the router.</div>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button ng-class="close_button_class" class="btn" type="button" ng-click="cancel()">Close</button>
+            </div>
         </div>
     </form>
 </script>

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/2f5ec0d8/console/config/js/qdrNewNode.js
----------------------------------------------------------------------
diff --git a/console/config/js/qdrNewNode.js b/console/config/js/qdrNewNode.js
index e000727..c66b455 100644
--- a/console/config/js/qdrNewNode.js
+++ b/console/config/js/qdrNewNode.js
@@ -223,22 +223,6 @@ var QDR = (function(QDR) {
             }
           })
         }
-        // add checkbox to apply this log module/enable to all routers
-        $scope.applyLog = {isChecked: false}
-        ediv.attributes.push( {
-          sort: 'last',
-          name: 'apply',
-          humanName: 'Apply to all routers',
-          description: 'Apply this to all routers',
-          type: 'checkbox',
-          rawtype: 'boolean',
-          input: 'checkbox',
-          selected: undefined,
-          'default': false,
-          value: $scope.applyLog.isChecked,
-          required: false,
-          unique: false
-        })
       }
       // sort ediv.attributes on name
       var allNames = ediv.attributes.map( function (attr) {

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/2f5ec0d8/console/config/js/qdrService.js
----------------------------------------------------------------------
diff --git a/console/config/js/qdrService.js b/console/config/js/qdrService.js
index 149f94e..fc8f9eb 100644
--- a/console/config/js/qdrService.js
+++ b/console/config/js/qdrService.js
@@ -146,52 +146,6 @@ var QDR = (function(QDR) {
   };
 })();
 
-function ngGridFlexibleHeightPlugin (opts) {
-    var self = this;
-    self.grid = null;
-    self.scope = null;
-    self.init = function (scope, grid, services) {
-        self.domUtilityService = services.DomUtilityService;
-        self.grid = grid;
-        self.scope = scope;
-        var recalcHeightForData = function () { setTimeout(innerRecalcForData, 1); };
-        var innerRecalcForData = function () {
-            var gridId = self.grid.gridId;
-            var footerPanelSel = '.' + gridId + ' .ngFooterPanel';
-            if (!self.grid.$topPanel || !self.grid.$canvas)
-              return;
-            var extraHeight = self.grid.$topPanel.height() + $(footerPanelSel).height();
-            var naturalHeight = self.grid.$canvas.height() + 1;
-            if (opts != null) {
-                if (opts.minHeight != null && (naturalHeight + extraHeight) < opts.minHeight) {
-                    naturalHeight = opts.minHeight - extraHeight - 2;
-                }
-                if (opts.maxHeight != null && (naturalHeight + extraHeight) > opts.maxHeight) {
-                    naturalHeight = opts.maxHeight;
-                }
-            }
-
-            var newViewportHeight = naturalHeight + 3;
-            if (!self.scope.baseViewportHeight || self.scope.baseViewportHeight !== newViewportHeight) {
-                self.grid.$viewport.css('height', newViewportHeight + 'px');
-                self.grid.$root.css('height', (newViewportHeight + extraHeight) + 'px');
-                self.scope.baseViewportHeight = newViewportHeight;
-                self.domUtilityService.RebuildGrid(self.scope, self.grid);
-            }
-        };
-        self.scope.catHashKeys = function () {
-            var hash = '',
-                idx;
-            for (idx in self.scope.renderedRows) {
-                hash += self.scope.renderedRows[idx].$$hashKey;
-            }
-            return hash;
-        };
-        self.scope.$watch('catHashKeys()', innerRecalcForData);
-        self.scope.$watch(self.grid.config.data, recalcHeightForData);
-    };
-}
-
 if (!String.prototype.startsWith) {
   String.prototype.startsWith = function (searchString, position) {
     return this.substr(position || 0, searchString.length) === searchString
@@ -324,4 +278,5 @@ if (!Array.prototype.findIndex) {
       return -1;
     }
   });
-}
\ No newline at end of file
+}
+

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/2f5ec0d8/console/config/js/qdrTopology.js
----------------------------------------------------------------------
diff --git a/console/config/js/qdrTopology.js b/console/config/js/qdrTopology.js
index 421191b..535bb52 100644
--- a/console/config/js/qdrTopology.js
+++ b/console/config/js/qdrTopology.js
@@ -31,6 +31,19 @@ var QDR = (function(QDR) {
 
       var settings = {baseName: "A", http_port: 5673, normal_port: 22000, internal_port: 2000, default_host: "0.0.0.0", artemis_port: 61616, qpid_port: 5672}
       var sections = ['log', 'connector', 'sslProfile', 'listener']
+      // mouse event vars
+      var selected_link = null,
+        mousedown_link = null,
+        mousedown_node = null,
+        mouseover_node = null,
+        mouseup_node = null,
+        initial_mouse_down_position = null;
+
+      $scope.selected_node = null
+      $scope.selected_nodes = {}
+      $scope.mockTopologies = []
+      $scope.mockTopologyDir = ""
+      $scope.ansible = false
 
       $scope.Publish = function () {
         doOperation("PUBLISH", function (response) {
@@ -38,13 +51,20 @@ var QDR = (function(QDR) {
           QDR.log.info("published " + $scope.mockTopologyDir)
         })
       }
-      $scope.Deploy = function () {
+      var startDeploy = function (hosts) {
+        var extra = {}
+        for (var i=0; i<hosts.length; i++) {
+          if (hosts[i].pass !== '')
+            extra['ansible_become_pass_' + hosts[i].name] = hosts[i].pass
+        }
         // send the deploy command
         doOperation("DEPLOY", function (response) {
           QDR.log.info("deployment " + $scope.mockTopologyDir + " started")
-        })
-        // show the deploy status dialog
-        doDeployDialog()
+        }, extra)
+      }
+      $scope.Deploy = function () {
+        // show the deploy dialog
+        doDeployDialog(startDeploy)
       }
 
       $scope.showConfig = function (node) {
@@ -143,6 +163,7 @@ var QDR = (function(QDR) {
       $scope.Clear = function () {
         nodes = []
         links = []
+        $scope.selected_nodes = {}
         $scope.selected_node = null
         resetMouseVars()
         force.nodes(nodes).links(links).start();
@@ -151,7 +172,25 @@ var QDR = (function(QDR) {
         })
       }
 
-      $scope.delNode = function (node, skipinit) {
+      $scope.deleteNode = function (multi) {
+        if (multi) {
+          var del = true
+          while (del) {
+            del = false
+            for (var i=0; i<nodes.length; i++) {
+              if (isSelectedNode(nodes[i])) {
+                delNode(nodes[i])   // this changes nodes. we can't continue looping
+                del = true
+                break;
+              }
+            }
+          }
+        } else {
+          delNode($scope.selected_node)
+        }
+      }
+
+      var delNode = function (node, skipinit) {
         // loop through all the nodes
         if (node.nodeType !== 'inter-router') {
           var n = findParentNode(node)
@@ -170,8 +209,9 @@ var QDR = (function(QDR) {
             }
           }
           for (var x=0; x<sub_nodes.length; x++) {
-            $scope.delNode(sub_nodes[x], true)
+            delNode(sub_nodes[x], true)
           }
+          removeSelectedNode(node)
         }
         // find the index of the node
         var index = nodes.findIndex( function (n) {return n.name === node.name})
@@ -192,6 +232,8 @@ var QDR = (function(QDR) {
           l.uid = l.source.id + "." + l.target.id
         })
 
+        $scope.selected_node = null
+
         if (!skipinit) {
           animate = true
           initGraph()
@@ -200,6 +242,14 @@ var QDR = (function(QDR) {
         }
       }
 
+      $scope.showMultiActions = function (e) {
+        $(document).click();
+        e.stopPropagation()
+        var position = $('#multiple_action_button').position()
+        position.top += $('#multiple_action_button').height() + 8
+        position['display'] = "block"
+        $('#multiple_action_menu').css(position)
+      }
       $scope.showActions = function (e) {
         $(document).click();
         e.stopPropagation()
@@ -269,15 +319,6 @@ var QDR = (function(QDR) {
         $scope.addingNode.step = 0;
       }
 
-      $scope.selected_node = null
-      // mouse event vars
-      var selected_link = null,
-        mousedown_link = null,
-        mousedown_node = null,
-        mouseover_node = null,
-        mouseup_node = null,
-        initial_mouse_down_position = null;
-
       $scope.hasConsoleListener = function (node) {
         if (!node) {
           for (var i=0; i<nodes.length; i++) {
@@ -315,7 +356,7 @@ var QDR = (function(QDR) {
         // find the actual console listener
         var n = findChildNode('listener', settings.http_port, node.name)
         if (n)
-          $scope.delNode(n)
+          delNode(n)
       }
 
       var yoffset = 1; // toggles between 1 and -1. used to position new nodes
@@ -443,18 +484,22 @@ var QDR = (function(QDR) {
         }
       }
       // menu item of router to set host of all listeners
-      $scope.setRouterHost = function (node) {
-        doSetRouterHostDialog(node)
+      $scope.setRouterHost = function (multi) {
+        if (!$scope.selected_node)
+          $scope.selected_node = firstSelectedNode()
+        doSetRouterHostDialog($scope.selected_node, multi)
       }
       // menu item of router to edit one of its sub-entities
-      $scope.editSection = function (node, type, section) {
-        doEditDialog(node, type, section)
+      $scope.editSection = function (multi, type, section) {
+        if (!$scope.selected_node)
+          $scope.selected_node = firstSelectedNode()
+        doEditDialog($scope.selected_node, type, section, multi)
       }
       // menu item of sub-entity to edit itself
       $scope.editThisSection = function (node) {
         var n = findParentNode(node)
         if (n)
-          doEditDialog(n, node.entity, node.entityKey)
+          doEditDialog(n, node.entity, node.entityKey, false)
       }
 
       var mouseX, mouseY;
@@ -473,6 +518,7 @@ var QDR = (function(QDR) {
       $(document).mousemove();
       $(document).click(function(e) {
         $("#svg_context_menu").fadeOut(200);
+        $("#multiple_action_menu").fadeOut(200);
         $("#action_menu").fadeOut(200);
         $("#link_context_menu").fadeOut(200);
         $("#client_context_menu").fadeOut(200);
@@ -482,6 +528,7 @@ var QDR = (function(QDR) {
         $('.hastip').empty();
         d3.select("#multiple_details").style("display", "none")
         d3.select("#link_details").style("display", "none")
+        d3.select('#multiple_action_menu').style('display', 'none');
         d3.select('#action_menu').style('display', 'none');
         d3.select('#svg_context_menu').style('display', 'none');
         d3.select('#link_context_menu').style('display', 'none');
@@ -492,7 +539,8 @@ var QDR = (function(QDR) {
         'inter-router': 25,
         'normal': 15,
         'on-demand': 15,
-        'route-container': 15
+        'route-container': 15,
+        'host': 20
       };
       var radius = 25;
       var radiusNormal = 15;
@@ -674,6 +722,21 @@ var QDR = (function(QDR) {
           .append("svg:path")
             .attr('d', 'M 10 -5 L 0 0 L 10 5 z');
 
+        var hostColors = d3.scale.category10();
+        var colors = []
+        for (var i=0; i<10; i++) {
+          colors.push(hostColors(i))
+        }
+        svg.append("svg:defs").selectAll('pattern')
+          .data(colors)
+          .enter().append("pattern")
+          .attr({ id:"host"+i, width:"8", height:"8", patternUnits:"userSpaceOnUse", patternTransform:"rotate(-45)"})
+          .attr('id', function (d, i) {return 'host_'+i})
+          .attr({width:"4", height:"8", patternUnits:"userSpaceOnUse", patternTransform:"rotate(-45)"})
+          .append("rect")
+          .attr('fill', function (d) {return d})
+          .attr({ width:"2", height:"8", transform:"translate(0,0)"});
+
         // handles to link and node element groups
         path = svg.append('svg:g').selectAll('path')
         circle = svg.append('svg:g').selectAll('g')
@@ -719,7 +782,8 @@ var QDR = (function(QDR) {
       var initForceGraph = function() {
 
         mouseover_node = null;
-        $scope.selected_node = null;
+        $scope.selected_nodes = {};
+        $scope.selected_node = null
         selected_link = null;
 
         initGraph()
@@ -814,8 +878,8 @@ var QDR = (function(QDR) {
             targetPadding = d.right ? radius + 16 : radius;
           } else {
             r = radiusNormal - 18;
-            sourcePadding = d.left ? radiusNormal + 18 : radiusNormal;
-            targetPadding = d.right ? radiusNormal + 16 : radiusNormal;
+            sourcePadding = d.left ? radiusNormal + 18 : radiusNormal + 10;
+            targetPadding = d.right ? radiusNormal + 6 : radiusNormal;
           }
           var dtx = Math.max(targetPadding, Math.min(width - r, d.target.x)),
             dty = Math.max(targetPadding, Math.min(height - r, d.target.y)),
@@ -875,13 +939,148 @@ var QDR = (function(QDR) {
           })
       }
 
-      function clerAllHighlights() {
-        for (var i = 0; i < links.length; ++i) {
-          links[i]['highlighted'] = false;
-        }
-        for (var i=0; i<nodes.length; ++i) {
-          nodes[i]['highlighted'] = false;
-        }
+      var appendTitle = function(g) {
+        g.append("svg:title").text(function(d) {
+          if (QDRService.isConsole(d)) {
+            return 'Dispatch console'
+          }
+          if (d.properties.product == 'qpid-cpp') {
+            return 'Broker - Qpid'
+          }
+          if (QDRService.isArtemis(d)) {
+            return 'Broker - Artemis'
+          }
+          if (d.cls === 'log') {
+            return 'Log' + (d.entityKey ? (': ' + d.entityKey) : '')
+          }
+          if (d.cdir === 'in')
+            return 'Listener on port ' + d.entityKey
+          if (d.cdir === 'out')
+            return 'Connector to ' + d.host + ':' + d.entityKey
+          if (d.cdir === 'both')
+            return 'sslProfile'
+          if (d.cls === 'host')
+            return 'Host ' + d.name
+          return d.nodeType == 'normal' ? 'client' : (d.nodeType == 'route-container' ? 'broker' : 'Router ' + d.name)
+        })
+      }
+
+      var appendCircle = function(g) {
+        // add new circles and set their attr/class/behavior
+        return g.append('svg:circle')
+          .attr('class', 'node')
+          .attr('r', function(d) {
+            return radii[d.nodeType]
+          })
+          .classed('normal', function(d) {
+            return d.nodeType == 'normal' || QDRService.isConsole(d)
+          })
+          .classed('in', function(d) {
+            return d.cdir == 'in'
+          })
+          .classed('out', function(d) {
+            return d.cdir == 'out'
+          })
+          .classed('connector', function(d) {
+            return d.cls == 'connector'
+          })
+          .classed('listener', function(d) {
+            return d.cls == 'listener'
+          })
+          .classed('selected', function (d) {
+            return $scope.selected_node === d
+          })
+          .classed('inout', function(d) {
+            return d.cdir == 'both'
+          })
+          .classed('inter-router', function(d) {
+            return d.nodeType == 'inter-router'
+          })
+          .classed('on-demand', function(d) {
+            return d.nodeType == 'route-container'
+          })
+          .classed('log', function(d) {
+            return d.cls === 'log'
+          })
+          .classed('console', function(d) {
+            return QDRService.isConsole(d)
+          })
+          .classed('artemis', function(d) {
+            return QDRService.isArtemis(d)
+          })
+          .classed('qpid-cpp', function(d) {
+            return QDRService.isQpid(d)
+          })
+          .classed('route-container', function (d) {
+            return (!QDRService.isArtemis(d) && !QDRService.isQpid(d) && d.nodeType === 'route-container')
+          })
+          .classed('client', function(d) {
+            return d.nodeType === 'normal' && !d.properties.console_identifier
+          })
+      }
+
+      var appendContent = function(g) {
+        // show node IDs
+        g.append('svg:text')
+          .attr('x', 0)
+          .attr('y', function(d) {
+            var y = 7;
+            if (QDRService.isArtemis(d))
+              y = 8;
+            else if (QDRService.isQpid(d))
+              y = 9;
+            else if (d.nodeType === 'inter-router')
+              y = 4;
+            return y;
+          })
+          .attr('class', 'id')
+          .classed('log', function(d) {
+            return d.cls === 'log'
+          })
+          .classed('console', function(d) {
+            return QDRService.isConsole(d)
+          })
+          .classed('normal', function(d) {
+            return d.nodeType === 'normal'
+          })
+          .classed('on-demand', function(d) {
+            return d.nodeType === 'on-demand'
+          })
+          .classed('artemis', function(d) {
+            return QDRService.isArtemis(d)
+          })
+          .classed('qpid-cpp', function(d) {
+            return QDRService.isQpid(d)
+          })
+          .text(function(d) {
+            if (QDRService.isConsole(d)) {
+              return '\uf108'; // icon-desktop for this console
+            } else if (QDRService.isArtemis(d)) {
+              return '\ue900'
+            } else if (QDRService.isQpid(d)) {
+              return '\ue901';
+            } else if (d.cls === 'log') {
+              return '\uf036';
+            } else if (d.nodeType === 'route-container') {
+              return d.properties.product ? d.properties.product[0].toUpperCase() : 'S'
+            } else if (d.nodeType === 'normal' && d.cdir === "in") // listener
+                return '\uf2a0'; // phone top
+              else if (d.nodeType === 'normal' && d.cdir === "out") // connector
+                return '\uf2a0'; // phone top (will be rotated)
+              else if (d.nodeType === 'normal' && d.cdir === "both") // not used
+                return '\uf023'; // icon-laptop for clients
+              else if (d.nodeType === 'host')
+                return ''
+
+            return d.name.length > 7 ? d.name.substr(0, 6) + '...' : d.name;
+          })
+          // rotatie the listener icon 180 degrees to use as the connector icon
+         .attr("transform", function (d) {
+            var nAngle = 0
+            if (d.nodeType === 'normal' && d.cdir === "out" && d.cls === 'connector')
+              nAngle = 180
+            return "rotate("+nAngle+")"
+          });
       }
 
       // takes the nodes and links array of objects and adds svg elements for everything that hasn't already
@@ -971,31 +1170,7 @@ var QDR = (function(QDR) {
 
         // circle (node) group
         // nodes are known by id
-        circle = circle.data(nodes, function(d) { return d.name });
-
-        var appendTitle = function(g) {
-          g.append("svg:title").text(function(d) {
-            if (QDRService.isConsole(d)) {
-              return 'Dispatch console'
-            }
-            if (d.properties.product == 'qpid-cpp') {
-              return 'Broker - Qpid'
-            }
-            if (QDRService.isArtemis(d)) {
-              return 'Broker - Artemis'
-            }
-            if (d.cls === 'log') {
-              return 'Log' + (d.entityKey ? (': ' + d.entityKey) : '')
-            }
-            if (d.cdir === 'in')
-              return 'Listener on port ' + d.entityKey
-            if (d.cdir === 'out')
-              return 'Connector to ' + d.host + ':' + d.entityKey
-            if (d.cdir === 'both')
-              return 'sslProfile'
-            return d.nodeType == 'normal' ? 'client' : (d.nodeType == 'route-container' ? 'broker' : 'Router ' + d.name)
-          })
-        }
+        circle = circle.data(nodes, function(d) { return d.name + d.host });
 
         // update existing nodes visual states
         circle.selectAll('circle')
@@ -1022,59 +1197,6 @@ var QDR = (function(QDR) {
             return (d.normals && d.normals.length > 1)
           })
 
-        var appendCircle = function(g) {
-          // add new circles and set their attr/class/behavior
-          return g.append('svg:circle')
-            .attr('class', 'node')
-            .attr('r', function(d) {
-              return radii[d.nodeType]
-            })
-            .classed('normal', function(d) {
-              return d.nodeType == 'normal' || QDRService.isConsole(d)
-            })
-            .classed('in', function(d) {
-              return d.cdir == 'in'
-            })
-            .classed('out', function(d) {
-              return d.cdir == 'out'
-            })
-            .classed('connector', function(d) {
-              return d.cls == 'connector'
-            })
-            .classed('listener', function(d) {
-              return d.cls == 'listener'
-            })
-            .classed('selected', function (d) {
-              return $scope.selected_node === d
-            })
-            .classed('inout', function(d) {
-              return d.cdir == 'both'
-            })
-            .classed('inter-router', function(d) {
-              return d.nodeType == 'inter-router'
-            })
-            .classed('on-demand', function(d) {
-              return d.nodeType == 'route-container'
-            })
-            .classed('log', function(d) {
-              return d.cls === 'log'
-            })
-            .classed('console', function(d) {
-              return QDRService.isConsole(d)
-            })
-            .classed('artemis', function(d) {
-              return QDRService.isArtemis(d)
-            })
-            .classed('qpid-cpp', function(d) {
-              return QDRService.isQpid(d)
-            })
-            .classed('route-container', function (d) {
-              return (!QDRService.isArtemis(d) && !QDRService.isQpid(d) && d.nodeType === 'route-container')
-            })
-            .classed('client', function(d) {
-              return d.nodeType === 'normal' && !d.properties.console_identifier
-            })
-        }
         appendCircle(g)
           .on('mouseover', function(d) {  // mouseover a circle
             if ($scope.addingNode.step > 0) {
@@ -1087,16 +1209,10 @@ var QDR = (function(QDR) {
             // enlarge target node
             d3.select(this).attr('transform', 'scale(1.1)');
             mousedown_node = null;
-
-            if (!$scope.selected_node) {
-              return;
-            }
-            clerAllHighlights()
           })
           .on('mouseout', function(d) { // mouse out for a circle
             // unenlarge target node
             d3.select(this).attr('transform', '');
-            clerAllHighlights()
             mouseover_node = null;
             restart();
           })
@@ -1125,6 +1241,9 @@ var QDR = (function(QDR) {
               Math.abs(cur_mouse[1] - initial_mouse_down_position[1]) > 4) {
               return
             }
+            if (d3.event.ctrlKey && d.cls === 'router') {
+              return
+            }
             // we want a link between the selected_node and this node
             if ($scope.selected_node && d !== $scope.selected_node) {
               if (d.nodeType !== 'inter-router')
@@ -1159,7 +1278,6 @@ var QDR = (function(QDR) {
               if (d.nodeType !== 'normal' && d.nodeType !== 'on-demand')
                 $scope.selected_node = mousedown_node;
             }
-            clerAllHighlights()
             mousedown_node = null;
             if (!$scope.$$phase) $scope.$apply()
             restart(false);
@@ -1173,6 +1291,8 @@ var QDR = (function(QDR) {
             if (!$scope.$$phase) $scope.$apply() // we just changed a scope variable during an async event
             var rm = relativeMouse()
             var menu = d.nodeType === 'inter-router' ? 'action_menu' : 'client_context_menu'
+            if (isSelectedNode(d))
+              menu = 'multiple_action_menu'
             d3.select('#'+menu)
               .style('left', rm.left + "px")
               .style('top', (rm.top - rm.offset.top) + "px")
@@ -1181,6 +1301,11 @@ var QDR = (function(QDR) {
           .on("click", function(d) {  // circle
             if (!mouseup_node)
               return;
+            if (d3.event.ctrlKey && d.cls === 'router') {
+              $timeout( function () {
+                toggleSelectedNode(d)
+              })
+            }
             // clicked on a circle
             clearPopups();
             clickPos = d3.mouse(this);
@@ -1188,69 +1313,7 @@ var QDR = (function(QDR) {
           })
         //.attr("transform", function (d) {return "scale(" + (d.nodeType === 'normal' ? .5 : 1) + ")"})
         //.transition().duration(function (d) {return d.nodeType === 'normal' ? 3000 : 0}).ease("elastic").attr("transform", "scale(1)")
-
-        var appendContent = function(g) {
-          // show node IDs
-          g.append('svg:text')
-            .attr('x', 0)
-            .attr('y', function(d) {
-              var y = 7;
-              if (QDRService.isArtemis(d))
-                y = 8;
-              else if (QDRService.isQpid(d))
-                y = 9;
-              else if (d.nodeType === 'inter-router')
-                y = 4;
-              return y;
-            })
-            .attr('class', 'id')
-            .classed('log', function(d) {
-              return d.cls === 'log'
-            })
-            .classed('console', function(d) {
-              return QDRService.isConsole(d)
-            })
-            .classed('normal', function(d) {
-              return d.nodeType === 'normal'
-            })
-            .classed('on-demand', function(d) {
-              return d.nodeType === 'on-demand'
-            })
-            .classed('artemis', function(d) {
-              return QDRService.isArtemis(d)
-            })
-            .classed('qpid-cpp', function(d) {
-              return QDRService.isQpid(d)
-            })
-            .text(function(d) {
-              if (QDRService.isConsole(d)) {
-                return '\uf108'; // icon-desktop for this console
-              } else if (QDRService.isArtemis(d)) {
-                return '\ue900'
-              } else if (QDRService.isQpid(d)) {
-                return '\ue901';
-              } else if (d.cls === 'log') {
-                return '\uf036';
-              } else if (d.nodeType === 'route-container') {
-                return d.properties.product ? d.properties.product[0].toUpperCase() : 'S'
-              } else if (d.nodeType === 'normal' && d.cdir === "in") // listener
-                  return '\uf2a0'; // phone top
-                else if (d.nodeType === 'normal' && d.cdir === "out") // connector
-                  return '\uf2a0'; // phone top (will be rotated)
-                else if (d.nodeType === 'normal' && d.cdir === "both") // not used
-                  return '\uf023'; // icon-laptop for clients
-
-              return d.name.length > 7 ? d.name.substr(0, 6) + '...' : d.name;
-            })
-            // rotatie the listener icon 180 degrees to use as the connector icon
-           .attr("transform", function (d) {
-              var nAngle = 0
-              if (d.nodeType === 'normal' && d.cdir === "out" && d.cls === 'connector')
-                nAngle = 180
-              return "rotate("+nAngle+")"
-            });
-        }
-
+        resetCircleHosts()
         appendContent(g)
         appendTitle(g);
 
@@ -1267,15 +1330,22 @@ var QDR = (function(QDR) {
 
         // dynamically create the legend based on which node types are present
         // the legend
-        d3.select("#svg_legend svg").remove();
-        lsvg = d3.select("#svg_legend")
-          .append('svg')
-          .attr('id', 'svglegend')
-        lsvg = lsvg.append('svg:g')
-          .attr('transform', 'translate(' + (radii['inter-router'] + 2) + ',' + (radii['inter-router'] + 2) + ')')
-          .selectAll('g');
+        updateLegend()
+
+        if (!mousedown_node || !$scope.selected_node)
+          return;
+
+        if (!start)
+          return;
+        // set the graph in motion
+        force.start();
+
+      }
+
+      function updateLegend() {
+        initLegend()
         var legendNodes = [];
-        legendNodes.push(aNode("Router", "", "inter-router", 'router', 0, 0, 0, 0, false, {}))
+        //legendNodes.push(aNode("Router", "", "inter-router", 'router', 0, 0, 0, 0, false, {}))
 
         if (!svg.selectAll('circle.console').empty()) {
           legendNodes.push(aNode("Console", "", "normal", 'console', 1, 0, 0, 0, false, {
@@ -1310,16 +1380,24 @@ var QDR = (function(QDR) {
           legendNodes.push(aNode("Service", "", "route-container", 'service', 8, 0, 0, 0, false,
           {product: ' External Service'}))
         }
+        // add a circle for each unique host in nodes
+        var hosts = getHosts()
+        for (var i=0; i<hosts.length; i++) {
+          var host = hosts[i]
+          legendNodes.push({key: host, name: host, nodeType: 'host', id: getHostIndex(host), properties: {}, host: host, cls: 'host'})
+        }
+
         lsvg = lsvg.data(legendNodes, function(d) {
           return d.key;
         });
         var lg = lsvg.enter().append('svg:g')
           .attr('transform', function(d, i) {
             // 45px between lines and add 10px space after 1st line
-            return "translate(0, " + (45 * i + (i > 0 ? 10 : 0)) + ")"
+            return "translate(0, " + (45 * i) + ")"
           })
 
         appendCircle(lg)
+        resetCircleHosts()
         appendContent(lg)
         appendTitle(lg)
         lg.append('svg:text')
@@ -1348,16 +1426,47 @@ var QDR = (function(QDR) {
           svgEl.style.width = (bb.x + bb.width) + 'px';
         }
 
-        if (!mousedown_node || !$scope.selected_node)
-          return;
+      }
 
-        if (!start)
-          return;
-        // set the graph in motion
-        force.start();
+      // Ensure a host name will always return the same number (for this session)
+      // This is to make sure the colors associated with host names don't change
+      // In other words, if you have a node with host==localhost and then delete it, all the
+      // other host colors won't change and if you re-add localhost it would get the original color
+      var knownHosts = {}
+      function initHosts() {
+        var hosts = getHosts()
+        var hostColors = d3.scale.category10();
+        for (var i=0; i<hosts.length; i++) {
+          knownHosts[hosts[i]] = i
+        }
 
-      }
+        var style = document.createElement("style");
+        style.appendChild(document.createTextNode(""));
+        document.head.appendChild(style);
+        for (var i=0; i<10; i++) {
+          //console.log("adding rule " + "circle.node.host"+i + ", fill: " + hostColors(i) + ";")
+          style.sheet.addRule("circle.node.host"+i, "fill: " + hostColors(i) + ";", 0);
+          style.sheet.addRule("circle.node.host"+i+".multi-selected", "fill: url(#host_"+i+")", 1);
+          style.sheet.addRule("div.node.host"+i, "background-color: " + hostColors(i) + ";", 2);
+        }
 
+      }
+      function getHosts() {
+        var hosts = {}
+        for (var i=0; i<nodes.length; i++) {
+          var node = nodes[i]
+          if (node.host && node.cls === 'router')
+            hosts[node.host] = 1
+        }
+        return Object.keys(hosts)
+      }
+      initHosts()
+      function getHostIndex(h) {
+        if (h in knownHosts)
+          return knownHosts[h]
+        knownHosts[h] = Object.keys(knownHosts).length
+        return knownHosts[h]
+      }
       function mousedown() {
         // prevent I-bar on drag
         //d3.event.preventDefault();
@@ -1379,9 +1488,6 @@ var QDR = (function(QDR) {
         window.removeEventListener('resize', resize);
       });
 
-      $scope.mockTopologies = []
-      $scope.mockTopologyDir = ""
-      $scope.ansible = false
       QDRService.sendMethod("ANSIBLE-INSTALLED", {}, function (response) {
         $scope.ansible = (response !== "")
       })
@@ -1410,8 +1516,55 @@ var QDR = (function(QDR) {
           }
         });
       }
+      // set the host# class for all inter-router circles
+      function resetCircleHosts() {
+        var sel = d3.selectAll('circle.node')
+        for (var i=0; i<10; i++) {
+          sel.classed('host'+i, function (d) {
+            return i === getHostIndex(d.host) && (d.cls === 'router' || d.cls === 'host')
+          })
+        }
+      }
+
+      function firstSelectedNode() {
+        return $scope.selected_nodes[Object.keys($scope.selected_nodes)[0]]
+      }
+      function isSelectedNode(d) {
+        return d.name in $scope.selected_nodes
+      }
+      $scope.anySelectedNodes = function() {
+        return (Object.keys($scope.selected_nodes).length > 0)
+      }
+      function updateSelectedNodes() {
+        d3.selectAll('circle.node.inter-router')
+          .classed('multi-selected', function (d) {
+            return d.name in $scope.selected_nodes
+          })
+          .attr('fill', function (d) {
+            var hostIndex = getHostIndex(d.host)
+            return "url(#host_" + hostIndex+")"
+          })
+        restart();
+      }
+      function removeSelectedNode(d) {
+        if (d.name in $scope.selected_nodes) {
+          delete $scope.selected_nodes[d.name]
+          updateSelectedNodes()
+        }
+      }
+      function toggleSelectedNode(d) {
+        if (d.name in $scope.selected_nodes)
+          delete $scope.selected_nodes[d.name]
+        else
+          $scope.selected_nodes[d.name] = d
+        updateSelectedNodes()
+      }
+      function addSelectedNode(d) {
+        $scope.selected_nodes[d.name] = d
+        updateSelectedNodes()
+      }
 
-      function doSetRouterHostDialog(node) {
+      function doSetRouterHostDialog(node, multi) {
         var d = $uibModal.open({
           dialogClass: "modal dlg-large",
           backdrop: true,
@@ -1429,13 +1582,24 @@ var QDR = (function(QDR) {
         $timeout(function () {
           d.result.then(function(result) {
             if (result) {
-              node.host = result.host
-              // loop through all listeners and set the host
-              if (node.listeners) {
-                for (var listener in node.listeners) {
-                  node.listeners[listener].host = result.host
+              function setAHost(n) {
+                n.host = result.host
+                // loop through all listeners and set the host
+                if (node.listeners) {
+                  for (var listener in n.listeners) {
+                    n.listeners[listener].host = result.host
+                  }
                 }
               }
+              if (multi) {
+                for (var i=0; i<nodes.length; i++) {
+                  if (isSelectedNode(nodes[i]))
+                    setAHost(nodes[i])
+                }
+              } else
+                setAHost(node)
+              resetCircleHosts()
+              restart()
             }
           });
         })
@@ -1468,7 +1632,7 @@ var QDR = (function(QDR) {
           });
         })
       };
-      function doDeployDialog() {
+      function doDeployDialog(startFn) {
         var host = undefined
         var port = undefined
         for (var i=0; i<nodes.length; i++) {
@@ -1499,6 +1663,25 @@ var QDR = (function(QDR) {
             },
             http_port: function () {
               return port
+            },
+            start_fn: function () {
+              return startFn
+            },
+            hosts: function () {
+              var hosts = []
+              var hostNames = getHosts()
+              for (var i=0; i<hostNames.length; i++) {
+                var hostName = hostNames[i]
+                var hostIndex = getHostIndex(hostName)
+                var hostNodes = []
+                for (var j=0; j<nodes.length; j++) {
+                  if (nodes[j].host === hostName && nodes[j].cls === 'router') {
+                    hostNodes.push(nodes[j].name)
+                  }
+                }
+                hosts.push({name: hostName, nodes: hostNodes, color: 'host'+hostIndex, pass: ''})
+              }
+              return hosts
             }
           }
         });
@@ -1540,7 +1723,7 @@ var QDR = (function(QDR) {
         return undefined
       }
 
-      function doEditDialog(node, entity, context) {
+      function doEditDialog(node, entity, context, multi) {
         var entity2key = {router: 'name', log: 'module', sslProfile: 'name', connector: 'port', listener: 'port'}
         var d = $uibModal.open({
           dialogClass: "modal dlg-large",
@@ -1601,7 +1784,7 @@ var QDR = (function(QDR) {
                   // find the 'normal' node that is associated with this entry
                   var n = findChildNode(entity, context, node.name)
                   if (n)
-                    $scope.delNode(n)
+                    delNode(n)
                 } else {
                   var rVals = valFromMapArray(result.entities, "actualName", entity)
                   if (rVals) {
@@ -1615,10 +1798,10 @@ var QDR = (function(QDR) {
                       delete nodeObj[context]
                     }
                     if (entity === 'log') {
-                      if (o.node.apply) {
-                        // apply this log module/enable to all routers
+                      if (multi) {
+                        // apply this log module/enable to all selected routers
                         for (var i=0; i<nodes.length; i++) {
-                          if (nodes[i].nodeType === 'inter-router') {
+                          if (isSelectedNode(nodes[i])) {
                             var logs = nodes[i]['logs']
                             if (!logs)
                               nodes[i]['logs'] = {}
@@ -1713,8 +1896,7 @@ var QDR = (function(QDR) {
                       {name: "baseName", humanName: "Starting router name", input: "input", type: "text", value: local_settings.baseName, required: true},
                       {name: "http", humanName: "Port for console listeners", input: "input", type: "text", value: local_settings.http_port, required: true},
                       {name: "normal_port", humanName: "Starting port for normal listeners/connectors", input: "input", type: "text", value: local_settings.normal_port, required: true},
-                      {name: "internal_port", humanName: "Starting port for inter-router listeners/connectors", input: "input", type: "text", value: local_settings.internal_port, required: true},
-                      {name: "default_host", humanName: "Default host for inter-router listeners/connectors", input: "input", type: "text", value: local_settings.default_host, required: true},
+                      {name: "internal_port", humanName: "Starting port for inter-router listeners/connectors", input: "input", type: "text", value: local_settings.internal_port, required: true}
                     ]}
 
     $scope.setSettings = function () {
@@ -1731,36 +1913,74 @@ var QDR = (function(QDR) {
 
   })
 
-  QDR.module.controller("QDR.DeployDialogController", function($scope, $uibModalInstance, QDRService, $timeout, $sce, dir, http_host, http_port) {
+  QDR.module.directive('focusWhen', function($timeout) {
+    return {
+      restrict : 'A',
+      link : function($scope, $element, $attr) {
+        $scope.$watch($attr.focusWhen, function(val) {
+          $timeout(function() {
+            if (val)
+              $element[0].focus()
+          });
+        });
+      }
+    }
+  })
+
+  QDR.module.controller("QDR.DeployDialogController", function($scope, $uibModalInstance, QDRService, $timeout, $sce, dir, http_host, http_port, hosts, start_fn) {
     // setup polling to get deployment status
     $scope.polling = true
-    $scope.state = "Deploying"
+    $scope.state = "Deploying " + dir
+    $scope.deploy_state = ""
+    $scope.close_button_class = "btn-warning"
+    var success_state = "Deploy Completed"
     $scope.address = ""
     var pollTimer = null
     function doPoll() {
       QDRService.sendMethod("DEPLOY-STATUS", {config: dir}, function (response) {
-        if (response[1] === 'DONE') {
+        if (response[1] !== 'DEPLOYING') {
           $scope.polling = false
-          $scope.state = "Deploy Completed"
-          Core.notification('info', dir + " deployed");
-          if (http_host && http_port) {
-            $scope.address = $sce.trustAsHtml("http://" + http_host + ":" + http_port + "/#!/topology")
+          if (response[1] === "DONE") {
+            $scope.state = success_state
+            $scope.close_button_class = "btn-success"
+            Core.notification('info', dir + " deployed");
+            if (http_host && http_port) {
+              $scope.address = $sce.trustAsHtml("http://" + http_host + ":" + http_port + "/#!/topology")
+            }
+          } else {
+            $scope.state = "Deploy Failed"
+            $scope.close_button_class = "btn-danger"
+            Core.notification('error', dir + " deployment failed");
           }
         }
         $timeout(function () {
           $scope.status = response[0]
           scrollToEnd()
-          if (response[1] === 'DONE') {
-          }
           if ($scope.polling) (
             pollTimer = setTimeout(doPoll, 1000)
           )
         })
       })
     }
-    pollTimer = setTimeout(doPoll, 1000)
+    $scope.deploy = function () {
+      $scope.deploy_state = "deploying"
+      start_fn(hosts)
+      doPoll()
+    }
+    $scope.done = function () {
+      return !$scope.polling && $scope.state === success_state
+    }
     $scope.hasConsole = function () {
-      return http_host && http_port
+      return http_host && http_port && $scope.state === success_state
+    }
+
+    $scope.showing_pass = undefined
+    $scope.show_pass = function (host) {
+      $scope.showing_pass = (host) ? host.name : ''
+    }
+    $scope.hosts = hosts
+    $scope.pass = function () {
+      console.log('set password')
     }
 
     function scrollTopTween(scrollTop) {
@@ -1770,9 +1990,9 @@ var QDR = (function(QDR) {
       }
     }
     var scrollToEnd = function () {
-      var scrollheight = d3.select("#deploy_status").property("scrollHeight");
+      var scrollheight = d3.select("#deploy_output").property("scrollHeight");
 
-      d3.select('#deploy_status')
+      d3.select('#deploy_output')
         .transition().duration(1000)
         .tween("uniquetweenname", scrollTopTween(scrollheight));
     }


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@qpid.apache.org
For additional commands, e-mail: commits-help@qpid.apache.org