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()