Merge "Check the zone has enough cpus and disks for remote instance creation."
diff --git a/Android.bp b/Android.bp
index fb9f40e..b211ee7 100644
--- a/Android.bp
+++ b/Android.bp
@@ -39,6 +39,7 @@
],
data: [
"public/data/default.config",
+ ":acloud_version",
],
libs: [
"acloud_create",
@@ -198,3 +199,10 @@
"asuite_metrics",
],
}
+
+genrule {
+ name: "acloud_version",
+ tool_files: ["gen_version.sh"],
+ cmd: "$(location gen_version.sh) $(out)",
+ out: ["public/data/VERSION"],
+}
diff --git a/README.md b/README.md
index e9e9c06..8dc3032 100755
--- a/README.md
+++ b/README.md
@@ -51,8 +51,6 @@
(running on your local host) use cases. You also have the option to use
a locally built image or an image from the Android Build servers.
-**Disclaimer: Creation of a cuttlefish local instance is not formally supported, please use at your own risk.**
-
Here's a quick cheat-sheet for the 4 use cases:
* Remote instance using an Android Build image (LKGB (Last Known Good Build)
diff --git a/create/avd_spec.py b/create/avd_spec.py
index b50b4c3..940a578 100644
--- a/create/avd_spec.py
+++ b/create/avd_spec.py
@@ -26,6 +26,7 @@
import re
import subprocess
import tempfile
+import threading
from acloud import errors
from acloud.create import create_common
@@ -41,7 +42,8 @@
# Default values for build target.
_BRANCH_RE = re.compile(r"^Manifest branch: (?P<branch>.+)")
-_COMMAND_REPO_INFO = ["repo", "info"]
+_COMMAND_REPO_INFO = "repo info platform/tools/acloud"
+_REPO_TIMEOUT = 3
_CF_ZIP_PATTERN = "*img*.zip"
_DEFAULT_BUILD_BITNESS = "x86"
_DEFAULT_BUILD_TYPE = "userdebug"
@@ -130,7 +132,9 @@
self._gpu = None
self._emulator_build_id = None
- # username and password only used for cheeps type.
+ # Fields only used for cheeps type.
+ self._stable_cheeps_host_image_name = None
+ self._stable_cheeps_host_image_project = None
self._username = None
self._password = None
@@ -264,7 +268,7 @@
Args:
args: Namespace object from argparse.parse_args.
"""
- self._cfg.OverrideHwPropertyWithFlavor(self._flavor)
+ self._cfg.OverrideHwProperty(self._flavor, self._instance_type)
self._hw_property = {}
self._hw_property = self._ParseHWPropertyStr(self._cfg.hw_property)
logger.debug("Default hw property for [%s] flavor: %s", self._flavor,
@@ -303,6 +307,8 @@
self._emulator_build_id = args.emulator_build_id
self._gpu = args.gpu
+ self._stable_cheeps_host_image_name = args.stable_cheeps_host_image_name
+ self._stable_cheeps_host_image_project = args.stable_cheeps_host_image_project
self._username = args.username
self._password = args.password
@@ -571,19 +577,31 @@
Returns:
branch: String, git branch name. e.g. "aosp-master"
"""
- repo_output = ""
- try:
- repo_output = subprocess.check_output(_COMMAND_REPO_INFO)
- except subprocess.CalledProcessError:
- utils.PrintColorString(
- "Unable to determine your repo branch, defaulting to %s"
- % _DEFAULT_BRANCH, utils.TextColors.WARNING)
- for line in repo_output.splitlines():
- match = _BRANCH_RE.match(EscapeAnsi(line))
- if match:
- branch_prefix = _BRANCH_PREFIX.get(self._GetGitRemote(),
- _DEFAULT_BRANCH_PREFIX)
- return branch_prefix + match.group("branch")
+ branch = None
+ # TODO(149460014): Migrate acloud to py3, then remove this
+ # workaround.
+ env = os.environ.copy()
+ env.pop("PYTHONPATH", None)
+ logger.info("Running command \"%s\"", _COMMAND_REPO_INFO)
+ process = subprocess.Popen(_COMMAND_REPO_INFO, shell=True, stdin=None,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT, env=env)
+ timer = threading.Timer(_REPO_TIMEOUT, process.kill)
+ timer.start()
+ stdout, _ = process.communicate()
+ if stdout:
+ for line in stdout.splitlines():
+ match = _BRANCH_RE.match(EscapeAnsi(line))
+ if match:
+ branch_prefix = _BRANCH_PREFIX.get(self._GetGitRemote(),
+ _DEFAULT_BRANCH_PREFIX)
+ branch = branch_prefix + match.group("branch")
+ timer.cancel()
+ if branch:
+ return branch
+ utils.PrintColorString(
+ "Unable to determine your repo branch, defaulting to %s"
+ % _DEFAULT_BRANCH, utils.TextColors.WARNING)
return _DEFAULT_BRANCH
def _GetBuildTarget(self, args):
@@ -650,22 +668,35 @@
def autoconnect(self):
"""autoconnect.
- args.autoconnect could pass in type of Boolean or String.
+ args.autoconnect could pass as Boolean or String.
Return: Boolean, True only if self._autoconnect is not False.
"""
return self._autoconnect is not False
@property
- def connect_vnc(self):
- """launch vnc.
+ def connect_adb(self):
+ """Auto-connect to adb.
- args.autoconnect could pass in type of Boolean or String.
- if args.autoconnect is "adb" or False, no need to launch vnc.
-
- Return: Boolean, True only if self._autoconnect is True.
+ Return: Boolean, whether autoconnect is enabled.
"""
- return self._autoconnect is True
+ return self._autoconnect is not False
+
+ @property
+ def connect_vnc(self):
+ """Launch vnc.
+
+ Return: Boolean, True if self._autoconnect is 'vnc'.
+ """
+ return self._autoconnect == constants.INS_KEY_VNC
+
+ @property
+ def connect_webrtc(self):
+ """Auto-launch webRTC AVD on the browser.
+
+ Return: Boolean, True if args.autoconnect is "webrtc".
+ """
+ return self._autoconnect == constants.INS_KEY_WEBRTC
@property
def unlock_screen(self):
@@ -733,6 +764,17 @@
return self._client_adb_port
@property
+ def stable_cheeps_host_image_name(self):
+ """Return the Cheeps host image name."""
+ return self._stable_cheeps_host_image_name
+
+ # pylint: disable=invalid-name
+ @property
+ def stable_cheeps_host_image_project(self):
+ """Return the project hosting the Cheeps host image."""
+ return self._stable_cheeps_host_image_project
+
+ @property
def username(self):
"""Return username."""
return self._username
diff --git a/create/avd_spec_test.py b/create/avd_spec_test.py
index 448cd39..467b7da 100644
--- a/create/avd_spec_test.py
+++ b/create/avd_spec_test.py
@@ -15,6 +15,7 @@
import glob
import os
+import subprocess
import unittest
import mock
@@ -129,21 +130,32 @@
"/test_path_to_dir/avd-system.tar.gz")
@mock.patch.object(avd_spec.AVDSpec, "_GetGitRemote")
- @mock.patch("subprocess.check_output")
- def testGetBranchFromRepo(self, mock_repo, mock_gitremote):
+ def testGetBranchFromRepo(self, mock_gitremote):
"""Test get branch name from repo info."""
# Check aosp repo gets proper branch prefix.
+ fake_subprocess = mock.MagicMock()
+ fake_subprocess.stdout = mock.MagicMock()
+ fake_subprocess.stdout.readline = mock.MagicMock(return_value='')
+ fake_subprocess.poll = mock.MagicMock(return_value=0)
+ fake_subprocess.returncode = 0
+ return_value = b"Manifest branch: master"
+ fake_subprocess.communicate = mock.MagicMock(return_value=(return_value, ''))
+ self.Patch(subprocess, "Popen", return_value=fake_subprocess)
+
mock_gitremote.return_value = "aosp"
- mock_repo.return_value = "Manifest branch: master"
self.assertEqual(self.AvdSpec._GetBranchFromRepo(), "aosp-master")
# Check default repo gets default branch prefix.
mock_gitremote.return_value = ""
- mock_repo.return_value = "Manifest branch: master"
+ return_value = b"Manifest branch: master"
+ fake_subprocess.communicate = mock.MagicMock(return_value=(return_value, ''))
+ self.Patch(subprocess, "Popen", return_value=fake_subprocess)
self.assertEqual(self.AvdSpec._GetBranchFromRepo(), "git_master")
# Can't get branch from repo info, set it as default branch.
- mock_repo.return_value = "Manifest branch:"
+ return_value = b"Manifest branch:"
+ fake_subprocess.communicate = mock.MagicMock(return_value=(return_value, ''))
+ self.Patch(subprocess, "Popen", return_value=fake_subprocess)
self.assertEqual(self.AvdSpec._GetBranchFromRepo(), "aosp-master")
def testGetBuildBranch(self):
@@ -354,21 +366,34 @@
self.AvdSpec._ProcessMiscArgs(self.args)
self.assertEqual(self.AvdSpec._instance_type, constants.INSTANCE_TYPE_HOST)
- # Test avd_spec.autoconnect and avd_spec.connect_vnc
- self.args.autoconnect = True
- self.AvdSpec._ProcessMiscArgs(self.args)
- self.assertEqual(self.AvdSpec.autoconnect, True)
- self.assertEqual(self.AvdSpec.connect_vnc, True)
-
+ # Test avd_spec.autoconnect
self.args.autoconnect = False
self.AvdSpec._ProcessMiscArgs(self.args)
self.assertEqual(self.AvdSpec.autoconnect, False)
+ self.assertEqual(self.AvdSpec.connect_adb, False)
self.assertEqual(self.AvdSpec.connect_vnc, False)
+ self.assertEqual(self.AvdSpec.connect_webrtc, False)
- self.args.autoconnect = "adb"
+ self.args.autoconnect = constants.INS_KEY_VNC
self.AvdSpec._ProcessMiscArgs(self.args)
self.assertEqual(self.AvdSpec.autoconnect, True)
+ self.assertEqual(self.AvdSpec.connect_adb, True)
+ self.assertEqual(self.AvdSpec.connect_vnc, True)
+ self.assertEqual(self.AvdSpec.connect_webrtc, False)
+
+ self.args.autoconnect = constants.INS_KEY_ADB
+ self.AvdSpec._ProcessMiscArgs(self.args)
+ self.assertEqual(self.AvdSpec.autoconnect, True)
+ self.assertEqual(self.AvdSpec.connect_adb, True)
self.assertEqual(self.AvdSpec.connect_vnc, False)
+ self.assertEqual(self.AvdSpec.connect_webrtc, False)
+
+ self.args.autoconnect = constants.INS_KEY_WEBRTC
+ self.AvdSpec._ProcessMiscArgs(self.args)
+ self.assertEqual(self.AvdSpec.autoconnect, True)
+ self.assertEqual(self.AvdSpec.connect_adb, True)
+ self.assertEqual(self.AvdSpec.connect_vnc, False)
+ self.assertEqual(self.AvdSpec.connect_webrtc, True)
if __name__ == "__main__":
diff --git a/create/cheeps_remote_image_remote_instance.py b/create/cheeps_remote_image_remote_instance.py
index 357e0b2..5b1b70e 100644
--- a/create/cheeps_remote_image_remote_instance.py
+++ b/create/cheeps_remote_image_remote_instance.py
@@ -111,9 +111,21 @@
instance = self._compute_client.GenerateInstanceName(
build_id=self._avd_spec.remote_image[constants.BUILD_ID],
build_target=self._avd_spec.remote_image[constants.BUILD_TARGET])
+
+ # Cheeps image specified through args (if any) overrides that in the
+ # Acloud config file.
+ image_name = (self._avd_spec.stable_cheeps_host_image_name or
+ self._cfg.stable_cheeps_host_image_name)
+ image_project = (self._avd_spec.stable_cheeps_host_image_project or
+ self._cfg.stable_cheeps_host_image_project)
+ if not (image_name and image_project):
+ raise ValueError(
+ "Both Cheeps image name and project should be set, either in "
+ "Acloud config or via command line args.")
+
self._compute_client.CreateInstance(
instance=instance,
- image_name=self._cfg.stable_cheeps_host_image_name,
- image_project=self._cfg.stable_cheeps_host_image_project,
+ image_name=image_name,
+ image_project=image_project,
avd_spec=self._avd_spec)
return instance
diff --git a/create/cheeps_remote_image_remote_instance_test.py b/create/cheeps_remote_image_remote_instance_test.py
index 6484113..2b0bba2 100644
--- a/create/cheeps_remote_image_remote_instance_test.py
+++ b/create/cheeps_remote_image_remote_instance_test.py
@@ -45,6 +45,15 @@
return_value=self.compute_client)
self.Patch(auth, "CreateCredentials", return_value=mock.MagicMock())
+ # Mock uuid
+ fake_uuid = mock.MagicMock(hex="1234")
+ self.Patch(uuid, "uuid4", return_value=fake_uuid)
+
+ # Mock compute client methods
+ self.compute_client.GetInstanceIP.return_value = self.IP
+ self.compute_client.GenerateImageName.return_value = self.IMAGE
+ self.compute_client.GenerateInstanceName.return_value = self.INSTANCE
+
def _CreateCfg(self):
"""A helper method that creates a mock configuration object."""
cfg = mock.MagicMock()
@@ -57,24 +66,21 @@
cfg.stable_cheeps_host_image_project = self.CHEEPS_HOST_IMAGE_PROJECT
return cfg
- def testCreate(self):
- """Test CreateDevices."""
- # Mock uuid
- fake_uuid = mock.MagicMock(hex="1234")
- self.Patch(uuid, "uuid4", return_value=fake_uuid)
-
- # Mock compute client methods
- self.compute_client.GetInstanceIP.return_value = self.IP
- self.compute_client.GenerateImageName.return_value = self.IMAGE
- self.compute_client.GenerateInstanceName.return_value = self.INSTANCE
-
- # Call CreateDevices
+ def _CreateAvdSpec(self, stable_cheeps_host_image_name=None,
+ stable_cheeps_host_image_project=None):
avd_spec = mock.MagicMock()
avd_spec.cfg = self._CreateCfg()
avd_spec.remote_image = {constants.BUILD_ID: self.ANDROID_BUILD_ID,
constants.BUILD_TARGET: self.ANDROID_BUILD_TARGET}
avd_spec.autoconnect = False
avd_spec.report_internal_ip = False
+ avd_spec.stable_cheeps_host_image_name = stable_cheeps_host_image_name
+ avd_spec.stable_cheeps_host_image_project = stable_cheeps_host_image_project
+ return avd_spec
+
+ def testCreate(self):
+ """Test CreateDevices."""
+ avd_spec = self._CreateAvdSpec()
instance = cheeps_remote_image_remote_instance.CheepsRemoteImageRemoteInstance()
report = instance.Create(avd_spec, no_prompts=False)
@@ -95,5 +101,27 @@
self.assertEqual(report.command, "create_cheeps")
self.assertEqual(report.status, "SUCCESS")
+ def testStableCheepsHostImageArgsOverrideConfig(self):
+ """Test that Cheeps host image specifed through args (which goes into
+ avd_spec) override values set in Acloud config."""
+ stable_cheeps_host_image_name = 'override-stable-host-image-name'
+ stable_cheeps_host_image_project = 'override-stable-host-image-project'
+ self.assertNotEqual(stable_cheeps_host_image_name,
+ self.CHEEPS_HOST_IMAGE_NAME)
+ self.assertNotEqual(stable_cheeps_host_image_project,
+ self.CHEEPS_HOST_IMAGE_PROJECT)
+
+ avd_spec = self._CreateAvdSpec(stable_cheeps_host_image_name,
+ stable_cheeps_host_image_project)
+ instance = cheeps_remote_image_remote_instance.CheepsRemoteImageRemoteInstance()
+ instance.Create(avd_spec, no_prompts=False)
+
+ # Verify
+ self.compute_client.CreateInstance.assert_called_with(
+ instance=self.INSTANCE,
+ image_name=stable_cheeps_host_image_name,
+ image_project=stable_cheeps_host_image_project,
+ avd_spec=avd_spec)
+
if __name__ == "__main__":
unittest.main()
diff --git a/create/create_args.py b/create/create_args.py
index 1202119..c4d1ecc 100644
--- a/create/create_args.py
+++ b/create/create_args.py
@@ -25,6 +25,7 @@
from acloud.internal.lib import utils
+_DEFAULT_GPU = "default"
CMD_CREATE = "create"
@@ -53,15 +54,18 @@
"--autoconnect",
type=str,
nargs="?",
- const=True,
+ const=constants.INS_KEY_VNC,
dest="autoconnect",
required=False,
- choices=[True, "adb"],
- help="For each remote instance, automatically create 2 ssh tunnels "
- "forwarding both adb & vnc, and then add the device to adb. "
- "For local cuttlefish instance, create a vnc connection. "
- "For local goldfish instance, create a window."
- "If need adb only, you can pass in 'adb' here.")
+ choices=[constants.INS_KEY_VNC, constants.INS_KEY_ADB,
+ constants.INS_KEY_WEBRTC],
+ help="Determines to establish a tunnel forwarding adb/vnc and "
+ "launch VNC/webrtc. Establish a tunnel forwarding adb and vnc "
+ "then launch vnc if --autoconnect vnc is provided. Establish a "
+ "tunnel forwarding adb if --autoconnect adb is provided. "
+ "Establish a tunnel forwarding adb and auto-launch on the browser "
+ "if --autoconnect webrtc is provided. For local goldfish "
+ "instance, create a window.")
parser.add_argument(
"--no-autoconnect",
action="store_false",
@@ -69,7 +73,7 @@
required=False,
help="Will not automatically create ssh tunnels forwarding adb & vnc "
"when instance created.")
- parser.set_defaults(autoconnect=True)
+ parser.set_defaults(autoconnect=constants.INS_KEY_VNC)
parser.add_argument(
"--unlock",
action="store_true",
@@ -197,6 +201,19 @@
required=False,
default=None,
help="Disable auto download logs when AVD booting up failed.")
+ # TODO(147335651): Add gpu in user config.
+ # TODO(147335651): Support "--gpu" without giving any value.
+ parser.add_argument(
+ "--gpu",
+ type=str,
+ const=_DEFAULT_GPU,
+ nargs="?",
+ dest="gpu",
+ required=False,
+ default=None,
+ help="GPU accelerator to use if any. e.g. nvidia-tesla-k80. For local "
+ "instances, this arg without assigning any value is to enable "
+ "local gpu support.")
# TODO(b/118439885): Old arg formats to support transition, delete when
# transistion is done.
@@ -395,14 +412,6 @@
# TODO(b/118439885): Verify args that are used in wrong avd_type.
# e.g. $acloud create --avd-type cuttlefish --emulator-build-id
create_parser.add_argument(
- "--gpu",
- type=str,
- dest="gpu",
- required=False,
- default=None,
- help="'goldfish only' GPU accelerator to use if any. "
- "e.g. nvidia-tesla-k80, omit to use swiftshader")
- create_parser.add_argument(
"--emulator-build-id",
type=int,
dest="emulator_build_id",
@@ -412,6 +421,24 @@
# Arguments for cheeps type.
create_parser.add_argument(
+ "--stable-cheeps-host-image-name",
+ type=str,
+ dest="stable_cheeps_host_image_name",
+ required=False,
+ default=None,
+ help=("'cheeps only' The Cheeps host image from which instances are "
+ "launched. If specified here, the value set in Acloud config "
+ "file will be overridden."))
+ create_parser.add_argument(
+ "--stable-cheeps-host-image-project",
+ type=str,
+ dest="stable_cheeps_host_image_project",
+ required=False,
+ default=None,
+ help=("'cheeps only' The project hosting the specified Cheeps host "
+ "image. If specified here, the value set in Acloud config file "
+ "will be overridden."))
+ create_parser.add_argument(
"--user",
type=str,
dest="username",
@@ -467,6 +494,11 @@
raise errors.CheckPathError(
"Specified path doesn't exist: %s" % tool_dir)
+ if args.autoconnect == constants.INS_KEY_WEBRTC:
+ if args.avd_type != constants.TYPE_CF:
+ raise errors.UnsupportedCreateArgs(
+ "'--autoconnect webrtc' only support cuttlefish.")
+
def _VerifyHostArgs(args):
"""Verify args starting with --host.
@@ -529,6 +561,11 @@
raise errors.UnsupportedCreateArgs(
"--num is not supported for local instance.")
+ if args.local_instance is None and args.gpu == _DEFAULT_GPU:
+ raise errors.UnsupportedCreateArgs(
+ "Please assign one gpu model for GCE instance. Reference: "
+ "https://cloud.google.com/compute/docs/gpus")
+
if args.adb_port:
utils.CheckPortFree(args.adb_port)
@@ -539,9 +576,13 @@
"[%s] is an invalid hw property, supported values are:%s. "
% (key, constants.HW_PROPERTIES))
- if (args.username or args.password) and args.avd_type != constants.TYPE_CHEEPS:
- raise ValueError("--username and --password are only valid with avd_type == %s"
- % constants.TYPE_CHEEPS)
+ if args.avd_type != constants.TYPE_CHEEPS and (
+ args.stable_cheeps_host_image_name or
+ args.stable_cheeps_host_image_project or
+ args.username or args.password):
+ raise errors.UnsupportedCreateArgs(
+ "--stable-cheeps-*, --username and --password are only valid with "
+ "avd_type == %s" % constants.TYPE_CHEEPS)
if (args.username or args.password) and not (args.username and args.password):
raise ValueError("--username and --password must both be set")
if not args.autoconnect and args.unlock_screen:
diff --git a/create/create_args_test.py b/create/create_args_test.py
new file mode 100644
index 0000000..8cca9ee
--- /dev/null
+++ b/create/create_args_test.py
@@ -0,0 +1,79 @@
+# Copyright 2020 - 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 create."""
+
+import unittest
+import mock
+
+from acloud import errors
+from acloud.create import create_args
+from acloud.internal import constants
+from acloud.internal.lib import driver_test_lib
+
+
+def _CreateArgs():
+ """set default pass in arguments."""
+ mock_args = mock.MagicMock(
+ flavor=None,
+ num=None,
+ adb_port=None,
+ hw_property=None,
+ stable_cheeps_host_image_name=None,
+ stable_cheeps_host_image_project=None,
+ username=None,
+ password=None,
+ local_image="",
+ local_system_image="",
+ system_branch=None,
+ system_build_id=None,
+ system_build_target=None,
+ local_instance=None,
+ remote_host=None,
+ host_user=constants.GCE_USER,
+ host_ssh_private_key_path=None,
+ avd_type=constants.TYPE_CF,
+ autoconnect=constants.INS_KEY_VNC)
+ return mock_args
+
+
+# pylint: disable=invalid-name,protected-access
+class CreateArgsTest(driver_test_lib.BaseDriverTest):
+ """Test create_args functions."""
+
+ def testVerifyArgs(self):
+ """test VerifyArgs."""
+ mock_args = _CreateArgs()
+ # Test args default setting shouldn't raise error.
+ self.assertEqual(None, create_args.VerifyArgs(mock_args))
+
+ def testVerifyArgs_ConnectWebRTC(self):
+ """test VerifyArgs args.autconnect webrtc.
+
+ WebRTC only apply to remote cuttlefish instance
+
+ """
+ mock_args = _CreateArgs()
+ mock_args.autoconnect = constants.INS_KEY_WEBRTC
+ # Test remote instance and avd_type cuttlefish(default)
+ # Test args.autoconnect webrtc shouldn't raise error.
+ self.assertEqual(None, create_args.VerifyArgs(mock_args))
+
+ # Test pass in none-cuttlefish avd_type should raise error.
+ mock_args.avd_type = constants.TYPE_GF
+ self.assertRaises(errors.UnsupportedCreateArgs,
+ create_args.VerifyArgs, mock_args)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/create/goldfish_local_image_local_instance.py b/create/goldfish_local_image_local_instance.py
index 76191e4..b98242a 100644
--- a/create/goldfish_local_image_local_instance.py
+++ b/create/goldfish_local_image_local_instance.py
@@ -162,9 +162,6 @@
contain required files.
errors.CreateError if an instance exists and cannot be deleted.
errors.CheckPathError if OTA tools are not found.
- errors.DeviceBootTimeoutError if the emulator does not boot within
- timeout.
- errors.SubprocessFail if any command fails.
"""
if not utils.IsSupportedPlatform(print_warning=True):
result_report = report.Report(command="create")
@@ -210,16 +207,22 @@
boot_timeout_secs = (avd_spec.boot_timeout_secs or
_DEFAULT_EMULATOR_TIMEOUT_SECS)
- self._WaitForEmulatorToStart(adb, proc, boot_timeout_secs)
-
- inst.WriteCreationTimestamp()
-
result_report = report.Report(command="create")
- result_report.SetStatus(report.Status.SUCCESS)
- # Emulator has no VNC port.
- result_report.AddData(
- key="devices",
- value={constants.ADB_PORT: inst.adb_port})
+ try:
+ self._WaitForEmulatorToStart(adb, proc, boot_timeout_secs)
+ except (errors.DeviceBootTimeoutError, errors.SubprocessFail) as e:
+ result_report.SetStatus(report.Status.BOOT_FAIL)
+ result_report.AddDeviceBootFailure(inst.name, inst.ip,
+ inst.adb_port, vnc_port=None,
+ error=str(e))
+ else:
+ result_report.SetStatus(report.Status.SUCCESS)
+ result_report.AddDevice(inst.name, inst.ip, inst.adb_port,
+ vnc_port=None)
+
+ if proc.poll() is None:
+ inst.WriteCreationTimestamp()
+
return result_report
@staticmethod
diff --git a/create/goldfish_local_image_local_instance_test.py b/create/goldfish_local_image_local_instance_test.py
index db64632..1af0efc 100644
--- a/create/goldfish_local_image_local_instance_test.py
+++ b/create/goldfish_local_image_local_instance_test.py
@@ -19,12 +19,21 @@
import unittest
import mock
+from acloud import errors
import acloud.create.goldfish_local_image_local_instance as instance_module
class GoldfishLocalImageLocalInstance(unittest.TestCase):
"""Test GoldfishLocalImageLocalInstance methods."""
+ _EXPECTED_DEVICES_IN_REPORT = [
+ {
+ "instance_name": "local-goldfish-instance",
+ "ip": "127.0.0.1:5555",
+ "adb_port": 5555
+ }
+ ]
+
def setUp(self):
self._goldfish = instance_module.GoldfishLocalImageLocalInstance()
self._temp_dir = tempfile.mkdtemp()
@@ -77,10 +86,14 @@
mock_instance):
mock_utils.IsSupportedPlatform.return_value = True
- mock_instance.return_value = mock.Mock(adb_port=5555,
- console_port="5554",
- device_serial="unittest",
- instance_dir=self._instance_dir)
+ mock_instance_object = mock.Mock(ip="127.0.0.1",
+ adb_port=5555,
+ console_port="5554",
+ device_serial="unittest",
+ instance_dir=self._instance_dir)
+ # name is a positional argument of Mock().
+ mock_instance_object.name = "local-goldfish-instance"
+ mock_instance.return_value = mock_instance_object
mock_adb_tools_object = mock.Mock()
mock_adb_tools_object.EmuCommand.side_effect = self._MockEmuCommand
@@ -136,7 +149,10 @@
with mock.patch.dict("acloud.create."
"goldfish_local_image_local_instance.os.environ",
mock_environ, clear=True):
- self._goldfish._CreateAVD(mock_avd_spec, no_prompts=False)
+ report = self._goldfish._CreateAVD(mock_avd_spec, no_prompts=False)
+
+ self.assertEqual(report.data.get("devices"),
+ self._EXPECTED_DEVICES_IN_REPORT)
mock_instance.assert_called_once_with(1, avd_flavor="phone")
@@ -175,7 +191,10 @@
with mock.patch.dict("acloud.create."
"goldfish_local_image_local_instance.os.environ",
dict(), clear=True):
- self._goldfish._CreateAVD(mock_avd_spec, no_prompts=True)
+ report = self._goldfish._CreateAVD(mock_avd_spec, no_prompts=True)
+
+ self.assertEqual(report.data.get("devices"),
+ self._EXPECTED_DEVICES_IN_REPORT)
mock_instance.assert_called_once_with(2, avd_flavor="phone")
@@ -197,6 +216,42 @@
"adb_tools.AdbTools")
@mock.patch("acloud.create.goldfish_local_image_local_instance."
"subprocess.Popen")
+ def testCreateAVDTimeout(self, mock_popen, mock_adb_tools,
+ mock_utils, mock_instance):
+ """Test _CreateAVD with SDK repository files and timeout error."""
+ self._SetUpMocks(mock_popen, mock_adb_tools, mock_utils, mock_instance)
+ mock_utils.PollAndWait.side_effect = errors.DeviceBootTimeoutError(
+ "timeout")
+
+ self._CreateEmptyFile(os.path.join(self._image_dir, "system.img"))
+ self._CreateEmptyFile(os.path.join(self._image_dir, "build.prop"))
+
+ mock_avd_spec = mock.Mock(flavor="phone",
+ boot_timeout_secs=None,
+ gpu=None,
+ autoconnect=True,
+ local_instance_id=2,
+ local_image_dir=self._image_dir,
+ local_system_image_dir=None,
+ local_tool_dirs=[self._tool_dir])
+
+ with mock.patch.dict("acloud.create."
+ "goldfish_local_image_local_instance.os.environ",
+ dict(), clear=True):
+ report = self._goldfish._CreateAVD(mock_avd_spec, no_prompts=True)
+
+ self.assertEqual(report.data.get("devices_failing_boot"),
+ self._EXPECTED_DEVICES_IN_REPORT)
+ self.assertEqual(report.errors, ["timeout"])
+
+ # pylint: disable=protected-access
+ @mock.patch("acloud.create.goldfish_local_image_local_instance.instance."
+ "LocalGoldfishInstance")
+ @mock.patch("acloud.create.goldfish_local_image_local_instance.utils")
+ @mock.patch("acloud.create.goldfish_local_image_local_instance."
+ "adb_tools.AdbTools")
+ @mock.patch("acloud.create.goldfish_local_image_local_instance."
+ "subprocess.Popen")
@mock.patch("acloud.create.goldfish_local_image_local_instance.ota_tools")
def testCreateAVDWithMixedImages(self, mock_ota_tools, mock_popen,
mock_adb_tools, mock_utils,
@@ -233,7 +288,10 @@
with mock.patch.dict("acloud.create."
"goldfish_local_image_local_instance.os.environ",
mock_environ, clear=True):
- self._goldfish._CreateAVD(mock_avd_spec, no_prompts=True)
+ report = self._goldfish._CreateAVD(mock_avd_spec, no_prompts=True)
+
+ self.assertEqual(report.data.get("devices"),
+ self._EXPECTED_DEVICES_IN_REPORT)
mock_instance.assert_called_once_with(3, avd_flavor="phone")
diff --git a/create/local_image_local_instance.py b/create/local_image_local_instance.py
index 0135942..5351152 100644
--- a/create/local_image_local_instance.py
+++ b/create/local_image_local_instance.py
@@ -34,7 +34,6 @@
[CUTTLEFISH_CONFIG_FILE] which is pointing to the runtime cuttlefish json.
"""
-import json
import logging
import os
import shutil
@@ -44,7 +43,6 @@
from acloud import errors
from acloud.create import base_avd_create
-from acloud.delete import delete
from acloud.internal import constants
from acloud.internal.lib import utils
from acloud.internal.lib.adb_tools import AdbTools
@@ -56,16 +54,20 @@
logger = logging.getLogger(__name__)
_CMD_LAUNCH_CVD_ARGS = (" -daemon -cpus %s -x_res %s -y_res %s -dpi %s "
- "-memory_mb %s -system_image_dir %s "
- "-instance_dir %s")
+ "-memory_mb %s -run_adb_connector=%s "
+ "-system_image_dir %s -instance_dir %s "
+ "-undefok=report_anonymous_usage_stats "
+ "-report_anonymous_usage_stats=y")
+_CMD_LAUNCH_CVD_GPU_ARG = " -gpu_mode=drm_virgl"
_CMD_LAUNCH_CVD_DISK_ARGS = (" -blank_data_image_mb %s "
"-data_policy always_create")
+_CMD_LAUNCH_CVD_WEBRTC_ARGS = (" -guest_enforce_security=false "
+ "-vm_manager=crosvm "
+ "-start_webrtc=true "
+ "-webrtc_public_ip=%s" % constants.LOCALHOST)
_CONFIRM_RELAUNCH = ("\nCuttlefish AVD[id:%d] is already running. \n"
"Enter 'y' to terminate current instance and launch a new "
"instance, enter anything else to exit out[y/N]: ")
-_ENV_CVD_HOME = "HOME"
-_ENV_CUTTLEFISH_INSTANCE = "CUTTLEFISH_INSTANCE"
-_LAUNCH_CVD_TIMEOUT_SECS = 120 # default timeout as 120 seconds
_LAUNCH_CVD_TIMEOUT_ERROR = ("Cuttlefish AVD launch timeout, did not complete "
"within %d secs.")
_VIRTUAL_DISK_PATHS = "virtual_disk_paths"
@@ -91,38 +93,55 @@
"""
# Running instances on local is not supported on all OS.
if not utils.IsSupportedPlatform(print_warning=True):
- result_report = report.Report(constants.LOCAL_INS_NAME)
+ result_report = report.Report(command="create")
result_report.SetStatus(report.Status.FAIL)
return result_report
- self.PrintDisclaimer()
local_image_path, host_bins_path = self.GetImageArtifactsPath(avd_spec)
launch_cvd_path = os.path.join(host_bins_path, "bin",
constants.CMD_LAUNCH_CVD)
cmd = self.PrepareLaunchCVDCmd(launch_cvd_path,
avd_spec.hw_property,
+ avd_spec.connect_adb,
local_image_path,
- avd_spec.local_instance_id)
+ avd_spec.local_instance_id,
+ avd_spec.connect_webrtc,
+ avd_spec.gpu)
+
+ result_report = report.Report(command="create")
+ instance_name = instance.GetLocalInstanceName(
+ avd_spec.local_instance_id)
try:
self.CheckLaunchCVD(
- cmd, host_bins_path, avd_spec.local_instance_id, local_image_path,
- no_prompts, avd_spec.boot_timeout_secs or _LAUNCH_CVD_TIMEOUT_SECS)
+ cmd, host_bins_path, avd_spec.local_instance_id,
+ local_image_path, no_prompts,
+ avd_spec.boot_timeout_secs or constants.DEFAULT_CF_BOOT_TIMEOUT)
except errors.LaunchCVDFail as launch_error:
- raise launch_error
+ result_report.SetStatus(report.Status.BOOT_FAIL)
+ result_report.AddDeviceBootFailure(
+ instance_name, constants.LOCALHOST, None, None,
+ error=str(launch_error))
+ return result_report
- result_report = report.Report(constants.LOCAL_INS_NAME)
- result_report.SetStatus(report.Status.SUCCESS)
- local_ports = instance.GetLocalPortsbyInsId(avd_spec.local_instance_id)
- result_report.AddData(
- key="devices",
- value={constants.ADB_PORT: local_ports.adb_port,
- constants.VNC_PORT: local_ports.vnc_port})
- # Launch vnc client if we're auto-connecting.
- if avd_spec.connect_vnc:
- utils.LaunchVNCFromReport(result_report, avd_spec, no_prompts)
- if avd_spec.unlock_screen:
- AdbTools(local_ports.adb_port).AutoUnlockScreen()
+ active_ins = list_instance.GetActiveCVD(avd_spec.local_instance_id)
+ if active_ins:
+ result_report.SetStatus(report.Status.SUCCESS)
+ result_report.AddDevice(instance_name, constants.LOCALHOST,
+ active_ins.adb_port, active_ins.vnc_port)
+ # Launch vnc client if we're auto-connecting.
+ if avd_spec.connect_vnc:
+ utils.LaunchVNCFromReport(result_report, avd_spec, no_prompts)
+ if avd_spec.connect_webrtc:
+ utils.LaunchBrowserFromReport(result_report)
+ if avd_spec.unlock_screen:
+ AdbTools(active_ins.adb_port).AutoUnlockScreen()
+ else:
+ err_msg = "cvd_status return non-zero after launch_cvd"
+ logger.error(err_msg)
+ result_report.SetStatus(report.Status.BOOT_FAIL)
+ result_report.AddDeviceBootFailure(
+ instance_name, constants.LOCALHOST, None, None, error=err_msg)
return result_report
@staticmethod
@@ -161,8 +180,9 @@
self._FindCvdHostBinaries(avd_spec.local_tool_dirs))
@staticmethod
- def PrepareLaunchCVDCmd(launch_cvd_path, hw_property, system_image_dir,
- local_instance_id):
+ def PrepareLaunchCVDCmd(launch_cvd_path, hw_property, connect_adb,
+ system_image_dir, local_instance_id, connect_webrtc,
+ gpu):
"""Prepare launch_cvd command.
Create the launch_cvd commands with all the required args and add
@@ -172,7 +192,11 @@
launch_cvd_path: String of launch_cvd path.
hw_property: dict object of hw property.
system_image_dir: String of local images path.
+ connect_adb: Boolean flag that enables adb_connector.
local_instance_id: Integer of instance id.
+ connect_webrtc: Boolean of connect_webrtc.
+ gpu: String of gpu name, the gpu name of local instance should be
+ "default" if gpu is enabled.
Returns:
String, launch_cvd cmd.
@@ -180,11 +204,17 @@
instance_dir = instance.GetLocalInstanceRuntimeDir(local_instance_id)
launch_cvd_w_args = launch_cvd_path + _CMD_LAUNCH_CVD_ARGS % (
hw_property["cpu"], hw_property["x_res"], hw_property["y_res"],
- hw_property["dpi"], hw_property["memory"], system_image_dir,
+ hw_property["dpi"], hw_property["memory"],
+ ("true" if connect_adb else "false"), system_image_dir,
instance_dir)
if constants.HW_ALIAS_DISK in hw_property:
launch_cvd_w_args = (launch_cvd_w_args + _CMD_LAUNCH_CVD_DISK_ARGS %
hw_property[constants.HW_ALIAS_DISK])
+ if connect_webrtc:
+ launch_cvd_w_args = launch_cvd_w_args + _CMD_LAUNCH_CVD_WEBRTC_ARGS
+
+ if gpu:
+ launch_cvd_w_args = launch_cvd_w_args + _CMD_LAUNCH_CVD_GPU_ARG
launch_cmd = utils.AddUserGroupsToCmd(launch_cvd_w_args,
constants.LIST_CF_USER_GROUPS)
@@ -193,7 +223,7 @@
def CheckLaunchCVD(self, cmd, host_bins_path, local_instance_id,
local_image_path, no_prompts=False,
- timeout_secs=_LAUNCH_CVD_TIMEOUT_SECS):
+ timeout_secs=constants.DEFAULT_CF_BOOT_TIMEOUT):
"""Execute launch_cvd command and wait for boot up completed.
1. Check if the provided image files are in use by any launch_cvd process.
@@ -213,10 +243,11 @@
# different dir (e.g. downloaded image).
os.environ[constants.ENV_ANDROID_HOST_OUT] = host_bins_path
# Check if the instance with same id is running.
- if self.IsLocalCVDRunning(local_instance_id):
+ existing_ins = list_instance.GetActiveCVD(local_instance_id)
+ if existing_ins:
if no_prompts or utils.GetUserAnswerYes(_CONFIRM_RELAUNCH %
local_instance_id):
- self._StopCvd(host_bins_path, local_instance_id)
+ existing_ins.Delete()
else:
sys.exit(constants.EXIT_BY_USER)
else:
@@ -234,36 +265,6 @@
self._LaunchCvd(cmd, local_instance_id, timeout=timeout_secs)
@staticmethod
- def _StopCvd(host_bins_path, local_instance_id):
- """Execute stop_cvd to stop cuttlefish instance.
-
- Args:
- host_bins_path: String of host package directory.
- local_instance_id: Integer of instance id.
- """
- stop_cvd_cmd = os.path.join(host_bins_path,
- "bin",
- constants.CMD_STOP_CVD)
- with open(os.devnull, "w") as dev_null:
- cvd_env = os.environ.copy()
- cvd_env[constants.ENV_CUTTLEFISH_CONFIG_FILE] = os.path.join(
- instance.GetLocalInstanceRuntimeDir(local_instance_id),
- constants.CUTTLEFISH_CONFIG_FILE)
- subprocess.check_call(
- utils.AddUserGroupsToCmd(
- stop_cvd_cmd, constants.LIST_CF_USER_GROUPS),
- stderr=dev_null, stdout=dev_null, shell=True, env=cvd_env)
-
- # Delete ssvnc viewer
- local_ports = instance.GetLocalPortsbyInsId(local_instance_id)
- delete.CleanupSSVncviewer(local_ports.vnc_port)
- # Disconnect adb device
- adb_cmd = AdbTools(local_ports.adb_port)
- # When relaunch a local instance, we need to pass in retry=True to make
- # sure adb device is completely gone since it will use the same adb port
- adb_cmd.DisconnectAdb(retry=True)
-
- @staticmethod
@utils.TimeExecute(function_description="Waiting for AVD(s) to boot up")
def _LaunchCvd(cmd, local_instance_id, timeout=None):
"""Execute Launch CVD.
@@ -286,8 +287,8 @@
os.makedirs(cvd_runtime_dir)
cvd_env = os.environ.copy()
- cvd_env[_ENV_CVD_HOME] = cvd_home_dir
- cvd_env[_ENV_CUTTLEFISH_INSTANCE] = str(local_instance_id)
+ cvd_env[constants.ENV_CVD_HOME] = cvd_home_dir
+ cvd_env[constants.ENV_CUTTLEFISH_INSTANCE] = str(local_instance_id)
# Check the result of launch_cvd command.
# An exit code of 0 is equivalent to VIRTUAL_DEVICE_BOOT_COMPLETED
process = subprocess.Popen(cmd, shell=True, stderr=subprocess.STDOUT,
@@ -305,27 +306,6 @@
"%s/launcher.log" % (str(process.returncode), cvd_runtime_dir))
@staticmethod
- def PrintDisclaimer():
- """Print Disclaimer."""
- utils.PrintColorString(
- "(Disclaimer: Local cuttlefish instance is not a fully supported\n"
- "runtime configuration, fixing breakages is on a best effort SLO.)\n",
- utils.TextColors.WARNING)
-
- @staticmethod
- def IsLocalCVDRunning(local_instance_id):
- """Check if the AVD with specific instance id is running
-
- Args:
- local_instance_id: Integer of instance id.
-
- Return:
- Boolean, True if AVD is running.
- """
- local_ports = instance.GetLocalPortsbyInsId(local_instance_id)
- return AdbTools(local_ports.adb_port).IsAdbConnected()
-
- @staticmethod
def IsLocalImageOccupied(local_image_dir):
"""Check if the given image path is being used by a running CVD process.
@@ -335,15 +315,12 @@
Return:
Integer of instance id which using the same image path.
"""
- local_cvd_ids = list_instance.GetActiveCVDIds()
- for cvd_id in local_cvd_ids:
- cvd_config_path = os.path.join(instance.GetLocalInstanceRuntimeDir(
- cvd_id), constants.CUTTLEFISH_CONFIG_FILE)
- if not os.path.isfile(cvd_config_path):
- continue
- with open(cvd_config_path, "r") as config_file:
- json_array = json.load(config_file)
- for disk_path in json_array[_VIRTUAL_DISK_PATHS]:
+ # TODO(149602560): Remove occupied image checking after after cf disk
+ # overlay is stable
+ for cf_runtime_config_path in instance.GetAllLocalInstanceConfigs():
+ ins = instance.LocalInstance(cf_runtime_config_path)
+ if ins.CvdStatus():
+ for disk_path in ins.virtual_disk_paths:
if local_image_dir in disk_path:
- return cvd_id
+ return ins.instance_id
return None
diff --git a/create/local_image_local_instance_test.py b/create/local_image_local_instance_test.py
index a2badab..0d416ed 100644
--- a/create/local_image_local_instance_test.py
+++ b/create/local_image_local_instance_test.py
@@ -24,6 +24,7 @@
from acloud import errors
from acloud.create import local_image_local_instance
from acloud.list import instance
+from acloud.list import list as list_instance
from acloud.internal import constants
from acloud.internal.lib import driver_test_lib
from acloud.internal.lib import utils
@@ -34,20 +35,88 @@
LAUNCH_CVD_CMD_WITH_DISK = """sg group1 <<EOF
sg group2
-launch_cvd -daemon -cpus fake -x_res fake -y_res fake -dpi fake -memory_mb fake -system_image_dir fake_image_dir -instance_dir fake_cvd_dir -blank_data_image_mb fake -data_policy always_create
+launch_cvd -daemon -cpus fake -x_res fake -y_res fake -dpi fake -memory_mb fake -run_adb_connector=true -system_image_dir fake_image_dir -instance_dir fake_cvd_dir -undefok=report_anonymous_usage_stats -report_anonymous_usage_stats=y -blank_data_image_mb fake -data_policy always_create
EOF"""
LAUNCH_CVD_CMD_NO_DISK = """sg group1 <<EOF
sg group2
-launch_cvd -daemon -cpus fake -x_res fake -y_res fake -dpi fake -memory_mb fake -system_image_dir fake_image_dir -instance_dir fake_cvd_dir
+launch_cvd -daemon -cpus fake -x_res fake -y_res fake -dpi fake -memory_mb fake -run_adb_connector=true -system_image_dir fake_image_dir -instance_dir fake_cvd_dir -undefok=report_anonymous_usage_stats -report_anonymous_usage_stats=y
EOF"""
+ LAUNCH_CVD_CMD_NO_DISK_WITH_GPU = """sg group1 <<EOF
+sg group2
+launch_cvd -daemon -cpus fake -x_res fake -y_res fake -dpi fake -memory_mb fake -run_adb_connector=true -system_image_dir fake_image_dir -instance_dir fake_cvd_dir -undefok=report_anonymous_usage_stats -report_anonymous_usage_stats=y -gpu_mode=drm_virgl
+EOF"""
+
+ LAUNCH_CVD_CMD_WITH_WEBRTC = """sg group1 <<EOF
+sg group2
+launch_cvd -daemon -cpus fake -x_res fake -y_res fake -dpi fake -memory_mb fake -run_adb_connector=true -system_image_dir fake_image_dir -instance_dir fake_cvd_dir -undefok=report_anonymous_usage_stats -report_anonymous_usage_stats=y -guest_enforce_security=false -vm_manager=crosvm -start_webrtc=true -webrtc_public_ip=127.0.0.1
+EOF"""
+
+ _EXPECTED_DEVICES_IN_REPORT = [
+ {
+ "instance_name": "local-instance-1",
+ "ip": "127.0.0.1:6520",
+ "adb_port": 6520,
+ "vnc_port": 6444
+ }
+ ]
+
+ _EXPECTED_DEVICES_IN_FAILED_REPORT = [
+ {
+ "instance_name": "local-instance-1",
+ "ip": "127.0.0.1"
+ }
+ ]
+
def setUp(self):
"""Initialize new LocalImageLocalInstance."""
super(LocalImageLocalInstanceTest, self).setUp()
self.local_image_local_instance = local_image_local_instance.LocalImageLocalInstance()
# pylint: disable=protected-access
+ @mock.patch("acloud.create.local_image_local_instance.utils")
+ @mock.patch.object(local_image_local_instance.LocalImageLocalInstance,
+ "PrepareLaunchCVDCmd")
+ @mock.patch.object(local_image_local_instance.LocalImageLocalInstance,
+ "GetImageArtifactsPath")
+ @mock.patch.object(local_image_local_instance.LocalImageLocalInstance,
+ "CheckLaunchCVD")
+ def testCreateAVD(self, mock_check_launch_cvd, mock_get_image,
+ _mock_prepare, mock_utils):
+ """Test the report returned by _CreateAVD."""
+ mock_utils.IsSupportedPlatform.return_value = True
+ mock_get_image.return_value = ("/image/path", "/host/bin/path")
+ mock_avd_spec = mock.Mock(connect_adb=False, unlock_screen=False)
+ self.Patch(instance, "GetLocalInstanceName",
+ return_value="local-instance-1")
+ local_ins = mock.MagicMock(
+ adb_port=6520,
+ vnc_port=6444
+ )
+ local_ins.CvdStatus.return_value = True
+ self.Patch(instance, "LocalInstance",
+ return_value=local_ins)
+ self.Patch(list_instance, "GetActiveCVD",
+ return_value=local_ins)
+
+ # Success
+ report = self.local_image_local_instance._CreateAVD(
+ mock_avd_spec, no_prompts=True)
+
+ self.assertEqual(report.data.get("devices"),
+ self._EXPECTED_DEVICES_IN_REPORT)
+ # Failure
+ mock_check_launch_cvd.side_effect = errors.LaunchCVDFail("timeout")
+
+ report = self.local_image_local_instance._CreateAVD(
+ mock_avd_spec, no_prompts=True)
+
+ self.assertEqual(report.data.get("devices_failing_boot"),
+ self._EXPECTED_DEVICES_IN_FAILED_REPORT)
+ self.assertEqual(report.errors, ["timeout"])
+
+ # pylint: disable=protected-access
@mock.patch("acloud.create.local_image_local_instance.os.path.isfile")
def testFindCvdHostBinaries(self, mock_isfile):
"""Test FindCvdHostBinaries."""
@@ -86,23 +155,33 @@
constants.LIST_CF_USER_GROUPS = ["group1", "group2"]
launch_cmd = self.local_image_local_instance.PrepareLaunchCVDCmd(
- constants.CMD_LAUNCH_CVD, hw_property, "fake_image_dir",
- "fake_cvd_dir")
+ constants.CMD_LAUNCH_CVD, hw_property, True, "fake_image_dir",
+ "fake_cvd_dir", False, None)
self.assertEqual(launch_cmd, self.LAUNCH_CVD_CMD_WITH_DISK)
# "disk" doesn't exist in hw_property.
hw_property = {"cpu": "fake", "x_res": "fake", "y_res": "fake",
- "dpi":"fake", "memory": "fake"}
+ "dpi": "fake", "memory": "fake"}
launch_cmd = self.local_image_local_instance.PrepareLaunchCVDCmd(
- constants.CMD_LAUNCH_CVD, hw_property, "fake_image_dir",
- "fake_cvd_dir")
+ constants.CMD_LAUNCH_CVD, hw_property, True, "fake_image_dir",
+ "fake_cvd_dir", False, None)
self.assertEqual(launch_cmd, self.LAUNCH_CVD_CMD_NO_DISK)
+ # "gpu" is enabled with "default"
+ launch_cmd = self.local_image_local_instance.PrepareLaunchCVDCmd(
+ constants.CMD_LAUNCH_CVD, hw_property, True, "fake_image_dir",
+ "fake_cvd_dir", False, "default")
+ self.assertEqual(launch_cmd, self.LAUNCH_CVD_CMD_NO_DISK_WITH_GPU)
+
+ launch_cmd = self.local_image_local_instance.PrepareLaunchCVDCmd(
+ constants.CMD_LAUNCH_CVD, hw_property, True, "fake_image_dir",
+ "fake_cvd_dir", True, None)
+ self.assertEqual(launch_cmd, self.LAUNCH_CVD_CMD_WITH_WEBRTC)
+
@mock.patch.object(local_image_local_instance.LocalImageLocalInstance,
"_LaunchCvd")
@mock.patch.object(utils, "GetUserAnswerYes")
- @mock.patch.object(local_image_local_instance.LocalImageLocalInstance,
- "IsLocalCVDRunning")
+ @mock.patch.object(list_instance, "GetActiveCVD")
@mock.patch.object(local_image_local_instance.LocalImageLocalInstance,
"IsLocalImageOccupied")
def testCheckLaunchCVD(self, mock_image_occupied, mock_cvd_running,
@@ -140,7 +219,7 @@
local_instance_id,
local_image_path)
mock_launch_cvd.assert_called_once_with(
- "fake_launch_cvd", 3, timeout=local_image_local_instance._LAUNCH_CVD_TIMEOUT_SECS)
+ "fake_launch_cvd", 3, timeout=constants.DEFAULT_CF_BOOT_TIMEOUT)
# pylint: disable=protected-access
@mock.patch.dict("os.environ", clear=True)
@@ -149,8 +228,8 @@
local_instance_id = 3
launch_cvd_cmd = "launch_cvd"
cvd_env = {}
- cvd_env[local_image_local_instance._ENV_CVD_HOME] = "fake_home"
- cvd_env[local_image_local_instance._ENV_CUTTLEFISH_INSTANCE] = str(
+ cvd_env[constants.ENV_CVD_HOME] = "fake_home"
+ cvd_env[constants.ENV_CUTTLEFISH_INSTANCE] = str(
local_instance_id)
process = mock.MagicMock()
process.wait.return_value = True
diff --git a/create/local_image_remote_host.py b/create/local_image_remote_host.py
index b874136..b93d78d 100644
--- a/create/local_image_remote_host.py
+++ b/create/local_image_remote_host.py
@@ -53,7 +53,8 @@
avd_type=constants.TYPE_CF,
boot_timeout_secs=avd_spec.boot_timeout_secs,
unlock_screen=avd_spec.unlock_screen,
- wait_for_boot=False)
+ wait_for_boot=False,
+ connect_webrtc=avd_spec.connect_webrtc)
# Launch vnc client if we're auto-connecting.
if avd_spec.connect_vnc:
utils.LaunchVNCFromReport(report, avd_spec, no_prompts)
diff --git a/create/local_image_remote_instance.py b/create/local_image_remote_instance.py
index 510073d..68c935a 100644
--- a/create/local_image_remote_instance.py
+++ b/create/local_image_remote_instance.py
@@ -18,7 +18,6 @@
Create class that is responsible for creating a remote instance AVD with a
local image.
"""
-
from acloud.create import create_common
from acloud.create import base_avd_create
from acloud.internal import constants
@@ -53,8 +52,12 @@
avd_type=constants.TYPE_CF,
boot_timeout_secs=avd_spec.boot_timeout_secs,
unlock_screen=avd_spec.unlock_screen,
- wait_for_boot=False)
+ wait_for_boot=False,
+ connect_webrtc=avd_spec.connect_webrtc)
# Launch vnc client if we're auto-connecting.
if avd_spec.connect_vnc:
utils.LaunchVNCFromReport(report, avd_spec, no_prompts)
+ if avd_spec.connect_webrtc:
+ utils.LaunchBrowserFromReport(report)
+
return report
diff --git a/create/remote_image_remote_host.py b/create/remote_image_remote_host.py
index 700b169..1004fff 100644
--- a/create/remote_image_remote_host.py
+++ b/create/remote_image_remote_host.py
@@ -20,12 +20,8 @@
"""
import logging
-import os
-import shutil
-import tempfile
from acloud.create import base_avd_create
-from acloud.create import create_common
from acloud.internal import constants
from acloud.internal.lib import utils
from acloud.public.actions import common_operations
@@ -35,33 +31,6 @@
logger = logging.getLogger(__name__)
-@utils.TimeExecute(function_description="Downloading Android Build artifact")
-def DownloadAndProcessArtifact(avd_spec, extract_path):
- """Download the CF image artifacts and process them.
-
- - Download image from the Android Build system, then decompress it.
- - Download cvd host package from the Android Build system.
-
- Args:
- avd_spec: AVDSpec object that tells us what we're going to create.
- extract_path: String, path to image folder.
- """
- cfg = avd_spec.cfg
- build_id = avd_spec.remote_image[constants.BUILD_ID]
- build_target = avd_spec.remote_image[constants.BUILD_TARGET]
-
- logger.debug("Extract path: %s", extract_path)
- # Image zip
- remote_image = "%s-img-%s.zip" % (build_target.split('-')[0],
- build_id)
- create_common.DownloadRemoteArtifact(
- cfg, build_target, build_id, remote_image, extract_path, decompress=True)
- # Cvd host package
- create_common.DownloadRemoteArtifact(
- cfg, build_target, build_id, constants.CVD_HOST_PACKAGE,
- extract_path)
-
-
class RemoteImageRemoteHost(base_avd_create.BaseAVDCreate):
"""Create class for a remote image remote host AVD."""
@@ -75,13 +44,8 @@
Returns:
A Report instance.
"""
- extract_path = tempfile.mkdtemp()
- DownloadAndProcessArtifact(avd_spec, extract_path)
device_factory = remote_instance_cf_device_factory.RemoteInstanceDeviceFactory(
- avd_spec=avd_spec,
- cvd_host_package_artifact=os.path.join(
- extract_path, constants.CVD_HOST_PACKAGE),
- local_image_dir=extract_path)
+ avd_spec=avd_spec)
report = common_operations.CreateDevices(
"create_cf", avd_spec.cfg, device_factory, num=1,
report_internal_ip=avd_spec.report_internal_ip,
@@ -89,9 +53,9 @@
avd_type=constants.TYPE_CF,
boot_timeout_secs=avd_spec.boot_timeout_secs,
unlock_screen=avd_spec.unlock_screen,
- wait_for_boot=False)
+ wait_for_boot=False,
+ connect_webrtc=avd_spec.connect_webrtc)
# Launch vnc client if we're auto-connecting.
if avd_spec.connect_vnc:
utils.LaunchVNCFromReport(report, avd_spec, no_prompts)
- shutil.rmtree(extract_path)
return report
diff --git a/create/remote_image_remote_host_test.py b/create/remote_image_remote_host_test.py
index abdce58..e69de29 100644
--- a/create/remote_image_remote_host_test.py
+++ b/create/remote_image_remote_host_test.py
@@ -1,64 +0,0 @@
-# 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 remote_image_local_instance."""
-
-import unittest
-import os
-import mock
-
-from acloud.create import create_common
-from acloud.create import remote_image_remote_host
-from acloud.internal.lib import driver_test_lib
-
-
-# pylint: disable=invalid-name, protected-access
-class RemoteImageRemoteHostTest(driver_test_lib.BaseDriverTest):
- """Test remote_image_local_instance methods."""
-
- def setUp(self):
- """Initialize remote_image_local_instance."""
- super(RemoteImageRemoteHostTest, self).setUp()
- self._fake_remote_image = {"build_target" : "aosp_cf_x86_phone-userdebug",
- "build_id": "1234"}
- self._extract_path = "/tmp/1111/"
-
- @mock.patch.object(create_common, "DownloadRemoteArtifact")
- def testDownloadAndProcessArtifact(self, mock_download):
- """Test process remote cuttlefish image."""
- avd_spec = mock.MagicMock()
- avd_spec.cfg = mock.MagicMock()
- avd_spec.remote_image = self._fake_remote_image
- avd_spec.image_download_dir = "/tmp"
- self.Patch(os.path, "exists", return_value=False)
- self.Patch(os, "makedirs")
- remote_image_remote_host.DownloadAndProcessArtifact(
- avd_spec, self._extract_path)
- build_id = "1234"
- build_target = "aosp_cf_x86_phone-userdebug"
- checkfile1 = "aosp_cf_x86_phone-img-1234.zip"
- checkfile2 = "cvd-host_package.tar.gz"
-
- # To validate DownloadArtifact runs twice.
- self.assertEqual(mock_download.call_count, 2)
-
- # To validate DownloadArtifact arguments correct.
- mock_download.assert_has_calls([
- mock.call(avd_spec.cfg, build_target, build_id, checkfile1,
- self._extract_path, decompress=True),
- mock.call(avd_spec.cfg, build_target, build_id, checkfile2,
- self._extract_path)], any_order=True)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/create/remote_image_remote_instance.py b/create/remote_image_remote_instance.py
index 13d24ad..50bf99a 100644
--- a/create/remote_image_remote_instance.py
+++ b/create/remote_image_remote_instance.py
@@ -18,7 +18,6 @@
Create class that is responsible for creating a remote instance AVD with a
remote image.
"""
-
from acloud.create import base_avd_create
from acloud.internal.lib import utils
from acloud.public.actions import common_operations
@@ -50,9 +49,12 @@
avd_type=constants.TYPE_CF,
boot_timeout_secs=avd_spec.boot_timeout_secs,
unlock_screen=avd_spec.unlock_screen,
- wait_for_boot=False)
+ wait_for_boot=False,
+ connect_webrtc=avd_spec.connect_webrtc)
# Launch vnc client if we're auto-connecting.
if avd_spec.connect_vnc:
utils.LaunchVNCFromReport(report, avd_spec, no_prompts)
+ if avd_spec.connect_webrtc:
+ utils.LaunchBrowserFromReport(report)
return report
diff --git a/delete/delete.py b/delete/delete.py
index d231367..02ee484 100644
--- a/delete/delete.py
+++ b/delete/delete.py
@@ -20,7 +20,6 @@
from __future__ import print_function
import logging
-import os
import re
import subprocess
@@ -42,53 +41,9 @@
_COMMAND_GET_PROCESS_ID = ["pgrep", "run_cvd"]
_COMMAND_GET_PROCESS_COMMAND = ["ps", "-o", "command", "-p"]
_RE_RUN_CVD = re.compile(r"^(?P<run_cvd>.+run_cvd)")
-_SSVNC_VIEWER_PATTERN = "vnc://127.0.0.1:%(vnc_port)d"
_LOCAL_INSTANCE_PREFIX = "local-"
-def _GetStopCvd():
- """Get stop_cvd path.
-
- "stop_cvd" and "run_cvd" are in the same folder(host package folder).
- Try to get directory of "run_cvd" by "ps -o command -p <pid>." command.
- For example: "/tmp/bin/run_cvd"
-
- Returns:
- String of stop_cvd file path.
-
- Raises:
- errors.NoExecuteCmd: Can't find stop_cvd.
- """
- process_id = subprocess.check_output(_COMMAND_GET_PROCESS_ID)
- process_info = subprocess.check_output(
- _COMMAND_GET_PROCESS_COMMAND + process_id.splitlines())
- for process in process_info.splitlines():
- match = _RE_RUN_CVD.match(process)
- if match:
- run_cvd_path = match.group("run_cvd")
- stop_cvd_cmd = os.path.join(os.path.dirname(run_cvd_path),
- constants.CMD_STOP_CVD)
- if os.path.exists(stop_cvd_cmd):
- logger.debug("stop_cvd command: %s", stop_cvd_cmd)
- return stop_cvd_cmd
-
- default_stop_cvd = utils.FindExecutable(constants.CMD_STOP_CVD)
- if default_stop_cvd:
- return default_stop_cvd
-
- raise errors.NoExecuteCmd("Cannot find stop_cvd binary.")
-
-
-def CleanupSSVncviewer(vnc_port):
- """Cleanup the old disconnected ssvnc viewer.
-
- Args:
- vnc_port: Integer, port number of vnc.
- """
- ssvnc_viewer_pattern = _SSVNC_VIEWER_PATTERN % {"vnc_port":vnc_port}
- utils.CleanupProcess(ssvnc_viewer_pattern)
-
-
def DeleteInstances(cfg, instances_to_delete):
"""Delete instances according to instances_to_delete.
@@ -118,8 +73,8 @@
else:
remote_instance_list.append(instance.name)
# Delete ssvnc viewer
- if instance.forwarding_vnc_port:
- CleanupSSVncviewer(instance.forwarding_vnc_port)
+ if instance.vnc_port:
+ utils.CleanupSSVncviewer(instance.vnc_port)
if remote_instance_list:
# TODO(119283708): We should move DeleteAndroidVirtualDevices into
@@ -143,7 +98,14 @@
Returns:
Report instance if there are instances to delete, None otherwise.
+
+ Raises:
+ error.ConfigError: when config doesn't support remote instances.
"""
+ if not cfg.SupportRemoteInstance():
+ raise errors.ConfigError("No gcp project info found in config! "
+ "The execution of deleting remote instances "
+ "has been aborted.")
utils.PrintColorString("")
for instance in instances_to_delete:
utils.PrintColorString(" - %s" % instance, utils.TextColors.WARNING)
@@ -164,7 +126,7 @@
def DeleteLocalCuttlefishInstance(instance, delete_report):
"""Delete a local cuttlefish instance.
- Delete local instance with stop_cvd command and write delete instance
+ Delete local instance and write delete instance
information to report.
Args:
@@ -175,21 +137,12 @@
delete_report.
"""
try:
- with open(os.devnull, "w") as dev_null:
- cvd_env = os.environ.copy()
- if instance.instance_dir:
- cvd_env[constants.ENV_CUTTLEFISH_CONFIG_FILE] = os.path.join(
- instance.instance_dir, constants.CUTTLEFISH_CONFIG_FILE)
- subprocess.check_call(
- utils.AddUserGroupsToCmd(_GetStopCvd(),
- constants.LIST_CF_USER_GROUPS),
- stderr=dev_null, stdout=dev_null, shell=True, env=cvd_env)
- delete_report.SetStatus(report.Status.SUCCESS)
- device_driver.AddDeletionResultToReport(
- delete_report, [instance.name], failed=[],
- error_msgs=[],
- resource_name="instance")
- CleanupSSVncviewer(instance.vnc_port)
+ instance.Delete()
+ delete_report.SetStatus(report.Status.SUCCESS)
+ device_driver.AddDeletionResultToReport(
+ delete_report, [instance.name], failed=[],
+ error_msgs=[],
+ resource_name="instance")
except subprocess.CalledProcessError as e:
delete_report.AddError(str(e))
delete_report.SetStatus(report.Status.FAIL)
@@ -303,19 +256,16 @@
"""
# Prioritize delete instances by names without query all instance info from
# GCP project.
+ cfg = config.GetAcloudConfig(args)
if args.instance_names:
- return DeleteInstanceByNames(config.GetAcloudConfig(args),
+ return DeleteInstanceByNames(cfg,
args.instance_names)
if args.remote_host:
- cfg = config.GetAcloudConfig(args)
return CleanUpRemoteHost(cfg, args.remote_host, args.host_user,
args.host_ssh_private_key_path)
instances = list_instances.GetLocalInstances()
- if args.local_only:
- cfg = None
- else:
- cfg = config.GetAcloudConfig(args)
+ if not args.local_only and cfg.SupportRemoteInstance():
instances.extend(list_instances.GetRemoteInstances(cfg))
if args.adb_port:
diff --git a/delete/delete_test.py b/delete/delete_test.py
index f1c1e74..829ab7b 100644
--- a/delete/delete_test.py
+++ b/delete/delete_test.py
@@ -14,12 +14,10 @@
"""Tests for delete."""
import unittest
-import subprocess
import mock
from acloud.delete import delete
from acloud.internal.lib import driver_test_lib
-from acloud.internal.lib import utils
from acloud.list import list as list_instances
from acloud.public import report
@@ -28,20 +26,8 @@
class DeleteTest(driver_test_lib.BaseDriverTest):
"""Test delete functions."""
- # pylint: disable=protected-access
- @mock.patch("os.path.exists", return_value=True)
- @mock.patch("subprocess.check_output")
- def testGetStopcvd(self, mock_subprocess, mock_path_exist):
- """Test _GetStopCvd."""
- mock_subprocess.side_effect = ["fake_id",
- "/tmp/bin/run_cvd"]
- expected_value = "/tmp/bin/stop_cvd"
- self.assertEqual(expected_value, delete._GetStopCvd())
-
- @mock.patch.object(delete, "_GetStopCvd", return_value="")
@mock.patch("subprocess.check_call")
- def testDeleteLocalCuttlefishInstance(self, mock_subprocess,
- mock_get_stopcvd):
+ def testDeleteLocalCuttlefishInstance(self, mock_subprocess):
"""Test DeleteLocalCuttlefishInstance."""
mock_subprocess.return_value = True
instance_object = mock.MagicMock()
@@ -108,21 +94,6 @@
self.assertTrue(len(delete_report.errors) > 0)
self.assertEqual(delete_report.status, "FAIL")
- # pylint: disable=protected-access, no-member
- def testCleanupSSVncviwer(self):
- """test cleanup ssvnc viewer."""
- fake_vnc_port = 9999
- fake_ss_vncviewer_pattern = delete._SSVNC_VIEWER_PATTERN % {
- "vnc_port": fake_vnc_port}
- self.Patch(utils, "IsCommandRunning", return_value=True)
- self.Patch(subprocess, "check_call", return_value=True)
- delete.CleanupSSVncviewer(fake_vnc_port)
- subprocess.check_call.assert_called_with(["pkill", "-9", "-f", fake_ss_vncviewer_pattern])
-
- subprocess.check_call.call_count = 0
- self.Patch(utils, "IsCommandRunning", return_value=False)
- subprocess.check_call.assert_not_called()
-
@mock.patch.object(delete, "DeleteInstances", return_value="")
@mock.patch.object(delete, "DeleteRemoteInstances", return_value="")
def testDeleteInstanceByNames(self, mock_delete_remote_ins,
@@ -132,6 +103,7 @@
# Test delete local instances.
instances = ["local-instance-1", "local-instance-2"]
self.Patch(list_instances, "FilterInstancesByNames", return_value="")
+ self.Patch(list_instances, "GetLocalInstances", return_value=[])
delete.DeleteInstanceByNames(cfg, instances)
mock_delete_local_ins.assert_called()
diff --git a/gen_version.sh b/gen_version.sh
new file mode 100755
index 0000000..2f49dcd
--- /dev/null
+++ b/gen_version.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+OUTFILE="$1"
+BUILD_NUMBER_FROM_FILE=${OUT_DIR}/build_number.txt
+if test -f "$BUILD_NUMBER_FROM_FILE"; then
+ cp ${BUILD_NUMBER_FROM_FILE} ${OUTFILE}
+else
+ DATETIME=$(TZ='UTC' date +'%Y.%m.%d')
+ echo ${DATETIME}_local_build > ${OUTFILE}
+fi
diff --git a/internal/constants.py b/internal/constants.py
index 5890aad..a4d88dd 100755
--- a/internal/constants.py
+++ b/internal/constants.py
@@ -130,6 +130,9 @@
SSH_BIN = "ssh"
SCP_BIN = "scp"
ADB_BIN = "adb"
+# Default timeout, the unit is seconds.
+DEFAULT_SSH_TIMEOUT = 300
+DEFAULT_CF_BOOT_TIMEOUT = 450
LABEL_CREATE_BY = "created_by"
@@ -141,6 +144,7 @@
INS_KEY_IP = "ip"
INS_KEY_ADB = "adb"
INS_KEY_VNC = "vnc"
+INS_KEY_WEBRTC = "webrtc"
INS_KEY_CREATETIME = "creationTimestamp"
INS_KEY_AVD_TYPE = "avd_type"
INS_KEY_AVD_FLAVOR = "flavor"
@@ -148,8 +152,9 @@
INS_KEY_ZONE = "zone"
INS_STATUS_RUNNING = "RUNNING"
LOCAL_INS_NAME = "local-instance"
-LOCAL_INS_HOME_PREFIX = "instance_home_"
ENV_CUTTLEFISH_CONFIG_FILE = "CUTTLEFISH_CONFIG_FILE"
+ENV_CUTTLEFISH_INSTANCE = "CUTTLEFISH_INSTANCE"
+ENV_CVD_HOME = "HOME"
CUTTLEFISH_CONFIG_FILE = "cuttlefish_config.json"
TEMP_ARTIFACTS_FOLDER = "acloud_image_artifacts"
@@ -164,3 +169,10 @@
# For reuse gce instance
SELECT_ONE_GCE_INSTANCE = "select_one_gce_instance"
+
+# Webrtc
+WEBRTC_LOCAL_PORT = 8443
+WEBRTC_LOCAL_HOST = "localhost"
+
+# Remote Log
+REMOTE_LOG_FOLDER = "/home/%s/cuttlefish_runtime" % GCE_USER
diff --git a/internal/lib/adb_tools_test.py b/internal/lib/adb_tools_test.py
index cac26d0..6d753ff 100644
--- a/internal/lib/adb_tools_test.py
+++ b/internal/lib/adb_tools_test.py
@@ -16,6 +16,7 @@
import subprocess
import unittest
import mock
+from six import b
from acloud import errors
from acloud.internal.lib import adb_tools
@@ -25,13 +26,13 @@
class AdbToolsTest(driver_test_lib.BaseDriverTest):
"""Test adb functions."""
- DEVICE_ALIVE = ("List of devices attached\n"
- "127.0.0.1:48451 device product:aosp_cf_x86_phone "
- "model:Cuttlefish_x86_phone device:vsoc_x86 "
- "transport_id:98")
- DEVICE_OFFLINE = ("List of devices attached\n"
- "127.0.0.1:48451 offline")
- DEVICE_NONE = ("List of devices attached")
+ DEVICE_ALIVE = b("List of devices attached\n"
+ "127.0.0.1:48451 device product:aosp_cf_x86_phone "
+ "model:Cuttlefish_x86_phone device:vsoc_x86 "
+ "transport_id:98")
+ DEVICE_OFFLINE = b("List of devices attached\n"
+ "127.0.0.1:48451 offline")
+ DEVICE_NONE = b("List of devices attached")
# pylint: disable=no-member
def testGetAdbConnectionStatus(self):
diff --git a/internal/lib/android_compute_client.py b/internal/lib/android_compute_client.py
index 71baab1..5608e2c 100755
--- a/internal/lib/android_compute_client.py
+++ b/internal/lib/android_compute_client.py
@@ -52,9 +52,7 @@
DATA_DISK_NAME_FMT = "data-{instance}"
BOOT_COMPLETED_MSG = "VIRTUAL_DEVICE_BOOT_COMPLETED"
BOOT_STARTED_MSG = "VIRTUAL_DEVICE_BOOT_STARTED"
- BOOT_TIMEOUT_SECS = 5 * 60 # 5 mins, usually it should take ~2 mins
BOOT_CHECK_INTERVAL_SECS = 10
-
OPERATION_TIMEOUT_SECS = 20 * 60 # Override parent value, 20 mins
NAME_LENGTH_LIMIT = 63
@@ -367,7 +365,7 @@
boot_timeout_secs: Integer, the maximum time in seconds used to
wait for the AVD to boot.
"""
- boot_timeout_secs = boot_timeout_secs or self.BOOT_TIMEOUT_SECS
+ boot_timeout_secs = boot_timeout_secs or constants.DEFAULT_CF_BOOT_TIMEOUT
logger.info("Waiting for instance to boot up %s for %s secs",
instance, boot_timeout_secs)
timeout_exception = errors.DeviceBootTimeoutError(
diff --git a/internal/lib/base_cloud_client.py b/internal/lib/base_cloud_client.py
index cf9ee06..f45d74c 100755
--- a/internal/lib/base_cloud_client.py
+++ b/internal/lib/base_cloud_client.py
@@ -27,7 +27,6 @@
# pylint: disable=import-error
from apiclient import errors as gerrors
from apiclient.discovery import build
-import apiclient.http
import httplib2
from oauth2client import client
@@ -246,7 +245,7 @@
def _CallBack(request_id, response, exception):
results[request_id] = (response, self._TranslateError(exception))
- batch = apiclient.http.BatchHttpRequest()
+ batch = self._service.new_batch_http_request()
for request_id, request in six.iteritems(requests):
batch.add(
request=request, callback=_CallBack, request_id=request_id)
diff --git a/internal/lib/base_cloud_client_test.py b/internal/lib/base_cloud_client_test.py
index de74cd8..60cc1b5 100644
--- a/internal/lib/base_cloud_client_test.py
+++ b/internal/lib/base_cloud_client_test.py
@@ -21,8 +21,6 @@
import unittest
import mock
-import apiclient
-
from acloud import errors
from acloud.internal.lib import base_cloud_client
from acloud.internal.lib import driver_test_lib
@@ -59,7 +57,8 @@
return_value=mock.MagicMock())
return base_cloud_client.BaseCloudApiClient(mock.MagicMock())
- def _SetupBatchHttpRequestMock(self, rid_to_responses, rid_to_exceptions):
+ def _SetupBatchHttpRequestMock(self, rid_to_responses, rid_to_exceptions,
+ client):
"""Setup BatchHttpRequest mock."""
rid_to_exceptions = rid_to_exceptions or {}
@@ -86,10 +85,8 @@
mock_batch.execute = _Execute
return mock_batch
- self.Patch(
- apiclient.http,
- "BatchHttpRequest",
- side_effect=_CreatMockBatchHttpRequest)
+ self.Patch(client.service, "new_batch_http_request",
+ side_effect=_CreatMockBatchHttpRequest)
def testBatchExecute(self):
"""Test BatchExecute."""
@@ -103,7 +100,7 @@
error_2 = FakeError("fake retriable error.")
responses = {"r1": response, "r2": None, "r3": None}
exceptions = {"r1": None, "r2": error_1, "r3": error_2}
- self._SetupBatchHttpRequestMock(responses, exceptions)
+ self._SetupBatchHttpRequestMock(responses, exceptions, client)
results = client.BatchExecute(
requests, other_retriable_errors=(FakeError, ))
expected_results = {
diff --git a/internal/lib/cvd_compute_client_multi_stage.py b/internal/lib/cvd_compute_client_multi_stage.py
index 05bcef9..b525a71 100644
--- a/internal/lib/cvd_compute_client_multi_stage.py
+++ b/internal/lib/cvd_compute_client_multi_stage.py
@@ -55,6 +55,9 @@
logger = logging.getLogger(__name__)
_DECOMPRESS_KERNEL_ARG = "-decompress_kernel=true"
+_GPU_ARG = "-gpu_mode=drm_virgl"
+_AGREEMENT_PROMPT_ARGS = ["-undefok=report_anonymous_usage_stats",
+ "-report_anonymous_usage_stats=y"]
_DEFAULT_BRANCH = "aosp-master"
_FETCHER_BUILD_TARGET = "aosp_cf_x86_phone-userdebug"
_FETCHER_NAME = "fetch_cvd"
@@ -62,6 +65,12 @@
_FETCH_ARTIFACT = "fetch_artifact_time"
_GCE_CREATE = "gce_create_time"
_LAUNCH_CVD = "launch_cvd_time"
+# WebRTC args for launching AVD
+_GUEST_ENFORCE_SECURITY_FALSE = "--guest_enforce_security=false"
+_START_WEBRTC = "--start_webrtc"
+_VM_MANAGER = "--vm_manager=crosvm"
+_WEBRTC_ARGS = [_GUEST_ENFORCE_SECURITY_FALSE, _START_WEBRTC, _VM_MANAGER]
+_NO_RETRY = 0
def _ProcessBuild(build_id=None, branch=None, build_target=None):
@@ -92,7 +101,8 @@
oauth2_credentials,
boot_timeout_secs=None,
ins_timeout_secs=None,
- report_internal_ip=None):
+ report_internal_ip=None,
+ gpu=None):
"""Initialize.
Args:
@@ -104,6 +114,7 @@
instance ready.
report_internal_ip: Boolean to report the internal ip instead of
external ip.
+ gpu: String, GPU to attach to the device.
"""
super(CvdComputeClient, self).__init__(acloud_config, oauth2_credentials)
@@ -114,6 +125,7 @@
self._boot_timeout_secs = boot_timeout_secs
self._ins_timeout_secs = ins_timeout_secs
self._report_internal_ip = report_internal_ip
+ self._gpu = gpu
# Store all failures result when creating one or multiple instances.
self._all_failures = dict()
self._extra_args_ssh_tunnel = acloud_config.extra_args_ssh_tunnel
@@ -264,6 +276,8 @@
if constants.HW_ALIAS_MEMORY in avd_spec.hw_property:
launch_cvd_args.append(
"-memory_mb=%s" % avd_spec.hw_property[constants.HW_ALIAS_MEMORY])
+ if avd_spec.connect_webrtc:
+ launch_cvd_args.extend(_WEBRTC_ARGS)
else:
resolution = self._resolution.split("x")
launch_cvd_args.append("-x_res=" + resolution[0])
@@ -279,6 +293,10 @@
if decompress_kernel:
launch_cvd_args.append(_DECOMPRESS_KERNEL_ARG)
+ if self._gpu:
+ launch_cvd_args.append(_GPU_ARG)
+
+ launch_cvd_args.extend(_AGREEMENT_PROMPT_ARGS)
return launch_cvd_args
@staticmethod
@@ -356,10 +374,10 @@
blank_data_disk_size_gb,
kernel_build,
decompress_kernel)
- boot_timeout_secs = boot_timeout_secs or self.BOOT_TIMEOUT_SECS
+ boot_timeout_secs = boot_timeout_secs or constants.DEFAULT_CF_BOOT_TIMEOUT
ssh_command = "./bin/launch_cvd -daemon " + " ".join(launch_cvd_args)
try:
- self._ssh.Run(ssh_command, boot_timeout_secs)
+ self._ssh.Run(ssh_command, boot_timeout_secs, retry=_NO_RETRY)
except (subprocess.CalledProcessError, errors.DeviceConnectionError) as e:
# TODO(b/140475060): Distinguish the error is command return error
# or timeout error.
@@ -444,9 +462,14 @@
machine_type=self._machine_type,
network=self._network,
zone=self._zone,
- extra_scopes=extra_scopes)
+ gpu=self._gpu,
+ extra_scopes=extra_scopes,
+ tags=["appstreaming"] if (
+ avd_spec and avd_spec.connect_webrtc) else None)
ip = gcompute_client.ComputeClient.GetInstanceIP(
self, instance=instance, zone=self._zone)
+ logger.debug("'instance_ip': %s", ip.internal
+ if self._report_internal_ip else ip.external)
self._execution_time[_GCE_CREATE] = round(time.time() - timestart, 2)
return ip
@@ -499,7 +522,8 @@
if kernel_build:
fetch_cvd_args.append("-kernel_build=" + kernel_build)
- self._ssh.Run("./fetch_cvd " + " ".join(fetch_cvd_args))
+ self._ssh.Run("./fetch_cvd " + " ".join(fetch_cvd_args),
+ timeout=constants.DEFAULT_SSH_TIMEOUT)
self._execution_time[_FETCH_ARTIFACT] = round(time.time() - timestart, 2)
def GetInstanceIP(self, instance=None):
diff --git a/internal/lib/cvd_compute_client_multi_stage_test.py b/internal/lib/cvd_compute_client_multi_stage_test.py
index 2ee543a..41d05e7 100644
--- a/internal/lib/cvd_compute_client_multi_stage_test.py
+++ b/internal/lib/cvd_compute_client_multi_stage_test.py
@@ -59,6 +59,7 @@
BOOT_DISK_SIZE_GB = 10
LAUNCH_ARGS = "--setupwizard_mode=REQUIRED"
EXTRA_SCOPES = ["scope1"]
+ GPU = "fake-gpu"
def _GetFakeConfig(self):
"""Create a fake configuration object.
@@ -93,7 +94,7 @@
self.Patch(Ssh, "WaitForSsh")
self.Patch(Ssh, "GetBaseCmd")
self.cvd_compute_client_multi_stage = cvd_compute_client_multi_stage.CvdComputeClient(
- self._GetFakeConfig(), mock.MagicMock())
+ self._GetFakeConfig(), mock.MagicMock(), gpu=self.GPU)
self.args = mock.MagicMock()
self.args.local_image = None
self.args.config_file = ""
@@ -109,14 +110,18 @@
"""test GetLaunchCvdArgs."""
# test GetLaunchCvdArgs with avd_spec
fake_avd_spec = avd_spec.AVDSpec(self.args)
- expeted_args = ['-x_res=1080', '-y_res=1920', '-dpi=240', '-cpus=2',
- '-memory_mb=4096', '--setupwizard_mode=REQUIRED']
+ expeted_args = ["-x_res=1080", "-y_res=1920", "-dpi=240", "-cpus=2",
+ "-memory_mb=4096", "--setupwizard_mode=REQUIRED",
+ "-gpu_mode=drm_virgl", "-undefok=report_anonymous_usage_stats",
+ "-report_anonymous_usage_stats=y"]
launch_cvd_args = self.cvd_compute_client_multi_stage._GetLaunchCvdArgs(fake_avd_spec)
self.assertEqual(launch_cvd_args, expeted_args)
# test GetLaunchCvdArgs without avd_spec
- expeted_args = ['-x_res=720', '-y_res=1280', '-dpi=160',
- '--setupwizard_mode=REQUIRED']
+ expeted_args = ["-x_res=720", "-y_res=1280", "-dpi=160",
+ "--setupwizard_mode=REQUIRED", "-gpu_mode=drm_virgl",
+ "-undefok=report_anonymous_usage_stats",
+ "-report_anonymous_usage_stats=y"]
launch_cvd_args = self.cvd_compute_client_multi_stage._GetLaunchCvdArgs(
avd_spec=None)
self.assertEqual(launch_cvd_args, expeted_args)
@@ -157,7 +162,7 @@
created_subprocess = mock.MagicMock()
created_subprocess.stdout = mock.MagicMock()
- created_subprocess.stdout.readline = mock.MagicMock(return_value='')
+ created_subprocess.stdout.readline = mock.MagicMock(return_value=b"")
created_subprocess.poll = mock.MagicMock(return_value=0)
created_subprocess.returncode = 0
created_subprocess.communicate = mock.MagicMock(return_value=('', ''))
@@ -182,7 +187,9 @@
machine_type=self.MACHINE_TYPE,
network=self.NETWORK,
zone=self.ZONE,
- extra_scopes=self.EXTRA_SCOPES)
+ extra_scopes=self.EXTRA_SCOPES,
+ gpu=self.GPU,
+ tags=None)
mock_check_img.return_value = True
#test use local image in the remote instance.
@@ -214,7 +221,9 @@
machine_type=self.MACHINE_TYPE,
network=self.NETWORK,
zone=self.ZONE,
- extra_scopes=self.EXTRA_SCOPES)
+ extra_scopes=self.EXTRA_SCOPES,
+ gpu=self.GPU,
+ tags=None)
if __name__ == "__main__":
diff --git a/internal/lib/cvd_runtime_config.py b/internal/lib/cvd_runtime_config.py
new file mode 100644
index 0000000..2fb7893
--- /dev/null
+++ b/internal/lib/cvd_runtime_config.py
@@ -0,0 +1,221 @@
+# Copyright 2020 - 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.
+"""cvd_runtime_config class."""
+
+import json
+import os
+import re
+
+from acloud import errors
+
+_CFG_KEY_ADB_CONNECTOR_BINARY = "adb_connector_binary"
+_CFG_KEY_X_RES = "x_res"
+_CFG_KEY_Y_RES = "y_res"
+_CFG_KEY_DPI = "dpi"
+_CFG_KEY_VIRTUAL_DISK_PATHS = "virtual_disk_paths"
+_CFG_KEY_INSTANCES = "instances"
+_CFG_KEY_ADB_IP_PORT = "adb_ip_and_port"
+_CFG_KEY_INSTANCE_DIR = "instance_dir"
+_CFG_KEY_VNC_PORT = "vnc_server_port"
+_CFG_KEY_ADB_PORT = "host_port"
+_CFG_KEY_ENABLE_WEBRTC = "enable_webrtc"
+# TODO(148648620): Check instance_home_[id] for backward compatible.
+_RE_LOCAL_INSTANCE_ID = re.compile(r".+(?:local-instance-|instance_home_)"
+ r"(?P<ins_id>\d+).+")
+
+
+def _GetIdFromInstanceDirStr(instance_dir):
+ """Look for instance id from the path of instance dir.
+
+ Args:
+ instance_dir: String, path of instance_dir.
+
+ Returns:
+ String of instance id.
+ """
+ match = _RE_LOCAL_INSTANCE_ID.match(instance_dir)
+ if match:
+ return match.group("ins_id")
+ else:
+ # To support the device which is not created by acloud.
+ if os.path.expanduser("~") in instance_dir:
+ return "1"
+
+ return None
+
+
+class CvdRuntimeConfig(object):
+ """The class that hold the information from cuttlefish_config.json.
+
+ The example of cuttlefish_config.json
+ {
+ "memory_mb" : 4096,
+ "cpus" : 2,
+ "dpi" : 320,
+ "virtual_disk_paths" :
+ [
+ "/path-to-image"
+ ],
+ "adb_ip_and_port" : "127.0.0.1:6520",
+ "instance_dir" : "/path-to-instance-dir",
+ }
+
+ If we launched multiple local instances, the config will be as below:
+ {
+ "memory_mb" : 4096,
+ "cpus" : 2,
+ "dpi" : 320,
+ "instances" :
+ {
+ "1" :
+ {
+ "adb_ip_and_port" : "127.0.0.1:6520",
+ "instance_dir" : "/path-to-instance-dir",
+ "virtual_disk_paths" :
+ [
+ "/path-to-image"
+ ],
+ }
+ }
+ }
+
+ If the avd enable the webrtc, the config will be as below:
+ {
+ "enable_webrtc" : true,
+ "vnc_server_binary" : "/home/vsoc-01/bin/vnc_server",
+ "webrtc_assets_dir" : "/home/vsoc-01/usr/share/webrtc/assets",
+ "webrtc_binary" : "/home/vsoc-01/bin/webRTC",
+ "webrtc_certs_dir" : "/home/vsoc-01/usr/share/webrtc/certs",
+ "webrtc_enable_adb_websocket" : false,
+ "webrtc_public_ip" : "127.0.0.1",
+ }
+
+ """
+
+ def __init__(self, config_path=None, raw_data=None):
+ self._config_path = config_path
+ self._instance_id = "1" if raw_data else _GetIdFromInstanceDirStr(
+ config_path)
+ self._config_dict = self._GetCuttlefishRuntimeConfig(config_path,
+ raw_data)
+ self._x_res = self._config_dict.get(_CFG_KEY_X_RES)
+ self._y_res = self._config_dict.get(_CFG_KEY_Y_RES)
+ self._dpi = self._config_dict.get(_CFG_KEY_DPI)
+ adb_connector = self._config_dict.get(_CFG_KEY_ADB_CONNECTOR_BINARY)
+ self._cvd_tools_path = (os.path.dirname(adb_connector)
+ if adb_connector else None)
+
+ # Below properties will be collected inside of instance id node if there
+ # are more than one instance.
+ self._instance_dir = self._config_dict.get(_CFG_KEY_INSTANCE_DIR)
+ self._vnc_port = self._config_dict.get(_CFG_KEY_VNC_PORT)
+ self._adb_port = self._config_dict.get(_CFG_KEY_ADB_PORT)
+ self._adb_ip_port = self._config_dict.get(_CFG_KEY_ADB_IP_PORT)
+ self._virtual_disk_paths = self._config_dict.get(
+ _CFG_KEY_VIRTUAL_DISK_PATHS)
+ self._enable_webrtc = self._config_dict.get(_CFG_KEY_ENABLE_WEBRTC)
+ if not self._instance_dir:
+ ins_cfg = self._config_dict.get(_CFG_KEY_INSTANCES)
+ ins_dict = ins_cfg.get(self._instance_id)
+ if not ins_dict:
+ raise errors.ConfigError("instances[%s] property does not exist"
+ " in: %s" %
+ (self._instance_id, config_path))
+ self._instance_dir = ins_dict.get(_CFG_KEY_INSTANCE_DIR)
+ self._vnc_port = ins_dict.get(_CFG_KEY_VNC_PORT)
+ self._adb_port = ins_dict.get(_CFG_KEY_ADB_PORT)
+ self._adb_ip_port = ins_dict.get(_CFG_KEY_ADB_IP_PORT)
+ self._virtual_disk_paths = ins_dict.get(_CFG_KEY_VIRTUAL_DISK_PATHS)
+
+ @staticmethod
+ def _GetCuttlefishRuntimeConfig(runtime_cf_config_path, raw_data=None):
+ """Get and parse cuttlefish_config.json.
+
+ Args:
+ runtime_cf_config_path: String, path of the cvd runtime config.
+ raw_data: String, data of the cvd runtime config.
+
+ Returns:
+ A dictionary that parsed from cuttlefish runtime config.
+
+ Raises:
+ errors.ConfigError: if file not found or config load failed.
+ """
+ if raw_data:
+ return json.loads(raw_data)
+ if not os.path.exists(runtime_cf_config_path):
+ raise errors.ConfigError(
+ "file does not exist: %s" % runtime_cf_config_path)
+ with open(runtime_cf_config_path, "r") as cf_config:
+ return json.load(cf_config)
+
+ @property
+ def cvd_tools_path(self):
+ """Return string of the path to the cvd tools."""
+ return self._cvd_tools_path
+
+ @property
+ def x_res(self):
+ """Return x_res."""
+ return self._x_res
+
+ @property
+ def y_res(self):
+ """Return y_res."""
+ return self._y_res
+
+ @property
+ def dpi(self):
+ """Return dpi."""
+ return self._dpi
+
+ @property
+ def adb_ip_port(self):
+ """Return adb_ip_port."""
+ return self._adb_ip_port
+
+ @property
+ def instance_dir(self):
+ """Return instance_dir."""
+ return self._instance_dir
+
+ @property
+ def vnc_port(self):
+ """Return vnc_port."""
+ return self._vnc_port
+
+ @property
+ def adb_port(self):
+ """Return adb_port."""
+ return self._adb_port
+
+ @property
+ def config_path(self):
+ """Return config_path."""
+ return self._config_path
+
+ @property
+ def virtual_disk_paths(self):
+ """Return virtual_disk_paths"""
+ return self._virtual_disk_paths
+
+ @property
+ def instance_id(self):
+ """Return _instance_id"""
+ return self._instance_id
+
+ @property
+ def enable_webrtc(self):
+ """Return _enable_webrtc"""
+ return self._enable_webrtc
diff --git a/internal/lib/cvd_runtime_config_test.py b/internal/lib/cvd_runtime_config_test.py
new file mode 100644
index 0000000..f540a88
--- /dev/null
+++ b/internal/lib/cvd_runtime_config_test.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python
+#
+# Copyright 2020 - 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 cvd_runtime_config class."""
+
+import os
+import mock
+import six
+
+from acloud.internal.lib import cvd_runtime_config as cf_cfg
+from acloud.internal.lib import driver_test_lib
+
+
+class CvdRuntimeconfigTest(driver_test_lib.BaseDriverTest):
+ """Test CvdRuntimeConfig."""
+
+ CF_RUNTIME_CONFIG = """
+{"x_display" : ":20",
+ "x_res" : 720,
+ "y_res" : 1280,
+ "instances": {
+ "1":{
+ "adb_ip_and_port": "127.0.0.1:6520",
+ "host_port": 6520,
+ "instance_dir": "/path-to-instance-dir",
+ "vnc_server_port": 6444
+ }
+ }
+}
+"""
+
+ # pylint: disable=protected-access
+ def testGetCuttlefishRuntimeConfig(self):
+ """Test GetCuttlefishRuntimeConfig."""
+ # Should raise error when file does not exist.
+ self.Patch(os.path, "exists", return_value=False)
+ # Verify return data.
+ self.Patch(os.path, "exists", return_value=True)
+ expected_dict = {u'y_res': 1280,
+ u'x_res': 720,
+ u'x_display': u':20',
+ u'instances':
+ {u'1':
+ {u'adb_ip_and_port': u'127.0.0.1:6520',
+ u'host_port': 6520,
+ u'instance_dir': u'/path-to-instance-dir',
+ u'vnc_server_port': 6444}
+ },
+ }
+ mock_open = mock.mock_open(read_data=self.CF_RUNTIME_CONFIG)
+ cf_cfg_path = "/fake-path/local-instance-1/fake.config"
+ with mock.patch.object(six.moves.builtins, "open", mock_open):
+ self.assertEqual(expected_dict,
+ cf_cfg.CvdRuntimeConfig(cf_cfg_path)._config_dict)
diff --git a/internal/lib/gcompute_client_test.py b/internal/lib/gcompute_client_test.py
index d46a236..23b3d79 100644
--- a/internal/lib/gcompute_client_test.py
+++ b/internal/lib/gcompute_client_test.py
@@ -24,8 +24,6 @@
import six
# pylint: disable=import-error
-import apiclient.http
-
from acloud import errors
from acloud.internal import constants
from acloud.internal.lib import driver_test_lib
@@ -405,7 +403,9 @@
mock_batch = mock.MagicMock()
mock_batch.add = _Add
mock_batch.execute = _Execute
- self.Patch(apiclient.http, "BatchHttpRequest", return_value=mock_batch)
+ self.Patch(self.compute_client._service,
+ "new_batch_http_request",
+ return_value=mock_batch)
@mock.patch.object(gcompute_client.ComputeClient, "WaitOnOperation")
def testDeleteImages(self, mock_wait):
diff --git a/internal/lib/ota_tools.py b/internal/lib/ota_tools.py
index 25ee4f9..b7c4e30 100644
--- a/internal/lib/ota_tools.py
+++ b/internal/lib/ota_tools.py
@@ -19,6 +19,9 @@
import subprocess
import tempfile
+from six import b
+
+
from acloud import errors
from acloud.internal import constants
from acloud.internal.lib import utils
@@ -164,18 +167,18 @@
if split_line[0] == "dynamic_partition_list":
partition_names = split_line[1].split()
elif split_line[0] == "lpmake":
- output_file.write("lpmake=%s\n" % lpmake_path)
+ output_file.write(b("lpmake=%s\n" % lpmake_path))
continue
elif split_line[0].endswith("_image"):
continue
- output_file.write(line)
+ output_file.write(b(line))
if not partition_names:
logger.w("No dynamic partition list in misc info.")
for partition_name in partition_names:
- output_file.write("%s_image=%s\n" %
- (partition_name, get_image(partition_name)))
+ output_file.write(b("%s_image=%s\n" %
+ (partition_name, get_image(partition_name))))
@utils.TimeExecute(function_description="Build super image")
@utils.TimeoutException(_BUILD_SUPER_IMAGE_TIMEOUT_SECS)
@@ -246,11 +249,11 @@
for line in input_file:
split_line = line.split()
if len(split_line) == 3:
- output_file.write("%s %s %s\n" % (get_image(split_line[1]),
- split_line[1],
- split_line[2]))
+ output_file.write(b("%s %s %s\n" % (get_image(split_line[1]),
+ split_line[1],
+ split_line[2])))
else:
- output_file.write(line)
+ output_file.write(b(line))
@utils.TimeExecute(function_description="Make combined image")
@utils.TimeoutException(_MK_COMBINED_IMG_TIMEOUT_SECS)
diff --git a/internal/lib/ssh.py b/internal/lib/ssh.py
index a9a6de3..6dce93f 100755
--- a/internal/lib/ssh.py
+++ b/internal/lib/ssh.py
@@ -16,6 +16,7 @@
import logging
import subprocess
+import sys
import threading
from acloud import errors
@@ -29,16 +30,45 @@
_SSH_IDENTITY = "-l %(login_user)s %(ip_addr)s"
_SSH_CMD_MAX_RETRY = 5
_SSH_CMD_RETRY_SLEEP = 3
-_WAIT_FOR_SSH_MAX_TIMEOUT = 60
+_CONNECTION_TIMEOUT = 10
+
+
+def _SshCallWait(cmd, timeout=None):
+ """Runs a single SSH command.
+
+ - SSH returns code 0 for "Successful execution".
+ - Use wait() until the process is complete without receiving any output.
+
+ Args:
+ cmd: String of the full SSH command to run, including the SSH binary
+ and its arguments.
+ timeout: Optional integer, number of seconds to give
+
+ Returns:
+ An exit status of 0 indicates that it ran successfully.
+ """
+ logger.info("Running command \"%s\"", cmd)
+ process = subprocess.Popen(cmd, shell=True, stdin=None,
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+ if timeout:
+ # TODO: if process is killed, out error message to log.
+ timer = threading.Timer(timeout, process.kill)
+ timer.start()
+ process.wait()
+ if timeout:
+ timer.cancel()
+ return process.returncode
def _SshCall(cmd, timeout=None):
"""Runs a single SSH command.
- SSH returns code 0 for "Successful execution".
+ - SSH returns code 0 for "Successful execution".
+ - Use communicate() until the process and the child thread are complete.
Args:
- cmd: String of the full SSH command to run, including the SSH binary and its arguments.
+ cmd: String of the full SSH command to run, including the SSH binary
+ and its arguments.
timeout: Optional integer, number of seconds to give
Returns:
@@ -74,9 +104,10 @@
errors.DeviceConnectionError: Failed to connect to the GCE instance.
subprocess.CalledProc: The process exited with an error on the instance.
"""
+ # Use "exec" to let cmd to inherit the shell process, instead of having the
+ # shell launch a child process which does not get killed.
+ cmd = "exec " + cmd
logger.info("Running command \"%s\"", cmd)
- # This code could use check_output instead, but this construction supports
- # streaming the logs as they are received.
process = subprocess.Popen(cmd, shell=True, stdin=None,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
if timeout:
@@ -85,8 +116,8 @@
timer.start()
stdout, _ = process.communicate()
if stdout:
- if show_output:
- print(stdout.strip())
+ if show_output or process.returncode != 0:
+ print(stdout.strip(), file=sys.stderr)
else:
# fetch_cvd and launch_cvd can be noisy, so left at debug
logger.debug(stdout.strip())
@@ -99,7 +130,8 @@
raise subprocess.CalledProcessError(process.returncode, cmd)
-def ShellCmdWithRetry(cmd, timeout=None, show_output=False):
+def ShellCmdWithRetry(cmd, timeout=None, show_output=False,
+ retry=_SSH_CMD_MAX_RETRY):
"""Runs a shell command on remote device.
If the network is unstable and causes SSH connect fail, it will retry. When
@@ -111,14 +143,15 @@
cmd: String of the full SSH command to run, including the SSH binary and its arguments.
timeout: Optional integer, number of seconds to give.
show_output: Boolean, True to show command output in screen.
+ retry: Integer, the retry times.
Raises:
errors.DeviceConnectionError: For any non-zero return code of
remote_cmd.
"""
utils.RetryExceptionType(
- exception_types=errors.DeviceConnectionError,
- max_retries=_SSH_CMD_MAX_RETRY,
+ exception_types=(errors.DeviceConnectionError, subprocess.CalledProcessError),
+ max_retries=retry,
functor=_SshLogOutput,
sleep_multiplier=_SSH_CMD_RETRY_SLEEP,
retry_backoff_factor=utils.DEFAULT_RETRY_BACKOFF_FACTOR,
@@ -157,7 +190,8 @@
self._ssh_private_key_path = ssh_private_key_path
self._extra_args_ssh_tunnel = extra_args_ssh_tunnel
- def Run(self, target_command, timeout=None, show_output=False):
+ def Run(self, target_command, timeout=None, show_output=False,
+ retry=_SSH_CMD_MAX_RETRY):
"""Run a shell command over SSH on a remote instance.
Example:
@@ -172,10 +206,12 @@
target_command: String, text of command to run on the remote instance.
timeout: Integer, the maximum time to wait for the command to respond.
show_output: Boolean, True to show command output in screen.
+ retry: Integer, the retry times.
"""
ShellCmdWithRetry(self.GetBaseCmd(constants.SSH_BIN) + " " + target_command,
timeout,
- show_output)
+ show_output,
+ retry)
def GetBaseCmd(self, execute_bin):
"""Get a base command over SSH on a remote instance.
@@ -209,6 +245,22 @@
raise errors.UnknownType("Don't support the execute bin %s." % execute_bin)
+ def GetCmdOutput(self, cmd):
+ """Runs a single SSH command and get its output.
+
+ Args:
+ cmd: String, text of command to run on the remote instance.
+
+ Returns:
+ String of the command output.
+ """
+ ssh_cmd = "exec " + self.GetBaseCmd(constants.SSH_BIN) + " " + cmd
+ logger.info("Running command \"%s\"", ssh_cmd)
+ process = subprocess.Popen(ssh_cmd, shell=True, stdin=None,
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+ stdout, _ = process.communicate()
+ return stdout
+
def CheckSshConnection(self, timeout):
"""Run remote 'uptime' ssh command to check ssh connection.
@@ -221,33 +273,33 @@
remote_cmd = [self.GetBaseCmd(constants.SSH_BIN)]
remote_cmd.append("uptime")
- if _SshCall(" ".join(remote_cmd), timeout) == 0:
+ if _SshCallWait(" ".join(remote_cmd), timeout) == 0:
return
raise errors.DeviceConnectionError(
"Ssh isn't ready in the remote instance.")
@utils.TimeExecute(function_description="Waiting for SSH server")
- def WaitForSsh(self, timeout=None, sleep_for_retry=_SSH_CMD_RETRY_SLEEP,
- max_retry=_SSH_CMD_MAX_RETRY):
+ def WaitForSsh(self, timeout=None, max_retry=_SSH_CMD_MAX_RETRY):
"""Wait until the remote instance is ready to accept commands over SSH.
Args:
timeout: Integer, the maximum time in seconds to wait for the
command to respond.
- sleep_for_retry: Integer, the sleep time in seconds for retry.
max_retry: Integer, the maximum number of retry.
Raises:
errors.DeviceConnectionError: Ssh isn't ready in the remote instance.
"""
- timeout_one_round = timeout / max_retry if timeout else None
+ ssh_timeout = timeout or constants.DEFAULT_SSH_TIMEOUT
+ sleep_multiplier = ssh_timeout / sum(range(max_retry + 1))
+ logger.debug("Retry with interval time: %s secs", str(sleep_multiplier))
utils.RetryExceptionType(
exception_types=errors.DeviceConnectionError,
max_retries=max_retry,
functor=self.CheckSshConnection,
- sleep_multiplier=sleep_for_retry,
+ sleep_multiplier=sleep_multiplier,
retry_backoff_factor=utils.DEFAULT_RETRY_BACKOFF_FACTOR,
- timeout=timeout_one_round or _WAIT_FOR_SSH_MAX_TIMEOUT)
+ timeout=_CONNECTION_TIMEOUT)
def ScpPushFile(self, src_file, dst_file):
"""Scp push file to remote.
diff --git a/internal/lib/ssh_test.py b/internal/lib/ssh_test.py
index b640e22..4c3a425 100644
--- a/internal/lib/ssh_test.py
+++ b/internal/lib/ssh_test.py
@@ -40,7 +40,7 @@
super(SshTest, self).setUp()
self.created_subprocess = mock.MagicMock()
self.created_subprocess.stdout = mock.MagicMock()
- self.created_subprocess.stdout.readline = mock.MagicMock(return_value='')
+ self.created_subprocess.stdout.readline = mock.MagicMock(return_value=b"")
self.created_subprocess.poll = mock.MagicMock(return_value=0)
self.created_subprocess.returncode = 0
self.created_subprocess.communicate = mock.MagicMock(return_value=
@@ -48,8 +48,8 @@
def testSSHExecuteWithRetry(self):
"""test SSHExecuteWithRetry method."""
- self.Patch(subprocess, "check_call",
- side_effect=errors.DeviceConnectionError(
+ self.Patch(subprocess, "Popen",
+ side_effect=subprocess.CalledProcessError(
None, "ssh command fail."))
self.assertRaises(subprocess.CalledProcessError,
ssh.ShellCmdWithRetry,
@@ -82,7 +82,7 @@
self.Patch(subprocess, "Popen", return_value=self.created_subprocess)
ssh_object = ssh.Ssh(self.FAKE_IP, self.FAKE_SSH_USER, self.FAKE_SSH_PRIVATE_KEY_PATH)
ssh_object.Run("command")
- expected_cmd = ("/usr/bin/ssh -i /fake/acloud_rea -q -o UserKnownHostsFile=/dev/null "
+ expected_cmd = ("exec /usr/bin/ssh -i /fake/acloud_rea -q -o UserKnownHostsFile=/dev/null "
"-o StrictHostKeyChecking=no -l fake_user 1.1.1.1 command")
subprocess.Popen.assert_called_with(expected_cmd,
shell=True,
@@ -98,7 +98,7 @@
self.FAKE_SSH_PRIVATE_KEY_PATH,
self.FAKE_EXTRA_ARGS_SSH)
ssh_object.Run("command")
- expected_cmd = ("/usr/bin/ssh -i /fake/acloud_rea -q -o UserKnownHostsFile=/dev/null "
+ expected_cmd = ("exec /usr/bin/ssh -i /fake/acloud_rea -q -o UserKnownHostsFile=/dev/null "
"-o StrictHostKeyChecking=no "
"-o ProxyCommand='ssh fake_user@2.2.2.2 Server 22' "
"-l fake_user 1.1.1.1 command")
@@ -113,7 +113,7 @@
self.Patch(subprocess, "Popen", return_value=self.created_subprocess)
ssh_object = ssh.Ssh(self.FAKE_IP, self.FAKE_SSH_USER, self.FAKE_SSH_PRIVATE_KEY_PATH)
ssh_object.ScpPullFile("/tmp/test", "/tmp/test_1.log")
- expected_cmd = ("/usr/bin/scp -i /fake/acloud_rea -q -o UserKnownHostsFile=/dev/null "
+ expected_cmd = ("exec /usr/bin/scp -i /fake/acloud_rea -q -o UserKnownHostsFile=/dev/null "
"-o StrictHostKeyChecking=no fake_user@1.1.1.1:/tmp/test /tmp/test_1.log")
subprocess.Popen.assert_called_with(expected_cmd,
shell=True,
@@ -129,7 +129,7 @@
self.FAKE_SSH_PRIVATE_KEY_PATH,
self.FAKE_EXTRA_ARGS_SSH)
ssh_object.ScpPullFile("/tmp/test", "/tmp/test_1.log")
- expected_cmd = ("/usr/bin/scp -i /fake/acloud_rea -q -o UserKnownHostsFile=/dev/null "
+ expected_cmd = ("exec /usr/bin/scp -i /fake/acloud_rea -q -o UserKnownHostsFile=/dev/null "
"-o StrictHostKeyChecking=no "
"-o ProxyCommand='ssh fake_user@2.2.2.2 Server 22' "
"fake_user@1.1.1.1:/tmp/test /tmp/test_1.log")
@@ -144,7 +144,7 @@
self.Patch(subprocess, "Popen", return_value=self.created_subprocess)
ssh_object = ssh.Ssh(self.FAKE_IP, self.FAKE_SSH_USER, self.FAKE_SSH_PRIVATE_KEY_PATH)
ssh_object.ScpPushFile("/tmp/test", "/tmp/test_1.log")
- expected_cmd = ("/usr/bin/scp -i /fake/acloud_rea -q -o UserKnownHostsFile=/dev/null "
+ expected_cmd = ("exec /usr/bin/scp -i /fake/acloud_rea -q -o UserKnownHostsFile=/dev/null "
"-o StrictHostKeyChecking=no /tmp/test fake_user@1.1.1.1:/tmp/test_1.log")
subprocess.Popen.assert_called_with(expected_cmd,
shell=True,
@@ -160,7 +160,7 @@
self.FAKE_SSH_PRIVATE_KEY_PATH,
self.FAKE_EXTRA_ARGS_SSH)
ssh_object.ScpPushFile("/tmp/test", "/tmp/test_1.log")
- expected_cmd = ("/usr/bin/scp -i /fake/acloud_rea -q -o UserKnownHostsFile=/dev/null "
+ expected_cmd = ("exec /usr/bin/scp -i /fake/acloud_rea -q -o UserKnownHostsFile=/dev/null "
"-o StrictHostKeyChecking=no "
"-o ProxyCommand='ssh fake_user@2.2.2.2 Server 22' "
"/tmp/test fake_user@1.1.1.1:/tmp/test_1.log")
@@ -205,7 +205,6 @@
self.assertRaises(errors.DeviceConnectionError,
ssh_object.WaitForSsh,
timeout=1,
- sleep_for_retry=1,
max_retry=1)
diff --git a/internal/lib/utils.py b/internal/lib/utils.py
index d19d448..2f71989 100755
--- a/internal/lib/utils.py
+++ b/internal/lib/utils.py
@@ -36,6 +36,7 @@
import tempfile
import time
import uuid
+import webbrowser
import zipfile
import six
@@ -61,6 +62,18 @@
"-L %(vnc_port)d:127.0.0.1:%(target_vnc_port)d "
"-L %(adb_port)d:127.0.0.1:%(target_adb_port)d "
"-N -f -l %(ssh_user)s %(ip_addr)s")
+_SSH_TUNNEL_WEBRTC_ARGS = (
+ "-i %(rsa_key_file)s -o UserKnownHostsFile=/dev/null "
+ "-o StrictHostKeyChecking=no "
+ "%(port_mapping)s"
+ "-N -f -l %(ssh_user)s %(ip_addr)s")
+PORT_MAPPING = "-L %(local_port)d:127.0.0.1:%(target_port)d "
+_RELEASE_PORT_CMD = "kill $(lsof -t -i :%d)"
+_WEBRTC_TARGET_PORT = 8443
+WEBRTC_PORTS_MAPPING = [{"local": constants.WEBRTC_LOCAL_PORT,
+ "target": _WEBRTC_TARGET_PORT},
+ {"local": 15550, "target": 15550},
+ {"local": 15551, "target": 15551}]
_ADB_CONNECT_ARGS = "connect 127.0.0.1:%(adb_port)d"
# Store the ports that vnc/adb are forwarded to, both are integers.
ForwardedPorts = collections.namedtuple("ForwardedPorts", [constants.VNC_PORT,
@@ -86,6 +99,9 @@
_DEFAULT_DISPLAY_SCALE = 1.0
_DIST_DIR = "DIST_DIR"
+# For webrtc
+_WEBRTC_URL = "https://%(webrtc_ip)s:%(webrtc_port)d/?use_tcp=true"
+
_CONFIRM_CONTINUE = ("In order to display the screen to the AVD, we'll need to "
"install a vnc client (ssvnc). \nWould you like acloud to "
"install it for you? (%s) \nPress 'y' to continue or "
@@ -93,8 +109,9 @@
_EvaluatedResult = collections.namedtuple("EvaluatedResult",
["is_result_ok", "result_message"])
# dict of supported system and their distributions.
-_SUPPORTED_SYSTEMS_AND_DISTS = {"Linux": ["Ubuntu", "Debian"]}
+_SUPPORTED_SYSTEMS_AND_DISTS = {"Linux": ["Ubuntu", "ubuntu", "Debian", "debian"]}
_DEFAULT_TIMEOUT_ERR = "Function did not complete within %d secs."
+_SSVNC_VIEWER_PATTERN = "vnc://127.0.0.1:%(vnc_port)d"
class TempDir(object):
@@ -401,7 +418,7 @@
key_type, data, _ = elements
try:
- binary_data = base64.decodestring(data)
+ binary_data = base64.decodestring(six.b(data))
# number of bytes of int type
int_length = 4
# binary_data is like "7ssh-key..." in a binary format.
@@ -411,7 +428,7 @@
# We will verify that the rsa conforms to this format.
# ">I" in the following line means "big-endian unsigned integer".
type_length = struct.unpack(">I", binary_data[:int_length])[0]
- if binary_data[int_length:int_length + type_length] != key_type:
+ if binary_data[int_length:int_length + type_length] != six.b(key_type):
raise errors.DriverError("rsa key is invalid: %s" % rsa)
except (struct.error, binascii.Error) as e:
raise errors.DriverError(
@@ -796,6 +813,56 @@
subprocess.check_call(command, stderr=dev_null, stdout=dev_null)
+def ReleasePort(port):
+ """Release local port.
+
+ Args:
+ port: Integer of local port number.
+ """
+ try:
+ with open(os.devnull, "w") as dev_null:
+ subprocess.check_call(_RELEASE_PORT_CMD % port,
+ stderr=dev_null, stdout=dev_null, shell=True)
+ except subprocess.CalledProcessError:
+ logger.debug("The port %d is available.", constants.WEBRTC_LOCAL_PORT)
+
+
+def EstablishWebRTCSshTunnel(ip_addr, rsa_key_file, ssh_user,
+ extra_args_ssh_tunnel=None):
+ """Create ssh tunnels for webrtc.
+
+ # TODO(151418177): Before fully supporting webrtc feature, we establish one
+ # WebRTC tunnel at a time. so always delete the previous connection before
+ # establishing new one.
+
+ Args:
+ ip_addr: String, use to build the adb & vnc tunnel between local
+ and remote instance.
+ rsa_key_file: String, Private key file path to use when creating
+ the ssh tunnels.
+ ssh_user: String of user login into the instance.
+ extra_args_ssh_tunnel: String, extra args for ssh tunnel connection.
+ """
+ ReleasePort(constants.WEBRTC_LOCAL_PORT)
+ try:
+ port_mapping = [PORT_MAPPING % {
+ "local_port":port["local"],
+ "target_port":port["target"]} for port in WEBRTC_PORTS_MAPPING]
+ ssh_tunnel_args = _SSH_TUNNEL_WEBRTC_ARGS % {
+ "rsa_key_file": rsa_key_file,
+ "ssh_user": ssh_user,
+ "ip_addr": ip_addr,
+ "port_mapping":" ".join(port_mapping)}
+ ssh_tunnel_args_list = shlex.split(ssh_tunnel_args)
+ if extra_args_ssh_tunnel != None:
+ ssh_tunnel_args_list.extend(shlex.split(extra_args_ssh_tunnel))
+ _ExecuteCommand(constants.SSH_BIN, ssh_tunnel_args_list)
+ except subprocess.CalledProcessError as e:
+ PrintColorString("\n%s\nFailed to create ssh tunnels, retry with '#acloud "
+ "reconnect'." % e, TextColors.FAIL)
+
+
+# TODO(147337696): create ssh tunnels tear down as adb and vnc.
# pylint: disable=too-many-locals
def AutoConnect(ip_addr, rsa_key_file, target_vnc_port, target_adb_port,
ssh_user, client_adb_port=None, extra_args_ssh_tunnel=None):
@@ -828,7 +895,7 @@
"ssh_user": ssh_user,
"ip_addr": ip_addr}
ssh_tunnel_args_list = shlex.split(ssh_tunnel_args)
- if extra_args_ssh_tunnel:
+ if extra_args_ssh_tunnel != None:
ssh_tunnel_args_list.extend(shlex.split(extra_args_ssh_tunnel))
_ExecuteCommand(constants.SSH_BIN, ssh_tunnel_args_list)
except subprocess.CalledProcessError as e:
@@ -906,6 +973,41 @@
TextColors.FAIL)
+def LaunchBrowserFromReport(report):
+ """Open browser when autoconnect to webrtc according to the instances report.
+
+ Args:
+ report: Report object, that stores and generates report.
+ """
+ PrintColorString("(This is an experimental project for webrtc, and since "
+ "the certificate is self-signed, Chrome will mark it as "
+ "an insecure website. keep going.)",
+ TextColors.WARNING)
+
+ for device in report.data.get("devices", []):
+ if device.get("ip"):
+ LaunchBrowser(constants.WEBRTC_LOCAL_HOST,
+ constants.WEBRTC_LOCAL_PORT)
+
+
+def LaunchBrowser(ip_addr, port):
+ """Launch browser to connect the webrtc AVD.
+
+ Args:
+ ip_addr: String, use to connect to webrtc AVD on the instance.
+ port: Integer, port number.
+ """
+ webrtc_link = _WEBRTC_URL % {
+ "webrtc_ip": ip_addr,
+ "webrtc_port": port}
+ if os.environ.get(_ENV_DISPLAY, None):
+ webbrowser.open_new_tab(webrtc_link)
+ else:
+ PrintColorString("Remote terminal can't support launch webbrowser.",
+ TextColors.FAIL)
+ PrintColorString("WebRTC AVD URL: %s "% webrtc_link)
+
+
def LaunchVncClient(port, avd_width=None, avd_height=None, no_prompts=False):
"""Launch ssvnc.
@@ -922,7 +1024,7 @@
"Skipping VNC startup.", TextColors.FAIL)
return
- if not FindExecutable(_VNC_BIN):
+ if IsSupportedPlatform() and not FindExecutable(_VNC_BIN):
if no_prompts or GetUserAnswerYes(_CONFIRM_CONTINUE):
try:
PrintColorString("Installing ssvnc vnc client... ", end="")
@@ -961,7 +1063,7 @@
report: A Report instance.
"""
PrintColorString("\n")
- PrintColorString("Device(s) summary:")
+ PrintColorString("Device summary:")
for device in report.data.get("devices", []):
adb_serial = "(None)"
adb_port = device.get("adb_port")
@@ -973,6 +1075,7 @@
instance_name, instance_ip)
PrintColorString(" - device serial: %s %s" % (adb_serial,
instance_details))
+ PrintColorString(" export ANDROID_SERIAL=%s" % adb_serial)
# TODO(b/117245508): Help user to delete instance if it got created.
if report.errors:
@@ -1109,7 +1212,7 @@
platform_supported = (system in _SUPPORTED_SYSTEMS_AND_DISTS and
dist in _SUPPORTED_SYSTEMS_AND_DISTS[system])
- logger.info("supported system and dists: %s",
+ logger.info("Updated supported system and dists: %s",
_SUPPORTED_SYSTEMS_AND_DISTS)
platform_supported_msg = ("%s[%s] %s supported platform" %
(system,
@@ -1227,3 +1330,13 @@
"""
return (namedtuple_object.__dict__.items() if six.PY2
else namedtuple_object._asdict().items())
+
+
+def CleanupSSVncviewer(vnc_port):
+ """Cleanup the old disconnected ssvnc viewer.
+
+ Args:
+ vnc_port: Integer, port number of vnc.
+ """
+ ssvnc_viewer_pattern = _SSVNC_VIEWER_PATTERN % {"vnc_port":vnc_port}
+ CleanupProcess(ssvnc_viewer_pattern)
diff --git a/internal/lib/utils_test.py b/internal/lib/utils_test.py
index 480b4d2..a5e388e 100644
--- a/internal/lib/utils_test.py
+++ b/internal/lib/utils_test.py
@@ -23,10 +23,11 @@
import subprocess
import tempfile
import time
+import webbrowser
import unittest
-import mock
import six
+import mock
from acloud import errors
from acloud.internal.lib import driver_test_lib
@@ -382,7 +383,7 @@
# pylint: disable=protected-access,no-member
def testExtraArgsSSHTunnel(self):
- """Tesg extra args will be the same with expanded args."""
+ """Test extra args will be the same with expanded args."""
fake_ip_addr = "1.1.1.1"
fake_rsa_key_file = "/tmp/rsa_file"
fake_target_vnc_port = 8888
@@ -411,6 +412,88 @@
first_call_args = utils._ExecuteCommand.call_args_list[0][0]
self.assertEqual(first_call_args[1], args_list)
+ # pylint: disable=protected-access,no-member
+ def testEstablishWebRTCSshTunnel(self):
+ """Test establish WebRTC ssh tunnel."""
+ fake_ip_addr = "1.1.1.1"
+ fake_rsa_key_file = "/tmp/rsa_file"
+ ssh_user = "fake_user"
+ self.Patch(utils, "ReleasePort")
+ self.Patch(utils, "_ExecuteCommand")
+ self.Patch(subprocess, "check_call", return_value=True)
+ extra_args_ssh_tunnel = "-o command='shell %s %h' -o command1='ls -la'"
+ utils.EstablishWebRTCSshTunnel(
+ ip_addr=fake_ip_addr, rsa_key_file=fake_rsa_key_file,
+ ssh_user=ssh_user, extra_args_ssh_tunnel=None)
+ args_list = ["-i", "/tmp/rsa_file",
+ "-o", "UserKnownHostsFile=/dev/null",
+ "-o", "StrictHostKeyChecking=no",
+ "-L", "8443:127.0.0.1:8443",
+ "-L", "15550:127.0.0.1:15550",
+ "-L", "15551:127.0.0.1:15551",
+ "-N", "-f", "-l", "fake_user", "1.1.1.1"]
+ first_call_args = utils._ExecuteCommand.call_args_list[0][0]
+ self.assertEqual(first_call_args[1], args_list)
+
+ extra_args_ssh_tunnel = "-o command='shell %s %h'"
+ utils.EstablishWebRTCSshTunnel(
+ ip_addr=fake_ip_addr, rsa_key_file=fake_rsa_key_file,
+ ssh_user=ssh_user, extra_args_ssh_tunnel=extra_args_ssh_tunnel)
+ args_list_with_extra_args = ["-i", "/tmp/rsa_file",
+ "-o", "UserKnownHostsFile=/dev/null",
+ "-o", "StrictHostKeyChecking=no",
+ "-L", "8443:127.0.0.1:8443",
+ "-L", "15550:127.0.0.1:15550",
+ "-L", "15551:127.0.0.1:15551",
+ "-N", "-f", "-l", "fake_user", "1.1.1.1",
+ "-o", "command=shell %s %h"]
+ first_call_args = utils._ExecuteCommand.call_args_list[1][0]
+ self.assertEqual(first_call_args[1], args_list_with_extra_args)
+
+ # pylint: disable=protected-access, no-member
+ def testCleanupSSVncviwer(self):
+ """test cleanup ssvnc viewer."""
+ fake_vnc_port = 9999
+ fake_ss_vncviewer_pattern = utils._SSVNC_VIEWER_PATTERN % {
+ "vnc_port": fake_vnc_port}
+ self.Patch(utils, "IsCommandRunning", return_value=True)
+ self.Patch(subprocess, "check_call", return_value=True)
+ utils.CleanupSSVncviewer(fake_vnc_port)
+ subprocess.check_call.assert_called_with(["pkill", "-9", "-f", fake_ss_vncviewer_pattern])
+
+ subprocess.check_call.call_count = 0
+ self.Patch(utils, "IsCommandRunning", return_value=False)
+ utils.CleanupSSVncviewer(fake_vnc_port)
+ subprocess.check_call.assert_not_called()
+
+ def testLaunchBrowserFromReport(self):
+ """test launch browser from report."""
+ self.Patch(webbrowser, "open_new_tab")
+ fake_report = mock.MagicMock(data={})
+
+ # test remote instance
+ self.Patch(os.environ, "get", return_value=True)
+ fake_report.data = {
+ "devices": [{"instance_name": "remote_cf_instance_name",
+ "ip": "192.168.1.1",},],}
+
+ utils.LaunchBrowserFromReport(fake_report)
+ webbrowser.open_new_tab.assert_called_once_with("https://localhost:8443/?use_tcp=true")
+ webbrowser.open_new_tab.call_count = 0
+
+ # test local instance
+ fake_report.data = {
+ "devices": [{"instance_name": "local-instance1",
+ "ip": "127.0.0.1:6250",},],}
+ utils.LaunchBrowserFromReport(fake_report)
+ webbrowser.open_new_tab.assert_called_once_with("https://localhost:8443/?use_tcp=true")
+ webbrowser.open_new_tab.call_count = 0
+
+ # verify terminal can't support launch webbrowser.
+ self.Patch(os.environ, "get", return_value=False)
+ utils.LaunchBrowserFromReport(fake_report)
+ self.assertEqual(webbrowser.open_new_tab.call_count, 0)
+
if __name__ == "__main__":
unittest.main()
diff --git a/list/instance.py b/list/instance.py
index f54b5dc..e5fc400 100644
--- a/list/instance.py
+++ b/list/instance.py
@@ -28,7 +28,6 @@
import collections
import datetime
-import json
import logging
import os
import re
@@ -39,8 +38,8 @@
import dateutil.parser
import dateutil.tz
-from acloud import errors
from acloud.internal import constants
+from acloud.internal.lib import cvd_runtime_config
from acloud.internal.lib import utils
from acloud.internal.lib.adb_tools import AdbTools
@@ -49,7 +48,7 @@
_ACLOUD_CVD_TEMP = os.path.join(tempfile.gettempdir(), "acloud_cvd_temp")
_CVD_RUNTIME_FOLDER_NAME = "cuttlefish_runtime"
-_LOCAL_INSTANCE_HOME = "instance_home_%s"
+_CVD_STATUS_BIN = "cvd_status"
_MSG_UNABLE_TO_CALCULATE = "Unable to calculate"
_RE_GROUP_ADB = "local_adb_port"
_RE_GROUP_VNC = "local_vnc_port"
@@ -65,12 +64,75 @@
_LOCAL_ZONE = "local"
_FULL_NAME_STRING = ("device serial: %(device_serial)s (%(instance_name)s) "
"elapsed time: %(elapsed_time)s")
+_INDENT = " " * 3
LocalPorts = collections.namedtuple("LocalPorts", [constants.VNC_PORT,
constants.ADB_PORT])
+def GetDefaultCuttlefishConfig():
+ """Get the path of default cuttlefish instance config.
+
+ Return:
+ String, path of cf runtime config.
+ """
+ return os.path.join(os.path.expanduser("~"), _CVD_RUNTIME_FOLDER_NAME,
+ constants.CUTTLEFISH_CONFIG_FILE)
+
+
+def GetLocalInstanceName(local_instance_id):
+ """Get local cuttlefish instance name by instance id.
+
+ Args:
+ local_instance_id: Integer of instance id.
+
+ Return:
+ String, the instance name.
+ """
+ return "%s-%d" % (constants.LOCAL_INS_NAME, local_instance_id)
+
+
+def GetLocalInstanceConfig(local_instance_id):
+ """Get the path of instance config.
+
+ Args:
+ local_instance_id: Integer of instance id.
+
+ Return:
+ String, path of cf runtime config.
+ """
+ cfg_path = os.path.join(GetLocalInstanceRuntimeDir(local_instance_id),
+ constants.CUTTLEFISH_CONFIG_FILE)
+ if os.path.isfile(cfg_path):
+ return cfg_path
+ return None
+
+
+def GetAllLocalInstanceConfigs():
+ """Get the list of instance config.
+
+ Return:
+ List of instance config path.
+ """
+ cfg_list = []
+ # Check if any instance config is under home folder.
+ cfg_path = GetDefaultCuttlefishConfig()
+ if os.path.isfile(cfg_path):
+ cfg_list.append(cfg_path)
+
+ # Check if any instance config is under acloud cvd temp folder.
+ if os.path.exists(_ACLOUD_CVD_TEMP):
+ for ins_name in os.listdir(_ACLOUD_CVD_TEMP):
+ cfg_path = os.path.join(_ACLOUD_CVD_TEMP,
+ ins_name,
+ _CVD_RUNTIME_FOLDER_NAME,
+ constants.CUTTLEFISH_CONFIG_FILE)
+ if os.path.isfile(cfg_path):
+ cfg_list.append(cfg_path)
+ return cfg_list
+
+
def GetLocalInstanceHomeDir(local_instance_id):
- """Get local instance home dir accroding to instance id.
+ """Get local instance home dir according to instance id.
Args:
local_instance_id: Integer of instance id.
@@ -79,7 +141,7 @@
String, path of instance home dir.
"""
return os.path.join(_ACLOUD_CVD_TEMP,
- _LOCAL_INSTANCE_HOME % local_instance_id)
+ GetLocalInstanceName(local_instance_id))
def GetLocalInstanceRuntimeDir(local_instance_id):
@@ -95,41 +157,6 @@
_CVD_RUNTIME_FOLDER_NAME)
-def GetCuttlefishRuntimeConfig(local_instance_id):
- """Get and parse cuttlefish_config.json.
-
- Args:
- local_instance_id: Integer of instance id.
-
- Returns:
- A dictionary that parsed from cuttlefish runtime config.
-
- Raises:
- errors.ConfigError: if file not found or config load failed.
- """
- runtime_cf_config_path = os.path.join(GetLocalInstanceRuntimeDir(
- local_instance_id), constants.CUTTLEFISH_CONFIG_FILE)
- if not os.path.exists(runtime_cf_config_path):
- raise errors.ConfigError(
- "file does not exist: %s" % runtime_cf_config_path)
- with open(runtime_cf_config_path, "r") as cf_config:
- return json.load(cf_config)
-
-
-def GetLocalPortsbyInsId(local_instance_id):
- """Get vnc and adb port by local instance id.
-
- Args:
- local_instance_id: local_instance_id: Integer of instance id.
-
- Returns:
- NamedTuple of (vnc_port, adb_port) used by local instance, both are
- integers.
- """
- return LocalPorts(vnc_port=constants.CF_VNC_PORT + local_instance_id - 1,
- adb_port=constants.CF_ADB_PORT + local_instance_id - 1)
-
-
def _GetCurrentLocalTime():
"""Return a datetime object for current time in local time zone."""
return datetime.datetime.now(dateutil.tz.tzlocal())
@@ -190,31 +217,30 @@
def Summary(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 elapse time: %s" % (indent, self._elapsed_time))
- representation.append("%s status: %s" % (indent, self._status))
- representation.append("%s avd type: %s" % (indent, self._avd_type))
- representation.append("%s display: %s" % (indent, self._display))
- representation.append("%s vnc: 127.0.0.1:%s" % (indent, self._vnc_port))
- representation.append("%s zone: %s" % (indent, self._zone))
+ representation.append("%s IP: %s" % (_INDENT, self._ip))
+ representation.append("%s create time: %s" % (_INDENT, self._createtime))
+ representation.append("%s elapse time: %s" % (_INDENT, self._elapsed_time))
+ representation.append("%s status: %s" % (_INDENT, self._status))
+ representation.append("%s avd type: %s" % (_INDENT, self._avd_type))
+ representation.append("%s display: %s" % (_INDENT, self._display))
+ representation.append("%s vnc: 127.0.0.1:%s" % (_INDENT, self._vnc_port))
+ representation.append("%s zone: %s" % (_INDENT, self._zone))
- if self._adb_port:
+ if self._adb_port and self._device_information:
representation.append("%s adb serial: 127.0.0.1:%s" %
- (indent, self._adb_port))
+ (_INDENT, self._adb_port))
representation.append("%s product: %s" % (
- indent, self._device_information["product"]))
+ _INDENT, self._device_information["product"]))
representation.append("%s model: %s" % (
- indent, self._device_information["model"]))
+ _INDENT, self._device_information["model"]))
representation.append("%s device: %s" % (
- indent, self._device_information["device"]))
+ _INDENT, self._device_information["device"]))
representation.append("%s transport_id: %s" % (
- indent, self._device_information["transport_id"]))
+ _INDENT, self._device_information["transport_id"]))
else:
- representation.append("%s adb serial: disconnected" % indent)
+ representation.append("%s adb serial: disconnected" % _INDENT)
return "\n".join(representation)
@@ -244,16 +270,6 @@
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 ssh_tunnel_is_connected(self):
"""Return the connect status."""
return self._ssh_tunnel_is_connected
@@ -296,55 +312,153 @@
class LocalInstance(Instance):
"""Class to store data of local cuttlefish instance."""
-
- def __init__(self, local_instance_id, x_res, y_res, dpi, create_time,
- ins_dir=None):
+ def __init__(self, cf_config_path):
"""Initialize a localInstance object.
Args:
- local_instance_id: Integer of instance id.
- x_res: Integer of x dimension.
- y_res: Integer of y dimension.
- dpi: Integer of dpi.
- date_str: String of create time.
- ins_dir: String, path of instance idr.
+ cf_config_path: String, path to the cf runtime config.
"""
- display = _DISPLAY_STRING % {"x_res": x_res, "y_res": y_res,
- "dpi": dpi}
- elapsed_time = _GetElapsedTime(create_time) if create_time else None
- name = "%s-%d" % (constants.LOCAL_INS_NAME, local_instance_id)
- local_ports = GetLocalPortsbyInsId(local_instance_id)
+ self._cf_runtime_cfg = cvd_runtime_config.CvdRuntimeConfig(cf_config_path)
+ self._instance_dir = self._cf_runtime_cfg.instance_dir
+ self._virtual_disk_paths = self._cf_runtime_cfg.virtual_disk_paths
+ self._local_instance_id = int(self._cf_runtime_cfg.instance_id)
+
+ display = _DISPLAY_STRING % {"x_res": self._cf_runtime_cfg.x_res,
+ "y_res": self._cf_runtime_cfg.y_res,
+ "dpi": self._cf_runtime_cfg.dpi}
+ # TODO(143063678), there's no createtime info in
+ # cuttlefish_config.json so far.
+ name = GetLocalInstanceName(self._local_instance_id)
fullname = (_FULL_NAME_STRING %
- {"device_serial": "127.0.0.1:%d" % local_ports.adb_port,
+ {"device_serial": "127.0.0.1:%s" % self._cf_runtime_cfg.adb_port,
"instance_name": name,
- "elapsed_time": elapsed_time})
- adb_device = AdbTools(local_ports.adb_port)
+ "elapsed_time": None})
+ adb_device = AdbTools(self._cf_runtime_cfg.adb_port)
device_information = None
if adb_device.IsAdbConnected():
device_information = adb_device.device_information
super(LocalInstance, self).__init__(
name=name, fullname=fullname, display=display, ip="127.0.0.1",
- status=constants.INS_STATUS_RUNNING, adb_port=local_ports.adb_port,
- vnc_port=local_ports.vnc_port, createtime=create_time,
- elapsed_time=elapsed_time, avd_type=constants.TYPE_CF,
+ status=constants.INS_STATUS_RUNNING,
+ adb_port=self._cf_runtime_cfg.adb_port,
+ vnc_port=self._cf_runtime_cfg.vnc_port,
+ createtime=None, elapsed_time=None, avd_type=constants.TYPE_CF,
is_local=True, device_information=device_information,
zone=_LOCAL_ZONE)
- # LocalInstance class properties
- self._instance_dir = ins_dir
+ def Summary(self):
+ """Return the string that this class is holding."""
+ instance_home = "%s instance home: %s" % (_INDENT, self._instance_dir)
+ return "%s\n%s" % (super(LocalInstance, self).Summary(), instance_home)
+
+ def CvdStatus(self):
+ """check if local instance is active.
+
+ Execute cvd_status cmd to check if it exit without error.
+
+ Returns
+ True if instance is active.
+ """
+ if not self._cf_runtime_cfg.cvd_tools_path:
+ logger.debug("No cvd tools path found from config:%s",
+ self._cf_runtime_cfg.config_path)
+ return False
+ cvd_env = os.environ.copy()
+ cvd_env[constants.ENV_CUTTLEFISH_CONFIG_FILE] = self._cf_runtime_cfg.config_path
+ cvd_env[constants.ENV_CVD_HOME] = GetLocalInstanceHomeDir(self._local_instance_id)
+ cvd_env[constants.ENV_CUTTLEFISH_INSTANCE] = str(self._local_instance_id)
+ try:
+ cvd_status_cmd = os.path.join(self._cf_runtime_cfg.cvd_tools_path,
+ _CVD_STATUS_BIN)
+ # TODO(b/150575261): Change the cvd home and cvd artifact path to
+ # another place instead of /tmp to prevent from the file not
+ # found exception.
+ if not os.path.exists(cvd_status_cmd):
+ logger.warning("Cvd tools path doesn't exist:%s", cvd_status_cmd)
+ if os.environ.get(constants.ENV_ANDROID_HOST_OUT) in cvd_status_cmd:
+ utils.PrintColorString(
+ "Can't find the cvd_status tool (Try lunching a "
+ "cuttlefish target like aosp_cf_x86_phone-userdebug "
+ "and running 'make hosttar' before list/delete local "
+ "instances)", utils.TextColors.WARNING)
+ return False
+ logger.debug("Running cmd[%s] to check cvd status.", cvd_status_cmd)
+ process = subprocess.Popen(cvd_status_cmd,
+ stdin=None,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ env=cvd_env)
+ stdout, _ = process.communicate()
+ if process.returncode != 0:
+ if stdout:
+ logger.debug("Local instance[%s] is not active: %s",
+ self.name, stdout.strip())
+ return False
+ return True
+ except subprocess.CalledProcessError as cpe:
+ logger.error("Failed to run cvd_status: %s", cpe.output)
+ return False
+
+ def Delete(self):
+ """Execute stop_cvd to stop local cuttlefish instance.
+
+ - We should get the same host tool used to launch cvd to delete instance
+ , So get stop_cvd bin from the cvd runtime config.
+ - Add CUTTLEFISH_CONFIG_FILE env variable to tell stop_cvd which cvd
+ need to be deleted.
+ - Stop adb since local instance use the fixed adb port and could be
+ reused again soon.
+ """
+ stop_cvd_cmd = os.path.join(self.cf_runtime_cfg.cvd_tools_path,
+ constants.CMD_STOP_CVD)
+ logger.debug("Running cmd[%s] to delete local cvd", stop_cvd_cmd)
+ with open(os.devnull, "w") as dev_null:
+ cvd_env = os.environ.copy()
+ if self.instance_dir:
+ cvd_env[constants.ENV_CUTTLEFISH_CONFIG_FILE] = self._cf_runtime_cfg.config_path
+ cvd_env[constants.ENV_CVD_HOME] = GetLocalInstanceHomeDir(
+ self._local_instance_id)
+ cvd_env[constants.ENV_CUTTLEFISH_INSTANCE] = str(self._local_instance_id)
+ else:
+ logger.error("instance_dir is null!! instance[%d] might not be"
+ " deleted", self._local_instance_id)
+ subprocess.check_call(
+ utils.AddUserGroupsToCmd(stop_cvd_cmd,
+ constants.LIST_CF_USER_GROUPS),
+ stderr=dev_null, stdout=dev_null, shell=True, env=cvd_env)
+
+ adb_cmd = AdbTools(self.adb_port)
+ # When relaunch a local instance, we need to pass in retry=True to make
+ # sure adb device is completely gone since it will use the same adb port
+ adb_cmd.DisconnectAdb(retry=True)
@property
def instance_dir(self):
"""Return _instance_dir."""
return self._instance_dir
+ @property
+ def instance_id(self):
+ """Return _local_instance_id."""
+ return self._local_instance_id
+
+ @property
+ def virtual_disk_paths(self):
+ """Return virtual_disk_paths"""
+ return self._virtual_disk_paths
+
+ @property
+ def cf_runtime_cfg(self):
+ """Return _cf_runtime_cfg"""
+ return self._cf_runtime_cfg
+
class LocalGoldfishInstance(Instance):
"""Class to store data of local goldfish instance."""
- _INSTANCE_DIR_PATTERN = re.compile(r"^instance-(?P<id>\d+)$")
- _INSTANCE_DIR_FORMAT = "instance-%(id)s"
+ _INSTANCE_NAME_PATTERN = re.compile(
+ r"^local-goldfish-instance-(?P<id>\d+)$")
_CREATION_TIMESTAMP_FILE_NAME = "creation_timestamp.txt"
_INSTANCE_NAME_FORMAT = "local-goldfish-instance-%(id)s"
_EMULATOR_DEFAULT_CONSOLE_PORT = 5554
@@ -411,7 +525,7 @@
def instance_dir(self):
"""Return the path to instance directory."""
return os.path.join(self._GetInstanceDirRoot(),
- self._INSTANCE_DIR_FORMAT % {"id": self._id})
+ self._INSTANCE_NAME_FORMAT % {"id": self._id})
@property
def creation_timestamp_path(self):
@@ -449,7 +563,7 @@
instances = []
for name in os.listdir(instance_root):
- match = cls._INSTANCE_DIR_PATTERN.match(name)
+ match = cls._INSTANCE_NAME_PATTERN.match(name)
timestamp_path = os.path.join(instance_root, name,
cls._CREATION_TIMESTAMP_FILE_NAME)
if match and os.path.isfile(timestamp_path):
diff --git a/list/instance_test.py b/list/instance_test.py
index 635a512..39dad5a 100644
--- a/list/instance_test.py
+++ b/list/instance_test.py
@@ -17,19 +17,18 @@
import collections
import datetime
-import os
import subprocess
import unittest
import mock
-import six
+from six import b
# pylint: disable=import-error
import dateutil.parser
import dateutil.tz
-from acloud import errors
from acloud.internal import constants
+from acloud.internal.lib import cvd_runtime_config
from acloud.internal.lib import driver_test_lib
from acloud.internal.lib.adb_tools import AdbTools
from acloud.list import instance
@@ -37,13 +36,13 @@
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 1.1.1.1")
- PS_LAUNCH_CVD = ("Sat Nov 10 21:55:10 2018 /fake_path/bin/run_cvd ")
+ PS_SSH_TUNNEL = b("/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 1.1.1.1")
+ PS_LAUNCH_CVD = b("Sat Nov 10 21:55:10 2018 /fake_path/bin/run_cvd ")
PS_RUNTIME_CF_CONFIG = {"x_res": "1080", "y_res": "1920", "dpi": "480"}
GCE_INSTANCE = {
constants.INS_KEY_NAME: "fake_ins_name",
@@ -64,25 +63,30 @@
def testCreateLocalInstance(self):
""""Test get local instance info from launch_cvd process."""
self.Patch(subprocess, "check_output", return_value=self.PS_LAUNCH_CVD)
- self.Patch(instance, "_GetElapsedTime", return_value="fake_time")
- local_instance = instance.LocalInstance(2,
- "1080",
- "1920",
- "480",
- "Sat Nov 10 21:55:10 2018",
- "fake_instance_dir")
+ cf_config = mock.MagicMock(
+ instance_id=2,
+ x_res=1080,
+ y_res=1920,
+ dpi=480,
+ instance_dir="fake_instance_dir",
+ adb_port=6521,
+ vnc_port=6445,
+ adb_ip_port="127.0.0.1:6521",
+ )
+ self.Patch(cvd_runtime_config, "CvdRuntimeConfig",
+ return_value=cf_config)
+ local_instance = instance.LocalInstance(cf_config)
self.assertEqual(constants.LOCAL_INS_NAME + "-2", 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) elapsed time: %s"
% ("6521",
constants.LOCAL_INS_NAME + "-2",
- "fake_time"))
+ "None"))
self.assertEqual(expected_full_name, local_instance.fullname)
- self.assertEqual(6521, local_instance.forwarding_adb_port)
- self.assertEqual(6445, local_instance.forwarding_vnc_port)
+ self.assertEqual(6521, local_instance.adb_port)
+ self.assertEqual(6445, local_instance.vnc_port)
@mock.patch("acloud.list.instance.tempfile")
@mock.patch("acloud.list.instance.AdbTools")
@@ -100,7 +104,7 @@
self.assertEqual(inst.console_port, 5554)
self.assertEqual(inst.device_serial, "emulator-5554")
self.assertEqual(inst.instance_dir,
- "/unit/test/acloud_gf_temp/instance-1")
+ "/unit/test/acloud_gf_temp/local-goldfish-instance-1")
@mock.patch("acloud.list.instance.open",
mock.mock_open(read_data="test createtime"))
@@ -117,25 +121,30 @@
mock_get_elapsed_time.return_value = datetime.timedelta(hours=10)
mock_adb_tools.return_value = mock.Mock(device_information={})
mock_tempfile.gettempdir.return_value = "/unit/test"
- mock_isdir.side_effect = lambda path: (
- path == "/unit/test/acloud_gf_temp")
+ acloud_gf_temp_path = "/unit/test/acloud_gf_temp"
+ subdir_names = (
+ "local-goldfish-instance-1",
+ "local-goldfish-instance-2",
+ "local-goldfish-instance-3")
+ timestamp_paths = (
+ "/unit/test/acloud_gf_temp/local-goldfish-instance-1/"
+ "creation_timestamp.txt",
+ "/unit/test/acloud_gf_temp/local-goldfish-instance-2/"
+ "creation_timestamp.txt",
+ "/unit/test/acloud_gf_temp/local-goldfish-instance-3/"
+ "creation_timestamp.txt")
+ mock_isdir.side_effect = lambda path: path == acloud_gf_temp_path
mock_listdir.side_effect = lambda path: (
- ["instance-1", "instance-2", "instance-3"] if
- path == "/unit/test/acloud_gf_temp" else [])
- mock_isfile.side_effect = lambda path: path in (
- "/unit/test/acloud_gf_temp/instance-1/creation_timestamp.txt",
- "/unit/test/acloud_gf_temp/instance-3/creation_timestamp.txt")
+ subdir_names if path == acloud_gf_temp_path else [])
+ mock_isfile.side_effect = lambda path: (
+ path in (timestamp_paths[0], timestamp_paths[2]))
instances = instance.LocalGoldfishInstance.GetExistingInstances()
- mock_isdir.assert_called_with("/unit/test/acloud_gf_temp")
- mock_listdir.assert_called_with("/unit/test/acloud_gf_temp")
- mock_isfile.assert_any_call(
- "/unit/test/acloud_gf_temp/instance-1/creation_timestamp.txt")
- mock_isfile.assert_any_call(
- "/unit/test/acloud_gf_temp/instance-2/creation_timestamp.txt")
- mock_isfile.assert_any_call(
- "/unit/test/acloud_gf_temp/instance-3/creation_timestamp.txt")
+ mock_isdir.assert_called_with(acloud_gf_temp_path)
+ mock_listdir.assert_called_with(acloud_gf_temp_path)
+ for timestamp_path in timestamp_paths:
+ mock_isfile.assert_any_call(timestamp_path)
self.assertEqual(len(instances), 2)
self.assertEqual(instances[0].console_port, 5554)
self.assertEqual(instances[0].createtime, "test createtime")
@@ -210,8 +219,8 @@
# test ssh_tunnel_is_connected will be true if ssh tunnel connection is found
instance_info = instance.RemoteInstance(self.GCE_INSTANCE)
self.assertTrue(instance_info.ssh_tunnel_is_connected)
- self.assertEqual(instance_info.forwarding_adb_port, fake_adb)
- self.assertEqual(instance_info.forwarding_vnc_port, fake_vnc)
+ self.assertEqual(instance_info.adb_port, fake_adb)
+ self.assertEqual(instance_info.vnc_port, fake_vnc)
self.assertEqual("1.1.1.1", instance_info.ip)
self.assertEqual("fake_status", instance_info.status)
self.assertEqual("fake_type", instance_info.avd_type)
@@ -288,19 +297,6 @@
" adb serial: disconnected")
self.assertEqual(remote_instance.Summary(), result_summary)
- def testGetCuttlefishRuntimeConfig(self):
- """Test GetCuttlefishRuntimeConfig."""
- # Should raise error when file does not exist.
- self.Patch(os.path, "exists", return_value=False)
- self.assertRaises(errors.ConfigError, instance.GetCuttlefishRuntimeConfig, 9)
- # Verify return data.
- self.Patch(os.path, "exists", return_value=True)
- fake_runtime_cf_config = ("{\"x_display\" : \":20\",\"x_res\" : 720,\"y_res\" : 1280}")
- mock_open = mock.mock_open(read_data=fake_runtime_cf_config)
- with mock.patch.object(six.moves.builtins, "open", mock_open):
- self.assertEqual({u'y_res': 1280, u'x_res': 720, u'x_display': u':20'},
- instance.GetCuttlefishRuntimeConfig(1))
-
def testGetZoneName(self):
"""Test GetZoneName."""
zone_info = "v1/projects/project/zones/us-central1-c"
diff --git a/list/list.py b/list/list.py
index 23d6296..febd6f3 100644
--- a/list/list.py
+++ b/list/list.py
@@ -20,8 +20,7 @@
from __future__ import print_function
import getpass
import logging
-import re
-import subprocess
+import os
from acloud import errors
from acloud.internal import constants
@@ -35,29 +34,6 @@
logger = logging.getLogger(__name__)
_COMMAND_PS_LAUNCH_CVD = ["ps", "-wweo", "lstart,cmd"]
-_RE_LOCAL_INSTANCE_ID = re.compile(r".+instance_home_(?P<ins_id>\d+).+")
-_RE_LOCAL_CVD_PORT = re.compile(r"^127\.0\.0\.1:65(?P<cvd_port_suffix>\d{2})\s+")
-
-
-def GetActiveCVDIds():
- """Get active local cvd ids from adb devices.
-
- The adb port of local instance will be decided according to instance id.
- The rule of adb port will be '6520 + [instance id] - 1'. So we grep last
- two digits of port and calculate the instance id.
-
- Return:
- List of cvd id.
- """
- local_cvd_ids = []
- adb_cmd = [constants.ADB_BIN, "devices"]
- device_info = subprocess.check_output(adb_cmd)
- for device in device_info.splitlines():
- match = _RE_LOCAL_CVD_PORT.match(device)
- if match:
- cvd_serial = match.group("cvd_port_suffix")
- local_cvd_ids.append(int(cvd_serial) - 19)
- return local_cvd_ids
def _ProcessInstances(instance_list):
@@ -145,31 +121,39 @@
Returns:
instance_list: List of local instances.
"""
- local_cvd_ids = GetActiveCVDIds()
local_instance_list = []
- for cvd_id in local_cvd_ids:
- ins_dir = x_res = y_res = dpi = cf_runtime_config_dict = None
- try:
- cf_runtime_config_dict = instance.GetCuttlefishRuntimeConfig(cvd_id)
- except errors.ConfigError:
- logger.error("Instance[id:%d] dir not found!", cvd_id)
-
- if cf_runtime_config_dict:
- ins_dir = instance.GetLocalInstanceRuntimeDir(cvd_id)
- x_res = cf_runtime_config_dict["x_res"]
- y_res = cf_runtime_config_dict["y_res"]
- dpi = cf_runtime_config_dict["dpi"]
- # TODO(143063678), there's no createtime info in
- # cuttlefish_config.json so far.
- local_instance_list.append(instance.LocalInstance(cvd_id,
- x_res,
- y_res,
- dpi,
- None,
- ins_dir))
+ for cf_runtime_config_path in instance.GetAllLocalInstanceConfigs():
+ ins = instance.LocalInstance(cf_runtime_config_path)
+ if ins.CvdStatus():
+ local_instance_list.append(ins)
+ else:
+ logger.info("cvd runtime config found but instance is not active:%s"
+ , cf_runtime_config_path)
return local_instance_list
+def GetActiveCVD(local_instance_id):
+ """Check if the local AVD with specific instance id is running
+
+ Args:
+ local_instance_id: Integer of instance id.
+
+ Return:
+ LocalInstance object.
+ """
+ cfg_path = instance.GetLocalInstanceConfig(local_instance_id)
+ if cfg_path:
+ ins = instance.LocalInstance(cfg_path)
+ if ins.CvdStatus():
+ return ins
+ cfg_path = instance.GetDefaultCuttlefishConfig()
+ if local_instance_id == 1 and os.path.isfile(cfg_path):
+ ins = instance.LocalInstance(cfg_path)
+ if ins.CvdStatus():
+ return ins
+ return None
+
+
def GetLocalInstances():
"""Look for local cuttleifsh and goldfish instances.
@@ -184,27 +168,6 @@
instance.LocalGoldfishInstance.GetExistingInstances())
-def _GetIdFromInstanceDirStr(instance_dir):
- """Look for instance id from the path of instance dir.
-
- Args:
- instance_dir: String, path of instance_dir.
-
- Returns:
- Integer of instance id.
-
- Raises:
- errors.InvalidInstanceDir: Invalid instance idr.
- """
- match = _RE_LOCAL_INSTANCE_ID.match(instance_dir)
- if match:
- return int(match.group("ins_id"))
-
- raise errors.InvalidInstanceDir("Instance dir is invalid:%s. local AVD "
- "launched outside acloud is not supported"
- % instance_dir)
-
-
def GetInstances(cfg):
"""Look for remote/local instances.
@@ -342,7 +305,7 @@
"""
all_instance_info = []
for instance_object in instances:
- if instance_object.forwarding_adb_port == adb_port:
+ if instance_object.adb_port == adb_port:
return [instance_object]
all_instance_info.append(instance_object.fullname)
@@ -375,8 +338,8 @@
args: Namespace object from argparse.parse_args.
"""
instances = GetLocalInstances()
- if not args.local_only:
- cfg = config.GetAcloudConfig(args)
+ cfg = config.GetAcloudConfig(args)
+ if not args.local_only and cfg.SupportRemoteInstance():
instances.extend(GetRemoteInstances(cfg))
PrintInstancesDetails(instances, args.verbose)
diff --git a/list/list_test.py b/list/list_test.py
index 7250d75..a4b466c 100644
--- a/list/list_test.py
+++ b/list/list_test.py
@@ -12,12 +12,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for list."""
-import subprocess
+
import unittest
import mock
from acloud import errors
+from acloud.internal.lib import cvd_runtime_config
from acloud.internal.lib import driver_test_lib
from acloud.internal.lib import utils
from acloud.list import list as list_instance
@@ -91,7 +92,7 @@
def testFilterInstancesByAdbPort(self):
"""test FilterInstancesByAdbPort."""
alive_instance1 = InstanceObject("alive_instance1")
- alive_instance1.forwarding_adb_port = 1111
+ alive_instance1.adb_port = 1111
alive_instance1.fullname = "device serial: 127.0.0.1:1111 alive_instance1"
expected_instance = [alive_instance1]
# Test to find instance by adb port number.
@@ -105,23 +106,40 @@
# pylint: disable=protected-access
def testGetLocalCuttlefishInstances(self):
"""test _GetLocalCuttlefishInstances."""
- cf_config = mock.MagicMock()
-
# Test getting two instance case
- self.Patch(list_instance, "GetActiveCVDIds", return_value=[1, 2])
- self.Patch(instance, "GetCuttlefishRuntimeConfig", return_value=cf_config)
+ self.Patch(instance, "GetAllLocalInstanceConfigs",
+ return_value=["fake_path1", "fake_path2"])
self.Patch(instance, "GetLocalInstanceRuntimeDir")
- self.Patch(instance, "LocalInstance")
+
+ local_ins = mock.MagicMock()
+ local_ins.CvdStatus.return_value = True
+ self.Patch(instance, "LocalInstance", return_value=local_ins)
ins_list = list_instance._GetLocalCuttlefishInstances()
self.assertEqual(2, len(ins_list))
+ local_ins = mock.MagicMock()
+ local_ins.CvdStatus.return_value = False
+ self.Patch(instance, "LocalInstance", return_value=local_ins)
+ ins_list = list_instance._GetLocalCuttlefishInstances()
+ self.assertEqual(0, len(ins_list))
+
# pylint: disable=no-member
def testPrintInstancesDetails(self):
"""test PrintInstancesDetails."""
# Test instance Summary should be called if verbose
self.Patch(instance.Instance, "Summary")
- ins = instance.LocalInstance(1, 728, 728, 240, None, "fake_dir")
+ cf_config = mock.MagicMock(
+ x_res=728,
+ y_res=728,
+ dpi=240,
+ instance_dir="fake_dir",
+ adb_ip_port="127.0.0.1:6520"
+ )
+ self.Patch(cvd_runtime_config, "CvdRuntimeConfig",
+ return_value=cf_config)
+
+ ins = instance.LocalInstance("fake_cf_path")
list_instance.PrintInstancesDetails([ins], verbose=True)
instance.Instance.Summary.assert_called_once()
@@ -134,15 +152,6 @@
list_instance.PrintInstancesDetails([], verbose=True)
instance.Instance.Summary.assert_not_called()
- # pylint: disable=no-member
- def testGetActiveCVDIds(self):
- """test GetActiveCVDIds."""
- # Test getting two local devices
- adb_output = "127.0.0.1:6520 device\n127.0.0.1:6521 device"
- expected_result = [1, 2]
- self.Patch(subprocess, "check_output", return_value=adb_output)
- self.assertEqual(list_instance.GetActiveCVDIds(), expected_result)
-
if __name__ == "__main__":
unittest.main()
diff --git a/public/acloud_main.py b/public/acloud_main.py
index f622661..b26a01e 100644
--- a/public/acloud_main.py
+++ b/public/acloud_main.py
@@ -128,6 +128,7 @@
LOGGING_FMT = "%(asctime)s |%(levelname)s| %(module)s:%(lineno)s| %(message)s"
ACLOUD_LOGGER = "acloud"
NO_ERROR_MESSAGE = ""
+PROG = "acloud"
# Commands
CMD_CREATE_CUTTLEFISH = "create_cf"
@@ -156,6 +157,9 @@
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
usage="acloud {" + usage + "} ...")
+ parser = argparse.ArgumentParser(prog=PROG)
+ parser.add_argument('--version', action='version', version=(
+ '%(prog)s ' + config.GetVersion()))
subparsers = parser.add_subparsers(metavar="{" + usage + "}")
subparser_list = []
@@ -163,6 +167,13 @@
create_cf_parser = subparsers.add_parser(CMD_CREATE_CUTTLEFISH)
create_cf_parser.required = False
create_cf_parser.set_defaults(which=CMD_CREATE_CUTTLEFISH)
+ create_cf_parser.add_argument(
+ "--num-avds-per-instance",
+ type=int,
+ dest="num_avds_per_instance",
+ required=False,
+ default=1,
+ help="Number of devices to create on a gce instance.")
create_args.AddCommonCreateArgs(create_cf_parser)
subparser_list.append(create_cf_parser)
@@ -189,14 +200,6 @@
help="Emulator build branch name, e.g. aosp-emu-master-dev. If specified"
" without emulator_build_id, the last green build will be used.")
create_gf_parser.add_argument(
- "--gpu",
- type=str,
- dest="gpu",
- required=False,
- default=None,
- help="GPU accelerator to use if any."
- " e.g. nvidia-tesla-k80, omit to use swiftshader")
- create_gf_parser.add_argument(
"--base_image",
type=str,
dest="base_image",
@@ -355,6 +358,9 @@
_SetupLogging(args.log_file, args.verbose)
_VerifyArgs(args)
+ if args.verbose:
+ print("%s %s" % (PROG, config.GetVersion()))
+
cfg = config.GetAcloudConfig(args)
# TODO: Move this check into the functions it is actually needed.
# Check access.
@@ -375,6 +381,7 @@
system_branch=args.system_branch,
system_build_id=args.system_build_id,
system_build_target=args.system_build_target,
+ gpu=args.gpu,
num=args.num,
serial_log_file=args.serial_log_file,
autoconnect=args.autoconnect,
diff --git a/public/actions/common_operations.py b/public/actions/common_operations.py
index 65c0471..3aa014a 100644
--- a/public/actions/common_operations.py
+++ b/public/actions/common_operations.py
@@ -169,7 +169,7 @@
report_internal_ip=False, autoconnect=False,
serial_log_file=None, client_adb_port=None,
boot_timeout_secs=None, unlock_screen=False,
- wait_for_boot=True):
+ wait_for_boot=True, connect_webrtc=False):
"""Create a set of devices using the given factory.
Main jobs in create devices.
@@ -191,6 +191,7 @@
unlock_screen: Boolean, whether to unlock screen after invoke vnc client.
wait_for_boot: Boolean, True to check serial log include boot up
message.
+ connect_webrtc: Boolean, whether to auto connect webrtc to device.
Raises:
errors: Create instance fail.
@@ -244,6 +245,12 @@
device_dict[constants.ADB_PORT] = forwarded_ports.adb_port
if unlock_screen:
AdbTools(forwarded_ports.adb_port).AutoUnlockScreen()
+ if connect_webrtc:
+ utils.EstablishWebRTCSshTunnel(
+ ip_addr=ip,
+ rsa_key_file=cfg.ssh_private_key_path,
+ ssh_user=constants.GCE_USER,
+ extra_args_ssh_tunnel=cfg.extra_args_ssh_tunnel)
if device.instance_name in failures:
reporter.AddData(key="devices_failing_boot", value=device_dict)
reporter.AddError(str(failures[device.instance_name]))
diff --git a/public/actions/create_cuttlefish_action.py b/public/actions/create_cuttlefish_action.py
index 4293251..bc1886c 100644
--- a/public/actions/create_cuttlefish_action.py
+++ b/public/actions/create_cuttlefish_action.py
@@ -43,25 +43,27 @@
build_target: String,Target name.
build_id: String, Build id, e.g. "2263051", "P2804227"
kernel_build_id: String, Kernel build id.
+ gpu: String, GPU to attach to the device or None. e.g. "nvidia-tesla-k80"
"""
LOG_FILES = ["/home/vsoc-01/cuttlefish_runtime/kernel.log",
"/home/vsoc-01/cuttlefish_runtime/logcat",
"/home/vsoc-01/cuttlefish_runtime/cuttlefish_config.json"]
+ #pylint: disable=too-many-locals
def __init__(self, cfg, build_target, build_id, branch=None,
kernel_build_id=None, kernel_branch=None,
kernel_build_target=None, system_branch=None,
system_build_id=None, system_build_target=None,
boot_timeout_secs=None, ins_timeout_secs=None,
- report_internal_ip=None):
+ report_internal_ip=None, gpu=None):
self.credentials = auth.CreateCredentials(cfg)
if cfg.enable_multi_stage:
compute_client = cvd_compute_client_multi_stage.CvdComputeClient(
cfg, self.credentials, boot_timeout_secs, ins_timeout_secs,
- report_internal_ip)
+ report_internal_ip, gpu)
else:
compute_client = cvd_compute_client.CvdComputeClient(
cfg, self.credentials)
@@ -190,6 +192,7 @@
system_branch=None,
system_build_id=None,
system_build_target=None,
+ gpu=None,
num=1,
serial_log_file=None,
autoconnect=False,
@@ -209,6 +212,7 @@
system_branch: Branch name to consume the system.img from, a string.
system_build_id: System branch build id, a string.
system_build_target: System image build target, a string.
+ gpu: String, GPU to attach to the device or None. e.g. "nvidia-tesla-k80"
num: Integer, Number of devices to create.
serial_log_file: String, A path to a tar file where serial output should
be saved to.
@@ -238,12 +242,13 @@
"system_branch: %s, "
"system_build_id: %s, "
"system_build_target: %s, "
+ "gpu: %s"
"num: %s, "
"serial_log_file: %s, "
"autoconnect: %s, "
"report_internal_ip: %s", cfg.project, build_target,
build_id, branch, kernel_build_id, kernel_branch, kernel_build_target,
- system_branch, system_build_id, system_build_target, num,
+ system_branch, system_build_id, system_build_target, gpu, num,
serial_log_file, autoconnect, report_internal_ip)
# If multi_stage enable, launch_cvd don't write serial log to instance. So
# it doesn't go WaitForBoot function.
@@ -257,7 +262,8 @@
system_build_target=system_build_target,
boot_timeout_secs=boot_timeout_secs,
ins_timeout_secs=ins_timeout_secs,
- report_internal_ip=report_internal_ip)
+ report_internal_ip=report_internal_ip,
+ gpu=gpu)
return common_operations.CreateDevices("create_cf", cfg, device_factory,
num, constants.TYPE_CF,
report_internal_ip, autoconnect,
diff --git a/public/actions/remote_instance_cf_device_factory.py b/public/actions/remote_instance_cf_device_factory.py
index 82ab5fe..0b40545 100644
--- a/public/actions/remote_instance_cf_device_factory.py
+++ b/public/actions/remote_instance_cf_device_factory.py
@@ -18,8 +18,10 @@
import glob
import logging
import os
+import shutil
+import tempfile
-from acloud import errors
+from acloud.create import create_common
from acloud.internal import constants
from acloud.internal.lib import auth
from acloud.internal.lib import cvd_compute_client_multi_stage
@@ -48,13 +50,12 @@
ssh: An Ssh object.
"""
def __init__(self, avd_spec, local_image_artifact=None,
- cvd_host_package_artifact=None, local_image_dir=None):
+ cvd_host_package_artifact=None):
"""Constructs a new remote instance device factory."""
self._avd_spec = avd_spec
self._cfg = avd_spec.cfg
self._local_image_artifact = local_image_artifact
self._cvd_host_package_artifact = cvd_host_package_artifact
- self._local_image_dir = local_image_dir
self._report_internal_ip = avd_spec.report_internal_ip
self.credentials = auth.CreateCredentials(avd_spec.cfg)
# Control compute_client with enable_multi_stage
@@ -62,10 +63,12 @@
acloud_config=avd_spec.cfg,
oauth2_credentials=self.credentials,
ins_timeout_secs=avd_spec.ins_timeout_secs,
- report_internal_ip=avd_spec.report_internal_ip)
+ report_internal_ip=avd_spec.report_internal_ip,
+ gpu=avd_spec.gpu)
super(RemoteInstanceDeviceFactory, self).__init__(compute_client)
self._ssh = None
+ # pylint: disable=broad-except
def CreateInstance(self):
"""Create a single configured cuttlefish device.
@@ -98,7 +101,7 @@
self._ProcessArtifacts(self._avd_spec.image_source)
self._LaunchCvd(instance=instance,
boot_timeout_secs=self._avd_spec.boot_timeout_secs)
- except errors.DeviceConnectionError as e:
+ except Exception as e:
self._SetFailures(instance, e)
return instance
@@ -141,6 +144,30 @@
self._ssh, ip, self._avd_spec.host_user)
return instance
+ @utils.TimeExecute(function_description="Downloading Android Build artifact")
+ def _DownloadArtifacts(self, extract_path):
+ """Download the CF image artifacts and process them.
+
+ - Download image from the Android Build system, then decompress it.
+ - Download cvd host package from the Android Build system.
+
+ Args:
+ extract_path: String, a path include extracted files.
+ """
+ cfg = self._avd_spec.cfg
+ build_id = self._avd_spec.remote_image[constants.BUILD_ID]
+ build_target = self._avd_spec.remote_image[constants.BUILD_TARGET]
+
+ # Image zip
+ remote_image = "%s-img-%s.zip" % (build_target.split('-')[0], build_id)
+ create_common.DownloadRemoteArtifact(
+ cfg, build_target, build_id, remote_image, extract_path, decompress=True)
+
+ # Cvd host package
+ create_common.DownloadRemoteArtifact(
+ cfg, build_target, build_id, constants.CVD_HOST_PACKAGE,
+ extract_path)
+
def _ProcessRemoteHostArtifacts(self):
"""Process remote host artifacts.
@@ -150,9 +177,21 @@
build to local and unzip it then upload to remote host, because there
is no permission to fetch build rom on the remote host.
"""
- self._UploadArtifacts(
- self._local_image_artifact, self._cvd_host_package_artifact,
- self._local_image_dir or self._avd_spec.local_image_dir)
+ if self._avd_spec.image_source == constants.IMAGE_SRC_LOCAL:
+ self._UploadArtifacts(
+ self._local_image_artifact, self._cvd_host_package_artifact,
+ self._avd_spec.local_image_dir)
+ else:
+ try:
+ artifacts_path = tempfile.mkdtemp()
+ logger.debug("Extracted path of artifacts: %s", artifacts_path)
+ self._DownloadArtifacts(artifacts_path)
+ self._UploadArtifacts(
+ None,
+ os.path.join(artifacts_path, constants.CVD_HOST_PACKAGE),
+ artifacts_path)
+ finally:
+ shutil.rmtree(artifacts_path)
def _ProcessArtifacts(self, image_source):
"""Process artifacts.
diff --git a/public/actions/remote_instance_cf_device_factory_test.py b/public/actions/remote_instance_cf_device_factory_test.py
index 448b93f..e680723 100644
--- a/public/actions/remote_instance_cf_device_factory_test.py
+++ b/public/actions/remote_instance_cf_device_factory_test.py
@@ -15,6 +15,8 @@
import glob
import os
+import shutil
+import tempfile
import unittest
import uuid
@@ -22,6 +24,7 @@
from acloud.create import avd_spec
from acloud.internal import constants
+from acloud.create import create_common
from acloud.internal.lib import android_build_client
from acloud.internal.lib import auth
from acloud.internal.lib import cvd_compute_client_multi_stage
@@ -328,6 +331,82 @@
self.assertEqual(mock_upload.call_count, 1)
self.assertEqual(mock_launchcvd.call_count, 1)
+ # pylint: disable=no-member
+ @mock.patch.object(create_common, "DownloadRemoteArtifact")
+ def testDownloadArtifacts(self, mock_download):
+ """Test process remote cuttlefish image."""
+ extract_path = "/tmp/1111/"
+ fake_remote_image = {"build_target" : "aosp_cf_x86_phone-userdebug",
+ "build_id": "1234"}
+ self.Patch(
+ cvd_compute_client_multi_stage,
+ "CvdComputeClient",
+ return_value=mock.MagicMock())
+ self.Patch(tempfile, "mkdtemp", return_value="/tmp/1111/")
+ self.Patch(shutil, "rmtree")
+ fake_avd_spec = mock.MagicMock()
+ fake_avd_spec.cfg = mock.MagicMock()
+ fake_avd_spec.remote_image = fake_remote_image
+ fake_avd_spec.image_download_dir = "/tmp"
+ self.Patch(os.path, "exists", return_value=False)
+ self.Patch(os, "makedirs")
+ factory = remote_instance_cf_device_factory.RemoteInstanceDeviceFactory(
+ fake_avd_spec)
+ factory._DownloadArtifacts(extract_path)
+ build_id = "1234"
+ build_target = "aosp_cf_x86_phone-userdebug"
+ checkfile1 = "aosp_cf_x86_phone-img-1234.zip"
+ checkfile2 = "cvd-host_package.tar.gz"
+
+ # To validate DownloadArtifact runs twice.
+ self.assertEqual(mock_download.call_count, 2)
+
+ # To validate DownloadArtifact arguments correct.
+ mock_download.assert_has_calls([
+ mock.call(fake_avd_spec.cfg, build_target, build_id, checkfile1,
+ extract_path, decompress=True),
+ mock.call(fake_avd_spec.cfg, build_target, build_id, checkfile2,
+ extract_path)], any_order=True)
+
+ @mock.patch.object(create_common, "DownloadRemoteArtifact")
+ @mock.patch.object(remote_instance_cf_device_factory.RemoteInstanceDeviceFactory,
+ "_UploadArtifacts")
+ def testProcessRemoteHostArtifacts(self, mock_upload, mock_download):
+ """Test process remote host artifacts."""
+ self.Patch(
+ cvd_compute_client_multi_stage,
+ "CvdComputeClient",
+ return_value=mock.MagicMock())
+ fake_avd_spec = mock.MagicMock()
+
+ # Test process remote host artifacts with local images.
+ fake_avd_spec.instance_type = constants.INSTANCE_TYPE_HOST
+ fake_avd_spec.image_source = constants.IMAGE_SRC_LOCAL
+ fake_avd_spec._instance_name_to_reuse = None
+ fake_host_package_name = "/fake/host_package.tar.gz"
+ fake_image_name = ""
+ factory = remote_instance_cf_device_factory.RemoteInstanceDeviceFactory(
+ fake_avd_spec,
+ fake_image_name,
+ fake_host_package_name)
+ factory._ProcessRemoteHostArtifacts()
+ self.assertEqual(mock_upload.call_count, 1)
+
+ # Test process remote host artifacts with remote images.
+ fake_tmp_folder = "/tmp/1111/"
+ mock_upload.call_count = 0
+ self.Patch(tempfile, "mkdtemp", return_value=fake_tmp_folder)
+ self.Patch(shutil, "rmtree")
+ fake_avd_spec.instance_type = constants.INSTANCE_TYPE_HOST
+ fake_avd_spec.image_source = constants.IMAGE_SRC_REMOTE
+ fake_avd_spec._instance_name_to_reuse = None
+ factory = remote_instance_cf_device_factory.RemoteInstanceDeviceFactory(
+ fake_avd_spec)
+ factory._ProcessRemoteHostArtifacts()
+ self.assertEqual(mock_upload.call_count, 1)
+ self.assertEqual(mock_download.call_count, 2)
+ shutil.rmtree.assert_called_once_with(fake_tmp_folder)
+
if __name__ == "__main__":
unittest.main()
diff --git a/public/config.py b/public/config.py
index b00ec57..a5d591d 100755
--- a/public/config.py
+++ b/public/config.py
@@ -63,6 +63,28 @@
_CONFIG_DATA_PATH = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "data")
_DEFAULT_CONFIG_FILE = "acloud.config"
+_DEFAULT_HW_PROPERTY = "cpu:2,resolution:720x1280,dpi:320,memory:4g"
+
+# VERSION
+_VERSION_FILE = "VERSION"
+_UNKNOWN = "UNKNOWN"
+_NUM_INSTANCES_ARG = "-num_instances"
+
+
+def GetVersion():
+ """Print the version of acloud.
+
+ The VERSION file is built into the acloud binary. The version file path is
+ under "public/data".
+
+ Returns:
+ String of the acloud version.
+ """
+ version_file_path = os.path.join(_CONFIG_DATA_PATH, _VERSION_FILE)
+ if os.path.exists(version_file_path):
+ with open(version_file_path) as version_file:
+ return version_file.read()
+ return _UNKNOWN
def GetDefaultConfigFile():
@@ -252,18 +274,32 @@
self.network = parsed_args.network
if parsed_args.multi_stage_launch is not None:
self.enable_multi_stage = parsed_args.multi_stage_launch
+ if (parsed_args.which == "create_cf" and
+ parsed_args.num_avds_per_instance > 1):
+ scrubbed_args = [arg for arg in self.launch_args.split()
+ if _NUM_INSTANCES_ARG not in arg]
+ scrubbed_args.append("%s=%d" % (_NUM_INSTANCES_ARG,
+ parsed_args.num_avds_per_instance))
- def OverrideHwPropertyWithFlavor(self, flavor):
- """Override hw configuration values with flavor name.
+ self.launch_args = " ".join(scrubbed_args)
- HwProperty will be overrided according to the change of flavor.
- If flavor is None, set hw configuration with phone(default flavor).
+ def OverrideHwProperty(self, flavor, instance_type=None):
+ """Override hw configuration values.
+
+ HwProperty will be overrided according to the change of flavor and
+ instance type. The format of key is flavor or instance_type-flavor.
+ e.g: 'phone' or 'local-phone'.
+ If the giving key is not found, set hw configuration with a default
+ phone property.
Args:
- flavor: string of flavor name.
+ flavor: String of flavor name.
+ instance_type: String of instance type.
"""
+ hw_key = ("%s-%s" % (instance_type, flavor)
+ if instance_type == constants.INSTANCE_TYPE_LOCAL else flavor)
self.hw_property = self.common_hw_property_map.get(
- flavor, constants.FLAVOR_PHONE)
+ hw_key, _DEFAULT_HW_PROPERTY)
def Verify(self):
"""Verify configuration fields."""
@@ -278,6 +314,10 @@
"invalid value: %d" % (self.precreated_data_image_map.keys(),
self.extra_data_disk_size_gb))
+ def SupportRemoteInstance(self):
+ """Return True if gcp project is provided in config."""
+ return True if self.project else False
+
class AcloudConfigManager(object):
"""A class that loads configurations."""
diff --git a/public/data/default.config b/public/data/default.config
index 71301ae..3763246 100644
--- a/public/data/default.config
+++ b/public/data/default.config
@@ -18,7 +18,7 @@
network: "default"
extra_data_disk_size_gb: 0
instance_name_pattern: "ins-{uuid}-{build_id}-{build_target}"
- fetch_cvd_version: "5941063"
+ fetch_cvd_version: "6170097"
metadata_variable {
key: "camera_front"
@@ -55,6 +55,26 @@
# AVD manager of android sdk.
# https://developer.android.com/studio/run/managing-avds
common_hw_property_map {
+ key: "local-phone"
+ value: "cpu:4,resolution:720x1280,dpi:320,memory:6g"
+}
+
+common_hw_property_map {
+ key: "local-auto"
+ value: "cpu:4,resolution:1280x800,dpi:160,memory:6g"
+}
+
+common_hw_property_map {
+ key: "local-wear"
+ value: "cpu:4,resolution:320x320,dpi:240,memory:2g"
+}
+
+common_hw_property_map {
+ key: "local-tablet"
+ value: "cpu:4,resolution:2560x1800,dpi:320,memory:6g"
+}
+
+common_hw_property_map {
key: "phone"
value: "cpu:2,resolution:720x1280,dpi:320,memory:4g"
}
diff --git a/public/report.py b/public/report.py
index bd81839..dd95c4e 100755
--- a/public/report.py
+++ b/public/report.py
@@ -59,6 +59,8 @@
import logging
import os
+from acloud.internal import constants
+
logger = logging.getLogger(__name__)
@@ -148,6 +150,43 @@
"requested to update to a status with lower severity %s, ignored.",
self.status, status)
+ def AddDevice(self, instance_name, ip_address, adb_port, vnc_port,
+ key="devices"):
+ """Add a record of a device.
+
+ Args:
+ instance_name: A string.
+ ip_address: A string.
+ adb_port: An integer.
+ vnc_port: An integer.
+ key: A string, the data entry where the record is added.
+ """
+ device = {constants.INSTANCE_NAME: instance_name}
+ if adb_port:
+ device[constants.ADB_PORT] = adb_port
+ device[constants.IP] = "%s:%d" % (ip_address, adb_port)
+ else:
+ device[constants.IP] = ip_address
+
+ if vnc_port:
+ device[constants.VNC_PORT] = vnc_port
+ self.AddData(key=key, value=device)
+
+ def AddDeviceBootFailure(self, instance_name, ip_address, adb_port,
+ vnc_port, error):
+ """Add a record of device boot failure.
+
+ Args:
+ instance_name: A string.
+ ip_address: A string.
+ adb_port: An integer.
+ vnc_port: An integer. Can be None if the device doesn't support it.
+ error: A string, the error message.
+ """
+ self.AddDevice(instance_name, ip_address, adb_port, vnc_port,
+ "devices_failing_boot")
+ self.AddError(error)
+
def Dump(self, report_file):
"""Dump report content to a file.
diff --git a/public/report_test.py b/public/report_test.py
index 1b9fd3f..d3987c8 100644
--- a/public/report_test.py
+++ b/public/report_test.py
@@ -60,6 +60,36 @@
test_report.SetStatus(report.Status.FAIL)
self.assertEqual(test_report.status, "BOOT_FAIL")
+ def testAddDevice(self):
+ """test AddDevice."""
+ test_report = report.Report("create")
+ test_report.AddDevice("instance_1", "127.0.0.1", 6520, 6444)
+ expected = {
+ "devices": [{
+ "instance_name": "instance_1",
+ "ip": "127.0.0.1:6520",
+ "adb_port": 6520,
+ "vnc_port": 6444
+ }]
+ }
+ self.assertEqual(test_report.data, expected)
+
+ def testAddDeviceBootFailure(self):
+ """test AddDeviceBootFailure."""
+ test_report = report.Report("create")
+ test_report.AddDeviceBootFailure("instance_1", "127.0.0.1", 6520, 6444,
+ "some errors")
+ expected = {
+ "devices_failing_boot": [{
+ "instance_name": "instance_1",
+ "ip": "127.0.0.1:6520",
+ "adb_port": 6520,
+ "vnc_port": 6444
+ }]
+ }
+ self.assertEqual(test_report.data, expected)
+ self.assertEqual(test_report.errors, ["some errors"])
+
if __name__ == "__main__":
unittest.main()
diff --git a/pull/pull.py b/pull/pull.py
index 5da45d5..8943278 100644
--- a/pull/pull.py
+++ b/pull/pull.py
@@ -34,8 +34,7 @@
logger = logging.getLogger(__name__)
-_REMOTE_LOG_FOLDER = "/home/%s/cuttlefish_runtime" % constants.GCE_USER
-_FIND_LOG_FILE_CMD = "find %s -type f" % _REMOTE_LOG_FOLDER
+_FIND_LOG_FILE_CMD = "find %s -type f" % constants.REMOTE_LOG_FOLDER
# Black list for log files.
_KERNEL = "kernel"
_IMG_FILE_EXTENSION = ".img"
@@ -143,7 +142,7 @@
"""
log_files = GetAllLogFilePaths(ssh)
if file_name:
- file_path = os.path.join(_REMOTE_LOG_FOLDER, file_name)
+ file_path = os.path.join(constants.REMOTE_LOG_FOLDER, file_name)
if file_path in log_files:
return [file_path]
raise errors.CheckPathError("Can't find this log file(%s) from remote "
@@ -157,7 +156,7 @@
return utils.GetAnswerFromList(log_files, enable_choose_all=True)
raise errors.CheckPathError("Can't find any log file in folder(%s) from "
- "remote instance." % _REMOTE_LOG_FOLDER)
+ "remote instance." % constants.REMOTE_LOG_FOLDER)
def GetAllLogFilePaths(ssh):
@@ -176,7 +175,7 @@
log_files = FilterLogfiles(files_output.splitlines())
except subprocess.CalledProcessError:
logger.debug("The folder(%s) that running launch_cvd doesn't exist.",
- _REMOTE_LOG_FOLDER)
+ constants.REMOTE_LOG_FOLDER)
return log_files
diff --git a/reconnect/reconnect.py b/reconnect/reconnect.py
index 1be9567..0760935 100644
--- a/reconnect/reconnect.py
+++ b/reconnect/reconnect.py
@@ -19,14 +19,16 @@
- restart vnc for remote/local instances
"""
+import os
import re
from acloud import errors
-from acloud.delete import delete
from acloud.internal import constants
from acloud.internal.lib import auth
from acloud.internal.lib import android_compute_client
+from acloud.internal.lib import cvd_runtime_config
from acloud.internal.lib import utils
+from acloud.internal.lib import ssh as ssh_object
from acloud.internal.lib.adb_tools import AdbTools
from acloud.list import list as list_instance
from acloud.public import config
@@ -37,6 +39,28 @@
_VNC_STARTED_PATTERN = "ssvnc vnc://127.0.0.1:%(vnc_port)d"
+def IsWebrtcEnable(ip_addr, host_user, host_ssh_private_key_path,
+ extra_args_ssh_tunnel):
+ """Check remote instance webRTC is enable.
+
+ Args:
+ ip_addr: String, use to connect to webrtc AVD on the instance.
+ host_user: String of user login into the instance.
+ host_ssh_private_key_path: String of host key for logging in to the
+ host.
+ extra_args_ssh_tunnel: String, extra args for ssh tunnel connection.
+ """
+ ssh = ssh_object.Ssh(ip=ssh_object.IP(ip=ip_addr), user=host_user,
+ ssh_private_key_path=host_ssh_private_key_path,
+ extra_args_ssh_tunnel=extra_args_ssh_tunnel)
+ remote_cuttlefish_config = os.path.join(constants.REMOTE_LOG_FOLDER,
+ constants.CUTTLEFISH_CONFIG_FILE)
+ raw_data = ssh.GetCmdOutput("cat " + remote_cuttlefish_config)
+ cf_runtime_cfg = cvd_runtime_config.CvdRuntimeConfig(
+ raw_data=raw_data.strip())
+ return cf_runtime_cfg.enable_webrtc
+
+
def StartVnc(vnc_port, display):
"""Start vnc connect to AVD.
@@ -51,7 +75,7 @@
vnc_started_pattern = _VNC_STARTED_PATTERN % {"vnc_port": vnc_port}
if not utils.IsCommandRunning(vnc_started_pattern):
#clean old disconnect ssvnc viewer.
- delete.CleanupSSVncviewer(vnc_port)
+ utils.CleanupSSVncviewer(vnc_port)
match = _RE_DISPLAY.match(display)
if match:
@@ -110,9 +134,9 @@
"unknown avd type: %s" %
(instance.name, instance.avd_type))
- adb_cmd = AdbTools(instance.forwarding_adb_port)
- vnc_port = instance.forwarding_vnc_port
- adb_port = instance.forwarding_adb_port
+ adb_cmd = AdbTools(instance.adb_port)
+ vnc_port = instance.vnc_port
+ adb_port = instance.adb_port
# ssh tunnel is up but device is disconnected on adb
if instance.ssh_tunnel_is_connected and not adb_cmd.IsAdbConnectionAlive():
adb_cmd.DisconnectAdb()
@@ -130,7 +154,18 @@
vnc_port = forwarded_ports.vnc_port
adb_port = forwarded_ports.adb_port
- if vnc_port and connect_vnc:
+ if IsWebrtcEnable(instance.ip,
+ constants.GCE_USER,
+ ssh_private_key_path,
+ extra_args_ssh_tunnel):
+ utils.EstablishWebRTCSshTunnel(
+ ip_addr=instance.ip,
+ rsa_key_file=ssh_private_key_path,
+ ssh_user=constants.GCE_USER,
+ extra_args_ssh_tunnel=extra_args_ssh_tunnel)
+ utils.LaunchBrowser(constants.WEBRTC_LOCAL_HOST,
+ constants.WEBRTC_LOCAL_PORT)
+ elif(vnc_port and connect_vnc):
StartVnc(vnc_port, instance.display)
device_dict = {
diff --git a/reconnect/reconnect_test.py b/reconnect/reconnect_test.py
index 493067b..960ba43 100644
--- a/reconnect/reconnect_test.py
+++ b/reconnect/reconnect_test.py
@@ -42,7 +42,7 @@
instance_object = mock.MagicMock()
instance_object.ip = "1.1.1.1"
instance_object.islocal = False
- instance_object.forwarding_adb_port = "8686"
+ instance_object.adb_port = "8686"
instance_object.avd_type = "cuttlefish"
self.Patch(subprocess, "check_call", return_value=True)
self.Patch(utils, "LaunchVncClient")
@@ -50,9 +50,10 @@
self.Patch(AdbTools, "IsAdbConnected", return_value=False)
self.Patch(AdbTools, "IsAdbConnectionAlive", return_value=False)
self.Patch(utils, "IsCommandRunning", return_value=False)
+ self.Patch(reconnect, "IsWebrtcEnable", return_value=False)
#test ssh tunnel not connected, remote instance.
- instance_object.forwarding_vnc_port = 6666
+ instance_object.vnc_port = 6666
instance_object.display = ""
utils.AutoConnect.call_count = 0
reconnect.ReconnectInstance(ssh_private_key_path, instance_object, fake_report)
@@ -69,7 +70,7 @@
instance_object.ssh_tunnel_is_connected = False
instance_object.display = ""
utils.AutoConnect.call_count = 0
- instance_object.forwarding_vnc_port = 5555
+ instance_object.vnc_port = 5555
extra_args_ssh_tunnel = None
self.Patch(utils, "AutoConnect",
return_value=ForwardedPorts(vnc_port=11111, adb_port=22222))
@@ -100,7 +101,7 @@
#test reconnect local instance.
instance_object.islocal = True
instance_object.display = ""
- instance_object.forwarding_vnc_port = 5555
+ instance_object.vnc_port = 5555
instance_object.ssh_tunnel_is_connected = False
utils.AutoConnect.call_count = 0
reconnect.ReconnectInstance(ssh_private_key_path,
@@ -115,12 +116,13 @@
fake_report = mock.MagicMock()
instance_object = mock.MagicMock()
instance_object.ip = "1.1.1.1"
- instance_object.forwarding_vnc_port = 9999
- instance_object.forwarding_adb_port = "9999"
+ instance_object.vnc_port = 9999
+ instance_object.adb_port = "9999"
instance_object.islocal = False
instance_object.ssh_tunnel_is_connected = False
self.Patch(utils, "AutoConnect")
self.Patch(reconnect, "StartVnc")
+ self.Patch(reconnect, "IsWebrtcEnable", return_value=False)
#test reconnect remote instance when avd_type as gce.
instance_object.avd_type = "gce"
reconnect.ReconnectInstance(ssh_private_key_path, instance_object, fake_report)
diff --git a/setup/gcp_setup_runner.py b/setup/gcp_setup_runner.py
index 14619c4..2d12ccc 100644
--- a/setup/gcp_setup_runner.py
+++ b/setup/gcp_setup_runner.py
@@ -55,6 +55,7 @@
_DEFAULT_SSH_KEY)
_DEFAULT_SSH_PUBLIC_KEY = os.path.join(_DEFAULT_SSH_FOLDER,
_DEFAULT_SSH_KEY + ".pub")
+_ENV_CLOUDSDK_PYTHON = "CLOUDSDK_PYTHON"
_GCLOUD_COMPONENT_ALPHA = "alpha"
# Regular expression to get project/zone information.
_PROJECT_RE = re.compile(r"^project = (?P<project>.+)")
@@ -150,6 +151,9 @@
"""
self.gcloud_command_path = os.path.join(google_sdk_folder, "gcloud")
self.gsutil_command_path = os.path.join(google_sdk_folder, "gsutil")
+ # TODO(137195528): Remove python2 environment after acloud support python3.
+ self._env = os.environ.copy()
+ self._env[_ENV_CLOUDSDK_PYTHON] = "python2"
def RunGcloud(self, cmd, **kwargs):
"""Run gcloud command.
@@ -162,7 +166,8 @@
Returns:
String, return message after execute gcloud command.
"""
- return subprocess.check_output([self.gcloud_command_path] + cmd, **kwargs)
+ return subprocess.check_output([self.gcloud_command_path] + cmd,
+ env=self._env, **kwargs)
def RunGsutil(self, cmd, **kwargs):
"""Run gsutil command.
@@ -175,7 +180,8 @@
Returns:
String, return message after execute gsutil command.
"""
- return subprocess.check_output([self.gsutil_command_path] + cmd, **kwargs)
+ return subprocess.check_output([self.gsutil_command_path] + cmd,
+ env=self._env, **kwargs)
class GoogleAPIService(object):
diff --git a/setup/gcp_setup_runner_test.py b/setup/gcp_setup_runner_test.py
index 7fc1fbf..cd81ab4 100644
--- a/setup/gcp_setup_runner_test.py
+++ b/setup/gcp_setup_runner_test.py
@@ -19,6 +19,7 @@
import os
import mock
import six
+from six import b
# pylint: disable=no-name-in-module,import-error,no-member
from acloud import errors
@@ -27,7 +28,7 @@
from acloud.public import config
from acloud.setup import gcp_setup_runner
-_GCP_USER_CONFIG = """
+_GCP_USER_CONFIG = b("""
[compute]
region = new_region
zone = new_zone
@@ -35,7 +36,7 @@
account = new@google.com
disable_usage_reporting = False
project = new_project
-"""
+""")
def _CreateCfgFile():
@@ -233,9 +234,11 @@
self.gcp_env_runner._EnableGcloudServices(self.gcloud_runner)
mock_run.assert_has_calls([
mock.call(["gcloud", "services", "enable",
- gcp_setup_runner._ANDROID_BUILD_SERVICE], stderr=-2),
+ gcp_setup_runner._ANDROID_BUILD_SERVICE],
+ env=self.gcloud_runner._env, stderr=-2),
mock.call(["gcloud", "services", "enable",
- gcp_setup_runner._COMPUTE_ENGINE_SERVICE], stderr=-2)])
+ gcp_setup_runner._COMPUTE_ENGINE_SERVICE],
+ env=self.gcloud_runner._env, stderr=-2)])
@mock.patch("subprocess.check_output")
def testGoogleAPIService(self, mock_run):
@@ -244,7 +247,8 @@
"error_message")
api_service.EnableService(self.gcloud_runner)
mock_run.assert_has_calls([
- mock.call(["gcloud", "services", "enable", "service_name"], stderr=-2)])
+ mock.call(["gcloud", "services", "enable", "service_name"],
+ env=self.gcloud_runner._env, stderr=-2)])
@mock.patch("subprocess.check_output")
def testCheckBillingEnable(self, mock_run):
@@ -253,8 +257,13 @@
mock_run.return_value = "billingEnabled: true"
self.gcp_env_runner._CheckBillingEnable(self.gcloud_runner)
mock_run.assert_has_calls([
- mock.call(["gcloud", "alpha", "billing", "projects", "describe",
- self.gcp_env_runner.project])])
+ mock.call(
+ [
+ "gcloud", "alpha", "billing", "projects", "describe",
+ self.gcp_env_runner.project
+ ],
+ env=self.gcloud_runner._env)
+ ])
# Test billing account in gcp project was not enabled.
mock_run.return_value = "billingEnabled: false"
diff --git a/setup/google_sdk.py b/setup/google_sdk.py
index 4867ca9..1343a3d 100644
--- a/setup/google_sdk.py
+++ b/setup/google_sdk.py
@@ -43,6 +43,7 @@
SDK_BIN_PATH = os.path.join("google-cloud-sdk", "bin")
GCLOUD_BIN = "gcloud"
+GCLOUD_COMPONENT_NOT_INSTALLED = "Not Installed"
GCP_SDK_VERSION = "209.0.0"
GCP_SDK_TOOLS_URL = "https://dl.google.com/dl/cloudsdk/channels/rapid/downloads"
LINUX_GCP_SDK_64_URL = "%s/google-cloud-sdk-%s-linux-x86_64.tar.gz" % (
@@ -126,7 +127,13 @@
gcloud_runner: A GcloudRunner class to run "gcloud" command.
component: String, name of gcloud component.
"""
- gcloud_runner.RunGcloud(["components", "install", "--quiet", component])
+ result = gcloud_runner.RunGcloud([
+ "components", "list", "--format", "get(state.name)", "--filter",
+ "ID=%s" % component
+ ])
+ if result.strip() == GCLOUD_COMPONENT_NOT_INSTALLED:
+ gcloud_runner.RunGcloud(
+ ["components", "install", "--quiet", component])
def GetSDKBinPath(self):
"""Get google SDK tools bin path.
diff --git a/setup/host_setup_runner.py b/setup/host_setup_runner.py
index a334c2c..9a668e5 100644
--- a/setup/host_setup_runner.py
+++ b/setup/host_setup_runner.py
@@ -23,7 +23,10 @@
import getpass
import logging
+import os
+import shutil
import sys
+import tempfile
from acloud.internal import constants
from acloud.internal.lib import utils
@@ -33,17 +36,20 @@
logger = logging.getLogger(__name__)
-# Install cuttlefish-common will probably not work now.
-# TODO: update this to pull from the proper repo.
-_AVD_REQUIRED_PKGS = ["cuttlefish-common",
- # TODO(b/117613492): This is all qemu related, take this
- # out once they are added back in as deps for
- # cuttlefish-common.
- "qemu-kvm", "qemu-system-common", "qemu-system-x86",
- "qemu-utils", "libvirt-clients", "libvirt-daemon-system"]
+# Packages "devscripts" and "equivs" are required for "mk-build-deps".
+_AVD_REQUIRED_PKGS = [
+ "devscripts", "equivs", "libvirt-clients", "libvirt-daemon-system"]
_BASE_REQUIRED_PKGS = ["ssvnc", "lzop"]
+_CUTTLEFISH_COMMOM_PKG = "cuttlefish-common"
+_CF_COMMOM_FOLDER = "cf-common"
_LIST_OF_MODULES = ["kvm_intel", "kvm"]
_UPDATE_APT_GET_CMD = "sudo apt-get update"
+_INSTALL_CUTTLEFISH_COMMOM_CMD = [
+ "git clone https://github.com/google/android-cuttlefish.git {git_folder}",
+ "cd {git_folder}",
+ "yes | sudo mk-build-deps -i -r -B",
+ "dpkg-buildpackage -uc -us",
+ "sudo apt-get install -y -f ../cuttlefish-common_*_amd64.deb"]
class BasePkgInstaller(base_task_runner.BaseTaskRunner):
@@ -109,6 +115,47 @@
PACKAGES = _BASE_REQUIRED_PKGS
+class CuttlefishCommonPkgInstaller(base_task_runner.BaseTaskRunner):
+ """Subtask base runner class for installing cuttlefish-common."""
+
+ WELCOME_MESSAGE_TITLE = "Install cuttlefish-common packages on the host"
+ WELCOME_MESSAGE = ("This step will walk you through the cuttlefish-common "
+ "packages installation for your host.")
+
+ def ShouldRun(self):
+ """Check if cuttlefish-common package is installed.
+
+ Returns:
+ Boolean, True if cuttlefish-common is not installed.
+ """
+ if not utils.IsSupportedPlatform():
+ return False
+
+ # Any required package is not installed or not up-to-date will need to
+ # run installation task.
+ if not setup_common.PackageInstalled(_CUTTLEFISH_COMMOM_PKG):
+ return True
+ return False
+
+ def _Run(self):
+ """Install cuttlefilsh-common packages."""
+ cf_common_path = os.path.join(tempfile.mkdtemp(), _CF_COMMOM_FOLDER)
+ logger.debug("cuttlefish-common path: %s", cf_common_path)
+ cmd = "\n".join(sub_cmd.format(git_folder=cf_common_path)
+ for sub_cmd in _INSTALL_CUTTLEFISH_COMMOM_CMD)
+
+ if not utils.GetUserAnswerYes("\nStart to install cuttlefish-common :\n%s"
+ "\nPress 'y' to continue or anything "
+ "else to do it myself and run acloud "
+ "again[y/N]: " % cmd):
+ sys.exit(constants.EXIT_BY_USER)
+ try:
+ setup_common.CheckCmdOutput(cmd, shell=True)
+ finally:
+ shutil.rmtree(os.path.dirname(cf_common_path))
+ logger.info("Cuttlefish-common package installed now.")
+
+
class CuttlefishHostSetup(base_task_runner.BaseTaskRunner):
"""Subtask class that setup host for cuttlefish."""
diff --git a/setup/host_setup_runner_test.py b/setup/host_setup_runner_test.py
index b424db5..e435be6 100644
--- a/setup/host_setup_runner_test.py
+++ b/setup/host_setup_runner_test.py
@@ -13,25 +13,30 @@
# limitations under the License.
"""Tests for host_setup_runner."""
import platform
+import shutil
+import tempfile
import unittest
+import mock
+from six import b
from acloud.internal.lib import driver_test_lib
from acloud.internal.lib import utils
from acloud.setup import setup_common
-from acloud.setup.host_setup_runner import CuttlefishHostSetup
from acloud.setup.host_setup_runner import AvdPkgInstaller
+from acloud.setup.host_setup_runner import CuttlefishCommonPkgInstaller
+from acloud.setup.host_setup_runner import CuttlefishHostSetup
class CuttlefishHostSetupTest(driver_test_lib.BaseDriverTest):
"""Test CuttlsfishHostSetup."""
- LSMOD_OUTPUT = """nvidia_modeset 860160 6 nvidia_drm
+ LSMOD_OUTPUT = b("""nvidia_modeset 860160 6 nvidia_drm
module1 12312 1
module2 12312 1
ghash_clmulni_intel 16384 0
aesni_intel 167936 3
aes_x86_64 20480 1 aesni_intel
-lrw 16384 1 aesni_intel"""
+lrw 16384 1 aesni_intel""")
# pylint: disable=invalid-name
def setUp(self):
@@ -85,9 +90,36 @@
def testShouldNotRun(self):
"""Test ShoudRun should raise error in non-linux os."""
self.Patch(platform, "system", return_value="Mac")
-
self.assertFalse(self.AvdPkgInstaller.ShouldRun())
+class CuttlefishCommonPkgInstallerTest(driver_test_lib.BaseDriverTest):
+ """Test CuttlefishCommonPkgInstallerTest."""
+
+ # pylint: disable=invalid-name
+ def setUp(self):
+ """Set up the test."""
+ super(CuttlefishCommonPkgInstallerTest, self).setUp()
+ self.CuttlefishCommonPkgInstaller = CuttlefishCommonPkgInstaller()
+
+ def testShouldRun(self):
+ """Test ShoudRun."""
+ self.Patch(platform, "system", return_value="Linux")
+ self.Patch(setup_common, "PackageInstalled", return_value=False)
+ self.assertTrue(self.CuttlefishCommonPkgInstaller.ShouldRun())
+
+ @mock.patch.object(shutil, "rmtree")
+ @mock.patch.object(setup_common, "CheckCmdOutput")
+ def testRun(self, mock_cmd, mock_rmtree):
+ """Test Run."""
+ fake_tmp_folder = "/tmp/cf-common"
+ self.Patch(tempfile, "mkdtemp", return_value=fake_tmp_folder)
+ self.Patch(utils, "GetUserAnswerYes", return_value="y")
+ self.Patch(CuttlefishCommonPkgInstaller, "ShouldRun", return_value=True)
+ self.CuttlefishCommonPkgInstaller.Run()
+ self.assertEqual(mock_cmd.call_count, 1)
+ mock_rmtree.assert_called_once_with(fake_tmp_folder)
+
+
if __name__ == "__main__":
unittest.main()
diff --git a/setup/setup.py b/setup/setup.py
index 36b6ffd..39513dc 100644
--- a/setup/setup.py
+++ b/setup/setup.py
@@ -48,6 +48,7 @@
# 2.Init all subtasks in queue and traverse them.
host_base_runner = host_setup_runner.HostBasePkgInstaller()
host_avd_runner = host_setup_runner.AvdPkgInstaller()
+ host_cf_common_runner = host_setup_runner.CuttlefishCommonPkgInstaller()
host_env_runner = host_setup_runner.CuttlefishHostSetup()
gcp_runner = gcp_setup_runner.GcpTaskRunner(args.config_file)
task_queue = []
@@ -55,6 +56,7 @@
if args.host:
task_queue.append(host_base_runner)
task_queue.append(host_avd_runner)
+ task_queue.append(host_cf_common_runner)
task_queue.append(host_env_runner)
# We should do these setup tasks if specified or if no args were used.
if args.host_base or (not args.host and not args.gcp_init):
diff --git a/setup/setup_common.py b/setup/setup_common.py
index 7ed9bde..f5ded57 100644
--- a/setup/setup_common.py
+++ b/setup/setup_common.py
@@ -132,11 +132,9 @@
installed_ver,
candidate_ver)
return False
- # installed package is old and we care about the version.
+ # TODO(148116924):Setup process should ask user to update package if the
+ # minimax required version is specified.
if compare_version and installed_ver != candidate_ver:
- logger.debug("Package %s version at %s, expected %s",
- pkg_name,
- installed_ver,
- candidate_ver)
- return False
+ logger.warning("Package %s version at %s, expected %s",
+ pkg_name, installed_ver, candidate_ver)
return True
diff --git a/setup/setup_common_test.py b/setup/setup_common_test.py
index d17d2dc..e17e529 100644
--- a/setup/setup_common_test.py
+++ b/setup/setup_common_test.py
@@ -14,6 +14,7 @@
"""Tests for host_setup_runner."""
import subprocess
import unittest
+from six import b
from acloud import errors
from acloud.internal.lib import driver_test_lib
@@ -22,21 +23,21 @@
class SetupCommonTest(driver_test_lib.BaseDriverTest):
"""Test HostPkgTaskRunner."""
- PKG_INFO_INSTALLED = """fake_pkg:
+ PKG_INFO_INSTALLED = b("""fake_pkg:
Installed: 0.7
Candidate: 0.7
Version table:
-"""
- PKG_INFO_NONE_INSTALL = """fake_pkg:
+""")
+ PKG_INFO_NONE_INSTALL = b("""fake_pkg:
Installed: (none)
Candidate: 0.7
Version table:
-"""
- PKG_INFO_OLD_VERSION = """fake_pkg:
+""")
+ PKG_INFO_OLD_VERSION = b("""fake_pkg:
Installed: 0.2
Candidate: 0.7
Version table:
-"""
+""")
# pylint: disable=invalid-name
def testPackageNotInstalled(self):
@@ -61,15 +62,15 @@
setup_common.PackageInstalled("fake_package")
# pylint: disable=invalid-name
- def testPackageInstalledFalseForOldVersion(self):
- """Test PackageInstalled should return False when pkg is out-of-date."""
+ def testPackageInstalledForOldVersion(self):
+ """Test PackageInstalled should return True when pkg is out-of-date."""
self.Patch(
setup_common,
"CheckCmdOutput",
return_value=self.PKG_INFO_OLD_VERSION)
- self.assertFalse(setup_common.PackageInstalled("fake_package",
- compare_version=True))
+ self.assertTrue(setup_common.PackageInstalled("fake_package",
+ compare_version=True))
def testPackageInstalled(self):
"""Test PackageInstalled should return True when pkg is installed."""