Merge "acloud: grab the build target name from the image name."
diff --git a/Android.bp b/Android.bp
index 25437d2..5e65614 100644
--- a/Android.bp
+++ b/Android.bp
@@ -42,7 +42,8 @@
         "acloud_create",
         "acloud_delete",
         "acloud_internal",
-	"acloud_metrics",
+        "acloud_list",
+        "acloud_metrics",
         "acloud_proto",
         "acloud_public",
         "acloud_setup",
@@ -71,6 +72,7 @@
         "acloud_create",
         "acloud_delete",
         "acloud_internal",
+        "acloud_list",
         "acloud_proto",
         "acloud_public",
         "acloud_setup",
@@ -149,6 +151,14 @@
 }
 
 python_library_host{
+    name: "acloud_list",
+    defaults: ["acloud_default"],
+    srcs: [
+         "list/*.py",
+    ],
+}
+
+python_library_host{
     name: "acloud_metrics",
     defaults: ["acloud_default"],
     srcs: [
diff --git a/internal/constants.py b/internal/constants.py
index 88cbe33..0951688 100755
--- a/internal/constants.py
+++ b/internal/constants.py
@@ -102,3 +102,22 @@
 
 SSH_BIN = "ssh"
 ADB_BIN = "adb"
+
+LABEL_CREATE_BY = "created_by"
+LABEL_FLAVOR = "flavor"
+LABEL_TYPE = "avd_type"
+
+# for list and delete cmd
+INS_KEY_NAME = "name"
+INS_KEY_FULLNAME = "full_name"
+INS_KEY_STATUS = "status"
+INS_KEY_DISPLAY = "display"
+INS_KEY_IP = "ip"
+INS_KEY_ADB = "adb"
+INS_KEY_VNC = "vnc"
+INS_KEY_CREATETIME = "creationTimestamp"
+INS_KEY_AVD_TYPE = LABEL_TYPE
+INS_KEY_AVD_FLAVOR = LABEL_FLAVOR
+INS_KEY_IS_LOCAL = "remote"
+INS_STATUS_RUNNING = "RUNNING"
+LOCAL_INS_NAME = "local-instance"
diff --git a/internal/lib/utils_test.py b/internal/lib/utils_test.py
index 6dab2ef..60bd34d 100644
--- a/internal/lib/utils_test.py
+++ b/internal/lib/utils_test.py
@@ -30,6 +30,23 @@
 from acloud.internal.lib import driver_test_lib
 from acloud.internal.lib import utils
 
+class FakeTkinter(object):
+    """Fake implementation of Tkinter.Tk()"""
+
+    def __init__(self, width=None, height=None):
+        self.width = width
+        self.height = height
+
+    # pylint: disable=invalid-name
+    def winfo_screenheight(self):
+        """Return the screen height."""
+        return self.height
+
+    # pylint: disable=invalid-name
+    def winfo_screenwidth(self):
+        """Return the screen width."""
+        return self.width
+
 
 class UtilsTest(driver_test_lib.BaseDriverTest):
     """Test Utils."""
@@ -245,40 +262,30 @@
                                                  enable_choose_all=True),
                          answer_list)
 
-    @mock.patch.object(Tkinter.Tk, "winfo_screenwidth")
-    @mock.patch.object(Tkinter.Tk, "winfo_screenheight")
-    def testCalculateVNCScreenRatio(self, mock_screenheight, mock_screenwidth):
+    @mock.patch.object(Tkinter, "Tk")
+    def testCalculateVNCScreenRatio(self, mock_tk):
         """Test Calculating the scale ratio of VNC display."""
-        # Tkinter.Tk requires $DISPLAY to be set if the screenName is None so
-        # set screenName to avoid TclError when running this test in a term that
-        # doesn't have $DISPLAY set.
-        mock.patch.object(Tkinter, "Tk", new=Tkinter.Tk(screenName=":0"))
-
         # Get scale-down ratio if screen height is smaller than AVD height.
-        mock_screenheight.return_value = 800
-        mock_screenwidth.return_value = 1200
+        mock_tk.return_value = FakeTkinter(height=800, width=1200)
         avd_h = 1920
         avd_w = 1080
         self.assertEqual(utils.CalculateVNCScreenRatio(avd_w, avd_h), 0.4)
 
         # Get scale-down ratio if screen width is smaller than AVD width.
