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/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