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