Hook up a packing plan endpoint in Heron UI (#3359)

* Hook up a packing plan endpoint in Heron UI

* add unit test
diff --git a/heron/tools/common/src/python/access/heron_api.py b/heron/tools/common/src/python/access/heron_api.py
index 1ab571c..709acb4 100644
--- a/heron/tools/common/src/python/access/heron_api.py
+++ b/heron/tools/common/src/python/access/heron_api.py
@@ -35,6 +35,7 @@
 EXECUTION_STATE_URL_FMT     = "%s/executionstate"     % TOPOLOGIES_URL_FMT
 LOGICALPLAN_URL_FMT         = "%s/logicalplan"        % TOPOLOGIES_URL_FMT
 PHYSICALPLAN_URL_FMT        = "%s/physicalplan"       % TOPOLOGIES_URL_FMT
+PACKINGPLAN_URL_FMT         = "%s/packingplan"        % TOPOLOGIES_URL_FMT
 SCHEDULER_LOCATION_URL_FMT  = "%s/schedulerlocation"  % TOPOLOGIES_URL_FMT
 
 METRICS_URL_FMT             = "%s/metrics"            % TOPOLOGIES_URL_FMT
@@ -268,6 +269,25 @@
 
 ################################################################################
 @tornado.gen.coroutine
+def get_packing_plan(cluster, environ, topology, role=None):
+  '''
+  Get the packing plan state of a topology in a cluster from tracker
+  :param cluster:
+  :param environ:
+  :param topology:
+  :param role:
+  :return:
+  '''
+  params = dict(cluster=cluster, environ=environ, topology=topology)
+  if role is not None:
+    params['role'] = role
+  request_url = tornado.httputil.url_concat(
+      create_url(PACKINGPLAN_URL_FMT), params)
+  raise tornado.gen.Return((yield fetch_url_as_json(request_url)))
+
+
+################################################################################
+@tornado.gen.coroutine
 def get_physical_plan(cluster, environ, topology, role=None):
   '''
   Get the physical plan state of a topology in a cluster from tracker
diff --git a/heron/tools/tracker/src/python/tracker.py b/heron/tools/tracker/src/python/tracker.py
index 3ce814d..15df468 100644
--- a/heron/tools/tracker/src/python/tracker.py
+++ b/heron/tools/tracker/src/python/tracker.py
@@ -598,7 +598,7 @@
 
     packingPlan["id"] = topology.packing_plan.id
     packingPlan["container_plans"] = containers
-    return json.dumps(packingPlan)
+    return packingPlan
 
   def setTopologyInfo(self, topology):
     """
diff --git a/heron/tools/tracker/tests/python/tracker_unittest.py b/heron/tools/tracker/tests/python/tracker_unittest.py
index 82afd64..136f5d6 100644
--- a/heron/tools/tracker/tests/python/tracker_unittest.py
+++ b/heron/tools/tracker/tests/python/tracker_unittest.py
@@ -250,3 +250,22 @@
                      {'topology.component.parallelism': '1'})
     self.assertEqual(pplan['instances'], {})
     self.assertEqual(pplan['stmgrs'], {})
+
+  def test_extract_packing_plan(self):
+    # Create topology
+    pb_pplan = MockProto().create_mock_simple_packing_plan()
+    topology = Topology('topology_name', 'ExclamationTopology')
+    topology.set_packing_plan(pb_pplan)
+    # Extract packing plan
+    packing_plan = self.tracker.extract_packing_plan(topology)
+    self.assertEqual(packing_plan['id'], 'ExclamationTopology')
+    self.assertEqual(packing_plan['container_plans'][0]['id'], 1)
+    self.assertEqual(packing_plan['container_plans'][0]['required_resources'],
+                     {'disk': 2048L, 'ram': 1024L, 'cpu': 1.0})
+    self.assertEqual(packing_plan['container_plans'][0]['instances'][0],
+                     {
+                       'component_index': 1,
+                        'component_name': u'word',
+                        'instance_resources': {'cpu': 1.0, 'disk': 2048L, 'ram': 1024L},
+                        'task_id': 1
+                     })
diff --git a/heron/tools/ui/src/python/handlers/api/__init__.py b/heron/tools/ui/src/python/handlers/api/__init__.py
index c9e17e0f..e36109d 100644
--- a/heron/tools/ui/src/python/handlers/api/__init__.py
+++ b/heron/tools/ui/src/python/handlers/api/__init__.py
@@ -24,6 +24,7 @@
     TopologyExceptionSummaryHandler,
     ListTopologiesJsonHandler,
     TopologyLogicalPlanJsonHandler,
+    TopologyPackingPlanJsonHandler,
     TopologyPhysicalPlanJsonHandler,
     TopologySchedulerLocationJsonHandler,
     TopologyExecutionStateJsonHandler,
diff --git a/heron/tools/ui/src/python/handlers/api/topology.py b/heron/tools/ui/src/python/handlers/api/topology.py
index c02f566..3df74d5 100644
--- a/heron/tools/ui/src/python/handlers/api/topology.py
+++ b/heron/tools/ui/src/python/handlers/api/topology.py
@@ -141,6 +141,32 @@
     self.write(result)
 
 
+class TopologyPackingPlanJsonHandler(base.BaseHandler):
+  ''' TopologyPackingPlanJsonHandler '''
+
+  @tornado.gen.coroutine
+  def get(self, cluster, environ, topology):
+    '''
+    :param cluster:
+    :param environ:
+    :param topology:
+    :return:
+    '''
+
+    start_time = time.time()
+    packing_plan = yield access.get_packing_plan(cluster, environ, topology)
+
+    result_map = dict(
+        status="success",
+        message="",
+        version=common.VERSION,
+        executiontime=time.time() - start_time,
+        result=packing_plan
+    )
+
+    self.write(result_map)
+
+
 class TopologyPhysicalPlanJsonHandler(base.BaseHandler):
   ''' TopologyPhysicalPlanJsonHandler '''
 
diff --git a/heron/tools/ui/src/python/main.py b/heron/tools/ui/src/python/main.py
index d72b06c..cb8b4b7 100644
--- a/heron/tools/ui/src/python/main.py
+++ b/heron/tools/ui/src/python/main.py
@@ -100,6 +100,8 @@
          handlers.api.ListTopologiesJsonHandler),
         (r"/topologies/([^\/]+)/([^\/]+)/([^\/]+)/logicalplan.json",
          handlers.api.TopologyLogicalPlanJsonHandler),
+        (r"/topologies/([^\/]+)/([^\/]+)/([^\/]+)/packingplan.json",
+         handlers.api.TopologyPackingPlanJsonHandler),
         (r"/topologies/([^\/]+)/([^\/]+)/([^\/]+)/physicalplan.json",
          handlers.api.TopologyPhysicalPlanJsonHandler),
         (r"/topologies/([^\/]+)/([^\/]+)/([^\/]+)/executionstate.json",