Create local instance with local image
- Prepare the launch_cvd cmd and check the required enviornment.
- Create local instance via launch_cvd cmd and wait for boot up.
- Launch vnc client after AVD boot up.
Bug: 111162251
Test: m acloud && atest acloud_test &&
acloud create --local_instance --local_image -vv
Change-Id: I73461b023c444c1ebb29818eef4475bbf86a4200
diff --git a/create/base_avd_create.py b/create/base_avd_create.py
index 19d8482..531fe66 100644
--- a/create/base_avd_create.py
+++ b/create/base_avd_create.py
@@ -66,7 +66,7 @@
"""
raise NotImplementedError
- def LaunchVncClient(self, port="6444"):
+ def LaunchVncClient(self, port=constants.VNC_PORT):
"""Launch ssvnc.
Args:
@@ -75,7 +75,9 @@
try:
os.environ[_ENV_DISPLAY]
except KeyError:
- logger.error("Remote terminal can't support VNC.")
+ utils.PrintColorString("Remote terminal can't support VNC. "
+ "Skipping VNC startup.",
+ utils.TextColors.FAIL)
return
if not find_executable(_VNC_BIN):
@@ -133,7 +135,7 @@
print("Creating %s AVD instance with the following details:" % avd_spec.instance_type)
if avd_spec.image_source == constants.IMAGE_SRC_LOCAL:
print("Image (local):")
- print(" %s" % avd_spec.local_image_path)
+ print(" %s" % avd_spec.local_image_dir)
elif avd_spec.image_source == constants.IMAGE_SRC_REMOTE:
print("Image:")
print(" %s - %s [%s]" % (avd_spec.remote_image[constants.BUILD_BRANCH],
diff --git a/create/create_common_test.py b/create/create_common_test.py
index d4c9aa1..bb7cf13 100644
--- a/create/create_common_test.py
+++ b/create/create_common_test.py
@@ -76,5 +76,6 @@
self.assertEqual(create_common.GetAnswerFromList(answer_list),
"image3.zip")
+
if __name__ == "__main__":
unittest.main()
diff --git a/create/local_image_local_instance.py b/create/local_image_local_instance.py
index 5c85185..816f2d2 100644
--- a/create/local_image_local_instance.py
+++ b/create/local_image_local_instance.py
@@ -19,18 +19,212 @@
local image.
"""
+from __future__ import print_function
+import logging
+import os
+import subprocess
+import time
+
+from acloud import errors
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.setup import host_setup_runner
+
+logger = logging.getLogger(__name__)
+
+_BOOT_COMPLETE = "VIRTUAL_DEVICE_BOOT_COMPLETED"
+_CMD_LAUNCH_CVD = "launch_cvd"
+# TODO(b/117366819): Currently --serial_number is not working.
+_CMD_LAUNCH_CVD_ARGS = (" --daemon --cpus %s --x_res %s --y_res %s --dpi %s "
+ "--memory_mb %s --blank_data_image_mb %s "
+ "--data_policy always_create "
+ "--system_image_dir %s "
+ "--vnc_server_port %s "
+ "--serial_number %s")
+_CMD_PGREP = "pgrep"
+_CMD_SG = "sg "
+_CMD_STOP_CVD = "stop_cvd"
+_CONFIRM_RELAUNCH = ("\nCuttlefish AVD is already running. \nPress 'y' to "
+ "terminate current instance and launch new instance \nor "
+ "anything else to exit out.")
+_CVD_SERIAL_PREFIX = "acloudCF"
+_ENV_ANDROID_HOST_OUT = "ANDROID_HOST_OUT"
class LocalImageLocalInstance(base_avd_create.BaseAVDCreate):
"""Create class for a local image local instance AVD."""
- # pylint: disable=no-self-use
def Create(self, avd_spec):
"""Create the AVD.
Args:
avd_spec: AVDSpec object that tells us what we're going to create.
"""
- print("We will create a local instance AVD with a local image: %s" %
- avd_spec)
+ self.PrintAvdDetails(avd_spec)
+ start = time.time()
+
+ local_image_path, launch_cvd_path = self.GetImageArtifactsPath(avd_spec)
+
+ cmd = self.PrepareLaunchCVDCmd(launch_cvd_path,
+ avd_spec.hw_property,
+ local_image_path,
+ avd_spec.flavor)
+ try:
+ self.CheckLaunchCVD(cmd)
+ except errors.LaunchCVDFail as launch_error:
+ raise launch_error
+
+ utils.PrintColorString("\n")
+ utils.PrintColorString("Total time: %ds" % (time.time() - start),
+ utils.TextColors.WARNING)
+ # TODO(b/117366819): Should display the correct device serial
+ # according to the args --serial_number.
+ utils.PrintColorString("Device serial: 127.0.0.1:6520",
+ utils.TextColors.WARNING)
+ if avd_spec.autoconnect:
+ self.LaunchVncClient()
+
+
+ @staticmethod
+ def GetImageArtifactsPath(avd_spec):
+ """Get image artifacts path.
+
+ This method will check if local image and launch_cvd are exist and
+ return the tuple path where they are located respectively.
+ For remote image, RemoteImageLocalInstance will override this method and
+ return the artifacts path which is extracted and downloaded from remote.
+
+ Args:
+ avd_spec: AVDSpec object that tells us what we're going to create.
+
+ Returns:
+ Tuple of (local image file, launch_cvd package) paths.
+ """
+ try:
+ # Check if local image is exist.
+ create_common.VerifyLocalImageArtifactsExist(
+ avd_spec.local_image_dir)
+
+ # TODO(b/117306227): help user to build out images and host package if
+ # anything needed is not found.
+ except errors.GetLocalImageError as imgerror:
+ logger.error(imgerror.message)
+ raise imgerror
+
+ # Check if launch_cvd is exist.
+ launch_cvd_path = os.path.join(
+ os.environ.get(_ENV_ANDROID_HOST_OUT), "bin", _CMD_LAUNCH_CVD)
+ if not os.path.exists(launch_cvd_path):
+ raise errors.GetCvdLocalHostPackageError(
+ "No launch_cvd found. Please run \"m launch_cvd\" first")
+
+ return avd_spec.local_image_dir, launch_cvd_path
+
+ @staticmethod
+ def PrepareLaunchCVDCmd(launch_cvd_path, hw_property, system_image_dir,
+ flavor):
+ """Prepare launch_cvd command.
+
+ Combine whole launch_cvd cmd including the hw property options and login
+ as the required groups if need. The reason using here-doc instead of
+ ampersand sign is all operations need to be ran in ths same pid.
+ The example of cmd:
+ $ sg kvm << EOF
+ sg libvirt
+ sg cvdnetwork
+ launch_cvd --cpus 2 --x_res 1280 --y_res 720 --dpi 160 --memory_mb 4096
+ EOF
+
+ Args:
+ launch_cvd_path: String of launch_cvd path.
+ hw_property: dict object of hw property.
+ system_image_dir: String of local images path.
+ flavor: String of flavor type.
+
+ Returns:
+ String, launch_cvd cmd.
+ """
+ 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"], hw_property["disk"],
+ system_image_dir, constants.VNC_PORT, _CVD_SERIAL_PREFIX+flavor)
+
+ combined_launch_cmd = ""
+ host_setup = host_setup_runner.CuttlefishHostSetup()
+ if not host_setup.CheckUserInGroups(constants.LIST_CF_USER_GROUPS):
+ # As part of local host setup to enable local instance support,
+ # the user is added to certain groups. For those settings to
+ # take effect systemwide requires the user to log out and
+ # log back in. In the scenario where the user has run setup and
+ # hasn't logged out, we still want them to be able to launch a
+ # local instance so add the user to the groups as part of the
+ # command to ensure success.
+ logger.debug("User group is not ready for cuttlefish")
+ for idx, group in enumerate(constants.LIST_CF_USER_GROUPS):
+ combined_launch_cmd += _CMD_SG + group
+ if idx == 0:
+ combined_launch_cmd += " <<EOF\n"
+ else:
+ combined_launch_cmd += "\n"
+ launch_cvd_w_args += "\nEOF"
+
+ combined_launch_cmd += launch_cvd_w_args
+ logger.debug("launch_cvd cmd:\n %s", combined_launch_cmd)
+ return combined_launch_cmd
+
+ def CheckLaunchCVD(self, cmd):
+ """Execute launch_cvd command and wait for boot up completed.
+
+ Args:
+ cmd: String, launch_cvd command.
+ """
+ start = time.time()
+
+ # Cuttlefish support launch single AVD at one time currently.
+ if self._IsLaunchCVDInUse():
+ logger.info("Cuttlefish AVD is already running.")
+ if utils.GetUserAnswerYes(_CONFIRM_RELAUNCH):
+ stop_cvd_cmd = os.path.join(os.environ.get(_ENV_ANDROID_HOST_OUT),
+ "bin", _CMD_STOP_CVD)
+ subprocess.check_output(stop_cvd_cmd)
+ else:
+ print("Only 1 cuttlefish AVD at a time, "
+ "please stop the current AVD via #acloud delete")
+ return
+
+ utils.PrintColorString("Waiting for AVD to boot... ",
+ utils.TextColors.WARNING, end="")
+
+ process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT)
+
+ boot_complete = False
+ for line in iter(process.stdout.readline, b''):
+ logger.debug(line.strip())
+ # cvd is still running and got boot complete.
+ if _BOOT_COMPLETE in line:
+ utils.PrintColorString("OK! (%ds)" % (time.time() - start),
+ utils.TextColors.OKGREEN)
+ boot_complete = True
+ break
+
+ if not boot_complete:
+ utils.PrintColorString("Fail!", utils.TextColors.WARNING)
+ raise errors.LaunchCVDFail(
+ "Can't launch cuttlefish AVD. No %s found" % _BOOT_COMPLETE)
+
+ @staticmethod
+ def _IsLaunchCVDInUse():
+ """Check if launch_cvd is running.
+
+ Returns:
+ Boolean, True if launch_cvd is running. False otherwise.
+ """
+ try:
+ subprocess.check_output([_CMD_PGREP, _CMD_LAUNCH_CVD])
+ return True
+ except subprocess.CalledProcessError:
+ # launch_cvd process is not in use.
+ return False
diff --git a/create/local_image_local_instance_test.py b/create/local_image_local_instance_test.py
new file mode 100644
index 0000000..4544f6c
--- /dev/null
+++ b/create/local_image_local_instance_test.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python
+#
+# Copyright 2018 - The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests for LocalImageLocalInstance."""
+
+import unittest
+import mock
+
+from acloud.create import local_image_local_instance
+from acloud.internal import constants
+from acloud.setup import host_setup_runner
+
+
+class LocalImageLocalInstanceTest(unittest.TestCase):
+ """Test LocalImageLocalInstance method."""
+
+ LAUNCH_CVD_CMD = """sg group1 <<EOF
+sg group2
+launch_cvd --daemon --cpus fake --x_res fake --y_res fake --dpi fake --memory_mb fake --blank_data_image_mb fake --data_policy always_create --system_image_dir fake_image_dir --vnc_server_port 6444 --serial_number acloudCFflavor
+EOF"""
+
+ def setUp(self):
+ """Initialize new LocalImageLocalInstance."""
+ self.local_image_local_instance = local_image_local_instance.LocalImageLocalInstance()
+
+ # pylint: disable=protected-access
+ @mock.patch.object(host_setup_runner.CuttlefishHostSetup, "CheckUserInGroups")
+ def testPrepareLaunchCVDCmd(self, mock_usergroups):
+ """test PrepareLaunchCVDCmd."""
+ mock_usergroups.return_value = False
+ hw_property = {"cpu": "fake", "x_res": "fake", "y_res": "fake",
+ "dpi":"fake", "memory": "fake", "disk": "fake"}
+ constants.LIST_CF_USER_GROUPS = ["group1", "group2"]
+
+ launch_cmd = self.local_image_local_instance.PrepareLaunchCVDCmd(
+ local_image_local_instance._CMD_LAUNCH_CVD,
+ hw_property, "fake_image_dir", "flavor")
+
+ self.assertEqual(launch_cmd, self.LAUNCH_CVD_CMD)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/create/local_image_remote_instance.py b/create/local_image_remote_instance.py
index 8bc33ad..eadc2c8 100644
--- a/create/local_image_remote_instance.py
+++ b/create/local_image_remote_instance.py
@@ -25,11 +25,11 @@
from acloud import errors
from acloud.create import create_common
from acloud.create import base_avd_create
+from acloud.internal import constants
logger = logging.getLogger(__name__)
-CVD_HOST_PACKAGE = "cvd-host_package.tar.gz"
-ENV_ANDROID_BUILD_TOP = "ANDROID_BUILD_TOP"
+_CVD_HOST_PACKAGE = "cvd-host_package.tar.gz"
class LocalImageRemoteInstance(base_avd_create.BaseAVDCreate):
@@ -66,7 +66,7 @@
A string, the path to the host package.
"""
dist_dir = os.path.join(
- os.environ.get(ENV_ANDROID_BUILD_TOP, "."), "out", "dist")
+ os.environ.get(constants.ENV_ANDROID_BUILD_TOP, "."), "out", "dist")
cvd_host_package_artifact = self.GetCvdHostPackage(
[local_image_dir, dist_dir])
logger.debug("cvd host package: %s", cvd_host_package_artifact)
@@ -86,11 +86,11 @@
errors.GetCvdLocalHostPackageError: Can't find cvd host package.
"""
for path in paths:
- cvd_host_package = os.path.join(path, CVD_HOST_PACKAGE)
+ cvd_host_package = os.path.join(path, _CVD_HOST_PACKAGE)
if os.path.exists(cvd_host_package):
return cvd_host_package
raise errors.GetCvdLocalHostPackageError, (
- "Can't find the cvd host package: \n%s." %
+ "Can't find the cvd host package: \n%s" %
'\n'.join(paths))
def Create(self, avd_spec):
diff --git a/create/local_image_remote_instance_test.py b/create/local_image_remote_instance_test.py
index 557dca1..52b864c 100644
--- a/create/local_image_remote_instance_test.py
+++ b/create/local_image_remote_instance_test.py
@@ -35,12 +35,12 @@
def testVerifyHostPackageArtifactsExist(self):
"""test verify host package artifacts exist."""
- #can't find the cvd host package
+ # Can't find the cvd host package
with mock.patch("os.path.exists") as exists:
exists.return_value = False
self.assertRaises(
- errors.GetCvdLocalHostPackageError, self.
- local_image_remote_instance.VerifyHostPackageArtifactsExist,
+ errors.GetCvdLocalHostPackageError,
+ self.local_image_remote_instance.VerifyHostPackageArtifactsExist,
"/fake_dirs")
@mock.patch("glob.glob")
diff --git a/errors.py b/errors.py
index 600d92d..6a5cb54 100644
--- a/errors.py
+++ b/errors.py
@@ -98,3 +98,7 @@
class UnsupportedCompressionFileType(SetupError):
"""Don't support the compression file type."""
+
+
+class LaunchCVDFail(CreateError):
+ """Cuttlefish AVD launch failed."""
diff --git a/internal/constants.py b/internal/constants.py
index e64c6ba..d3479ac 100755
--- a/internal/constants.py
+++ b/internal/constants.py
@@ -86,3 +86,10 @@
HW_Y_RES = "y_res"
USER_ANSWER_YES = {"y", "yes", "Y"}
+
+# Cuttlefish groups
+LIST_CF_USER_GROUPS = ["kvm", "libvirt", "cvdnetwork"]
+
+VNC_PORT = "6444"
+
+ENV_ANDROID_BUILD_TOP = "ANDROID_BUILD_TOP"
diff --git a/setup/host_setup_runner.py b/setup/host_setup_runner.py
index 7869cae..d5eaeb1 100644
--- a/setup/host_setup_runner.py
+++ b/setup/host_setup_runner.py
@@ -39,7 +39,6 @@
_AVD_REQUIRED_PKGS = ["cuttlefish-common", "ssvnc"]
# dict of supported system and their distributions.
_SUPPORTED_SYSTEMS_AND_DISTS = {"Linux": ["Ubuntu", "Debian"]}
-_LIST_OF_GROUPS = ["kvm", "libvirt", "cvdnetwork"]
_LIST_OF_MODULES = ["kvm_intel", "kvm"]
@@ -121,11 +120,11 @@
if not _IsSupportedPlatform():
return False
- return not (self._CheckUserInGroups(_LIST_OF_GROUPS)
+ return not (self.CheckUserInGroups(constants.LIST_CF_USER_GROUPS)
and self._CheckLoadedModules(_LIST_OF_MODULES))
@staticmethod
- def _CheckUserInGroups(group_name_list):
+ def CheckUserInGroups(group_name_list):
"""Check if the current user is in the group.
Args:
@@ -170,7 +169,7 @@
"sudo rmmod kvm",
"sudo modprobe kvm",
"sudo modprobe kvm_intel"]
- for group in _LIST_OF_GROUPS:
+ for group in constants.LIST_CF_USER_GROUPS:
setup_cmds.append("sudo usermod -aG %s % s" % (group, username))
print("Below commands will be run:")
diff --git a/setup/host_setup_runner_test.py b/setup/host_setup_runner_test.py
index fcb843a..4ec8290 100644
--- a/setup/host_setup_runner_test.py
+++ b/setup/host_setup_runner_test.py
@@ -43,7 +43,7 @@
def testShouldRunFalse(self):
"""Test ShouldRun returns False."""
- self.Patch(CuttlefishHostSetup, "_CheckUserInGroups", return_value=True)
+ self.Patch(CuttlefishHostSetup, "CheckUserInGroups", return_value=True)
self.Patch(CuttlefishHostSetup, "_CheckLoadedModules", return_value=True)
self.assertFalse(self.CuttlefishHostSetup.ShouldRun())
@@ -51,19 +51,19 @@
"""Test ShouldRun returns True."""
# 1. Checking groups fails.
self.Patch(
- CuttlefishHostSetup, "_CheckUserInGroups", return_value=False)
+ CuttlefishHostSetup, "CheckUserInGroups", return_value=False)
self.Patch(CuttlefishHostSetup, "_CheckLoadedModules", return_value=True)
self.assertTrue(self.CuttlefishHostSetup.ShouldRun())
# 2. Checking modules fails.
- self.Patch(CuttlefishHostSetup, "_CheckUserInGroups", return_value=True)
+ self.Patch(CuttlefishHostSetup, "CheckUserInGroups", return_value=True)
self.Patch(
CuttlefishHostSetup, "_CheckLoadedModules", return_value=False)
self.assertTrue(self.CuttlefishHostSetup.ShouldRun())
# pylint: disable=protected-access
def testCheckUserInGroups(self):
- """Test _CheckUserInGroups."""
+ """Test CheckUserInGroups."""
self.Patch(os, "getgroups", return_value=[1, 2, 3])
gr1 = mock.MagicMock()
gr1.gr_name = "fake_gr_1"
@@ -75,13 +75,13 @@
# User in all required groups should return true.
self.assertTrue(
- self.CuttlefishHostSetup._CheckUserInGroups(
+ self.CuttlefishHostSetup.CheckUserInGroups(
["fake_gr_1", "fake_gr_2"]))
# User not in all required groups should return False.
self.Patch(grp, "getgrgid", side_effect=[gr1, gr2, gr3])
self.assertFalse(
- self.CuttlefishHostSetup._CheckUserInGroups(
+ self.CuttlefishHostSetup.CheckUserInGroups(
["fake_gr_1", "fake_gr_4"]))
def testCheckLoadedModules(self):