-        mock_screenheight.return_value = 800
-        mock_screenwidth.return_value = 1200
+        mock_tk.return_value = FakeTkinter(height=800, width=1200)
         avd_h = 900
         avd_w = 1920
         self.assertEqual(utils.CalculateVNCScreenRatio(avd_w, avd_h), 0.6)
 
         # Scale ratio = 1 if screen is larger than AVD.
-        mock_screenheight.return_value = 1080
-        mock_screenwidth.return_value = 1920
+        mock_tk.return_value = FakeTkinter(height=1080, width=1920)
         avd_h = 800
         avd_w = 1280
         self.assertEqual(utils.CalculateVNCScreenRatio(avd_w, avd_h), 1)
 
         # Get the scale if ratio of width is smaller than the
         # ratio of height.
-        mock_screenheight.return_value = 1200
-        mock_screenwidth.return_value = 800
+        mock_tk.return_value = FakeTkinter(height=1200, width=800)
         avd_h = 1920
         avd_w = 1080
         self.assertEqual(utils.CalculateVNCScreenRatio(avd_w, avd_h), 0.6)
diff --git a/list/__init__.py b/list/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/list/__init__.py
diff --git a/list/instance.py b/list/instance.py
new file mode 100644
index 0000000..3e21c2d
--- /dev/null
+++ b/list/instance.py
@@ -0,0 +1,291 @@
+# Copyright 2018 - The Android Open Source Project
+#
+# Licensed 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.
+r"""Instance class.
+
+Define the instance class used to hold details about an AVD instance.
+
+The instance class will hold details about AVD instances (remote/local) used to
+enable users to understand what instances they've created. This will be leveraged
+for the list, delete, and reconnect commands.
+
+The details include:
+- instance name (for remote instances)
+- creation date/instance duration
+- instance image details (branch/target/build id)
+- and more!
+"""
+
+import collections
+import logging
+import re
+import subprocess
+
+from acloud.internal import constants
+
+logger = logging.getLogger(__name__)
+
+_COMMAND_PS = ["ps", "aux"]
+_RE_GROUP_ADB = "local_adb_port"
+_RE_GROUP_VNC = "local_vnc_port"
+_RE_SSH_TUNNEL_PATTERN = (r"((.*\s*-L\s)(?P<%s>\d+):127.0.0.1:%s)"
+                          r"((.*\s*-L\s)(?P<%s>\d+):127.0.0.1:%s)"
+                          r"(.+%s)")
+
+_COMMAND_PS_LAUNCH_CVD = ["ps", "-eo", "lstart,cmd"]
+_RE_LAUNCH_CVD = re.compile(r"(?P<date_str>^[^/]+)(.*launch_cvd --daemon )+"
+                            r"((.*\s*-cpus\s)(?P<cpu>\d+))?"
+                            r"((.*\s*-x_res\s)(?P<x_res>\d+))?"
+                            r"((.*\s*-y_res\s)(?P<y_res>\d+))?"
+                            r"((.*\s*-dpi\s)(?P<dpi>\d+))?"
+                            r"((.*\s*-memory_mb\s)(?P<memory>\d+))?"
+                            r"((.*\s*-blank_data_image_mb\s)(?P<disk>\d+))?")
+_FULL_NAME_STRING = "device serial: %(device_serial)s (%(instance_name)s)"
+ForwardedPorts = collections.namedtuple("ForwardedPorts",
+                                        [constants.VNC_PORT, constants.ADB_PORT])
+
+class Instance(object):
+    """Class to store data of instance."""
+
+    def __init__(self):
+        self._name = None
+        self._fullname = None
+        self._status = None
+        self._display = None  # Resolution and dpi
+        self._ip = None
+        self._adb_port = None  # adb port which is forwarding to remote
+        self._vnc_port = None  # vnc port which is forwarding to remote
+        self._is_connected = None  # True if ssh tunnel is still connected
+        self._createtime = None
+        self._avd_type = None
+        self._avd_flavor = None
+        self._is_local = None  # True if this is a local instance
+
+    def __repr__(self):
+        """Let's make it easy to see what this class is holding."""
+        indent = " " * 3
+        representation = []
+        representation.append(" name: %s" % self._name)
+        representation.append("%s IP: %s" % (indent, self._ip))
+        representation.append("%s create time: %s" %
+                              (indent, self._createtime))
+        representation.append("%s status: %s" %
+                              (indent, self._status))
+
+        if self._adb_port:
+            representation.append("%s adb serial: 127.0.0.1:%s" %
+                                  (indent, self._adb_port))
+        else:
+            representation.append("%s adb serial: disconnected" % indent)
+
+            representation.append("%s avd type: %s" %
+                                  (indent, self._avd_type))
+            representation.append("%s display: %s" %
+                                  (indent, self._display))
+
+        return "\n".join(representation)
+
+    @property
+    def name(self):
+        """Return the instance name."""
+        return self._name
+
+    @property
+    def fullname(self):
+        """Return the instance full name."""
+        return self._fullname
+
+    @property
+    def ip(self):
+        """Return the ip."""
+        return self._ip
+
+    @property
+    def status(self):
+        """Return status."""
+        return self._status
+
+    @property
+    def display(self):
+        """Return display."""
+        return self._display
+
+    @property
+    def forwarding_adb_port(self):
+        """Return the adb port."""
+        return self._adb_port
+
+    @property
+    def forwarding_vnc_port(self):
+        """Return the vnc port."""
+        return self._vnc_port
+
+    @property
+    def is_connected(self):
+        """Return the connect status."""
+        return self._is_connected
+
+    @property
+    def createtime(self):
+        """Return create time."""
+        return self._createtime
+
+    @property
+    def avd_type(self):
+        """Return avd_type."""
+        return self._avd_type
+
+    @property
+    def avd_flavor(self):
+        """Return avd_flavor."""
+        return self._avd_flavor
+
+    @property
+    def islocal(self):
+        """Return if it is a local instance."""
+        return self._is_local
+
+
+class LocalInstance(Instance):
+    """Class to store data of local instance."""
+
+    # pylint: disable=protected-access
+    def __new__(cls):
+        """Initialize a localInstance object.
+
+        Gather local instance information from launch_cvd process.
+
+        returns:
+            Instance object if launch_cvd process is found otherwise return None.
+        """
+        process_output = subprocess.check_output(_COMMAND_PS_LAUNCH_CVD)
+        for line in process_output.splitlines():
+            match = _RE_LAUNCH_CVD.match(line)
+            if match:
+                local_instance = Instance()
+                x_res = match.group("x_res")
+                y_res = match.group("y_res")
+                dpi = match.group("dpi")
+                date_str = match.group("date_str").strip()
+                local_instance._name = constants.LOCAL_INS_NAME
+                local_instance._fullname = (_FULL_NAME_STRING %
+                                            {"device_serial": "127.0.0.1:%d" %
+                                                              constants.DEFAULT_ADB_PORT,
+                                             "instance_name": local_instance._name})
+                local_instance._createtime = date_str
+                local_instance._avd_type = constants.TYPE_CF
+                local_instance._ip = "127.0.0.1"
+                local_instance._status = constants.INS_STATUS_RUNNING
+                local_instance._adb_port = constants.DEFAULT_ADB_PORT
+                local_instance._vnc_port = constants.DEFAULT_VNC_PORT
+                local_instance._display = ("%sx%s (%s)" % (x_res, y_res, dpi))
+                local_instance._is_local = True
+                return local_instance
+        return None
+
+
+class RemoteInstance(Instance):
+    """Class to store data of remote instance."""
+
+    def __init__(self, gce_instance):
+        """Process the args into class vars.
+
+        RemoteInstace initialized by gce dict object.
+        Reference:
+        https://cloud.google.com/compute/docs/reference/rest/v1/instances/get
+
+        Args:
+            gce_instance: dict object queried from gce.
+        """
+        super(RemoteInstance, self).__init__()
+        self._ProcessGceInstance(gce_instance)
+        self._is_local = False
+
+    def _ProcessGceInstance(self, gce_instance):
+        """Parse the required data from gce_instance to local variables.
+
+        We also gather more details on client side including the forwarding adb port
+        and vnc port which will be used to determine the status of connection.
+
+        Args:
+           gce_instance: dict object queried from gce.
+        """
+        self._name = gce_instance.get(constants.INS_KEY_NAME)
+
+        # TODO(b/119291750): calculate the elapsed time since instance has been created.
+        self._createtime = gce_instance.get(constants.INS_KEY_CREATETIME)
+        self._status = gce_instance.get(constants.INS_KEY_STATUS)
+
+        ip = None
+        for network_interface in gce_instance.get("networkInterfaces"):
+            for access_config in network_interface.get("accessConfigs"):
+                ip = access_config.get("natIP")
+
+        # Find ssl tunnel info.
+        if ip:
+            forwarded_ports = self._GetAdbVncPortFromSSHTunnel(ip)
+            self._ip = ip
+            self._adb_port = forwarded_ports.adb_port
+            self._vnc_port = forwarded_ports.vnc_port
+            if self._adb_port:
+                self._is_connected = True
+                self._fullname = (_FULL_NAME_STRING %
+                                  {"device_serial": "127.0.0.1:%d" % self._adb_port,
+                                   "instance_name": self._name})
+            else:
+                self._is_connected = False
+                self._fullname = (_FULL_NAME_STRING %
+                                  {"device_serial": "not connected",
+                                   "instance_name": self._name})
+
+        # Find avd type from labels
+        labels = gce_instance.get("labels")
+        self._avd_type = labels[constants.LABEL_TYPE]
+        self._avd_flavor = labels[constants.LABEL_FLAVOR]
+
+        # Get display information.
+        for metadata in gce_instance.get("metadata", {}).get("items", []):
+            if metadata["key"] == constants.INS_KEY_DISPLAY:
+                self._display = metadata["value"]
+                break
+
+    @staticmethod
+    def _GetAdbVncPortFromSSHTunnel(ip):
+        """Get forwarding adb and vnc port from ssh tunnel.
+
+        Args:
+            ip: String, ip address.
+
+        Returns:
+            NamedTuple ForwardedPorts(vnc_port, adb_port) holding the ports
+            used in the ssh forwarded call. Both fields are integers.
+        """
+        process_output = subprocess.check_output(_COMMAND_PS)
+        re_pattern = re.compile(_RE_SSH_TUNNEL_PATTERN %
+                                (_RE_GROUP_VNC, constants.DEFAULT_VNC_PORT,
+                                 _RE_GROUP_ADB, constants.DEFAULT_ADB_PORT, ip))
+
+        adb_port = None
+        vnc_port = None
+        for line in process_output.splitlines():
+            match = re_pattern.match(line)
+            if match:
+                adb_port = int(match.group(_RE_GROUP_ADB))
+                vnc_port = int(match.group(_RE_GROUP_VNC))
+                break
+
+        logger.debug(("grathering detail for ssh tunnel. "
+                      "IP:%s, forwarding (adb:%d, vnc:%d)"), ip, adb_port,
+                     vnc_port)
+
+        return ForwardedPorts(vnc_port=vnc_port, adb_port=adb_port)
diff --git a/list/instance_test.py b/list/instance_test.py
new file mode 100644
index 0000000..4fed8fa
--- /dev/null
+++ b/list/instance_test.py
@@ -0,0 +1,112 @@
+#!/usr/bin/env python
+#
+# Copyright 2018 - The Android Open Source Project
+#
+# Licensed 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.
+"""Tests for host_setup_runner."""
+import collections
+import subprocess
+
+import unittest
+
+from acloud.internal import constants
+from acloud.internal.lib import driver_test_lib
+from acloud.list import instance
+
+
+class InstanceTest(driver_test_lib.BaseDriverTest):
+    """Test instance."""
+    PS_SSH_TUNNEL = ("/fake_ps_1 --fake arg \n"
+                     "/fake_ps_2 --fake arg \n"
+                     "/usr/bin/ssh -i ~/.ssh/acloud_rsa "
+                     "-o UserKnownHostsFile=/dev/null "
+                     "-o StrictHostKeyChecking=no -L 12345:127.0.0.1:6444 "
+                     "-L 54321:127.0.0.1:6520 -N -f -l user fake_ip")
+    PS_LAUNCH_CVD = ("Sat Nov 10 21:55:10 2018 /fake_path/bin/launch_cvd "
+                     "--daemon --cpus 2 --x_res 1080 --y_res 1920 --dpi 480"
+                     " --memory_mb 4096 --blank_data_image_mb 4096 --data_policy"
+                     " always_create --system_image_dir /fake "
+                     "--vnc_server_port 6444")
+
+    # pylint: disable=protected-access
+    def testCreateLocalInstance(self):
+        """"Test get local instance info from launch_cvd process."""
+        self.Patch(subprocess, "check_output", return_value=self.PS_LAUNCH_CVD)
+        local_instance = instance.LocalInstance()
+        self.assertEqual(constants.LOCAL_INS_NAME, local_instance.name)
+        self.assertEqual(True, local_instance.islocal)
+        self.assertEqual("1080x1920 (480)", local_instance.display)
+        self.assertEqual("Sat Nov 10 21:55:10 2018", local_instance.createtime)
+        expected_full_name = "device serial: 127.0.0.1:%s (%s)" % (constants.DEFAULT_ADB_PORT,
+                                                                   local_instance.name)
+        self.assertEqual(expected_full_name, local_instance.fullname)
+
+        # test return None if no launch_cvd process found
+        self.Patch(subprocess, "check_output", return_value="no launch_cvd "
+                                                            "found")
+        self.assertEqual(None, instance.LocalInstance())
+
+    # pylint: disable=protected-access
+    def testGetAdbVncPortFromSSHTunnel(self):
+        """"Test Get forwarding adb and vnc port from ssh tunnel."""
+        self.Patch(subprocess, "check_output", return_value=self.PS_SSH_TUNNEL)
+        forwarded_ports = instance.RemoteInstance._GetAdbVncPortFromSSHTunnel("fake_ip")
+        self.assertEqual(54321, forwarded_ports.adb_port)
+        self.assertEqual(12345, forwarded_ports.vnc_port)
+
+    # pylint: disable=protected-access
+    def testProcessGceInstance(self):
+        """"Test process instance detail."""
+        gce_instance = {
+            constants.INS_KEY_NAME: "fake_ins_name",
+            constants.INS_KEY_CREATETIME: "fake_create_time",
+            constants.INS_KEY_STATUS: "fake_status",
+            "networkInterfaces": [{"accessConfigs": [{"natIP": "fake_ip"}]}],
+            "labels": {constants.INS_KEY_AVD_TYPE: "fake_type",
+                       constants.INS_KEY_AVD_FLAVOR: "fake_flavor"},
+            "metadata": {}
+        }
+
+        fake_adb = 123456
+        fake_vnc = 654321
+        forwarded_ports = collections.namedtuple("ForwardedPorts",
+                                                 [constants.VNC_PORT,
+                                                  constants.ADB_PORT])
+        self.Patch(
+            instance.RemoteInstance,
+            "_GetAdbVncPortFromSSHTunnel",
+            return_value=forwarded_ports(vnc_port=fake_vnc, adb_port=fake_adb))
+
+        # test is_connected will be true if ssh tunnel connection is found
+        instance_info = instance.RemoteInstance(gce_instance)
+        self.assertTrue(instance_info.is_connected)
+        self.assertEqual(instance_info.forwarding_adb_port, fake_adb)
+        self.assertEqual(instance_info.forwarding_vnc_port, fake_vnc)
+        expected_full_name = "device serial: 127.0.0.1:%s (%s)" % (fake_adb,
+                                                                   instance_info.name)
+        self.assertEqual(expected_full_name, instance_info.fullname)
+
+        # test is_connected will be false if ssh tunnel connection is not found
+        self.Patch(
+            instance.RemoteInstance,
+            "_GetAdbVncPortFromSSHTunnel",
+            return_value=forwarded_ports(vnc_port=None, adb_port=None))
+        instance_info = instance.RemoteInstance(gce_instance)
+        self.assertFalse(instance_info.is_connected)
+        expected_full_name = ("device serial: not connected (%s)" %
+                              instance_info.name)
+        self.assertEqual(expected_full_name, instance_info.fullname)
+
+
+if __name__ == "__main__":
+    unittest.main()