acloud: Create instance and upload local image

- Using host image to create the instance.
- upload the cvd-host-package.tar.gz and local image to instance.
- lanuch the CVD in the instance.

Bug: 112878792
Test: make dist that generate the local image
acloud create --local_image
acloud create --local_image /tmp/image_dir that for specified path
atest acloud_test

Change-Id: Ifbd80fb10b05fd45b1477d68e2472ba0f5357c93
diff --git a/create/avd_spec.py b/create/avd_spec.py
index cce3be0..f062cca 100644
--- a/create/avd_spec.py
+++ b/create/avd_spec.py
@@ -77,6 +77,7 @@
         # Let's define the private class vars here and then process the user
         # args afterwards.
         self._autoconnect = None
+        self._report_internal_ip = None
         self._avd_type = None
         self._flavor = None
         self._image_source = None
@@ -214,6 +215,7 @@
             args: Namespace object from argparse.parse_args.
         """
         self._autoconnect = args.autoconnect
+        self._report_internal_ip = args.report_internal_ip
         self._avd_type = args.avd_type
         self._flavor = args.flavor
         self._instance_type = (constants.INSTANCE_TYPE_LOCAL
@@ -377,6 +379,11 @@
         return self._num_of_instances
 
     @property
+    def report_internal_ip(self):
+        """Return report internal ip."""
+        return self._report_internal_ip
+
+    @property
     def kernel_build_id(self):
         """Return kernel build id."""
         return self._kernel_build_id
diff --git a/create/create_common.py b/create/create_common.py
index 5744833..c5473ce 100644
--- a/create/create_common.py
+++ b/create/create_common.py
@@ -23,6 +23,7 @@
 import sys
 
 from acloud import errors
+from acloud.internal.lib import utils
 
 logger = logging.getLogger(__name__)
 
@@ -128,3 +129,31 @@
         image_path = images[0]
     logger.debug("Local image: %s ", image_path)
     return image_path
+
+
+def DisplayJobResult(report):
+    """Get job result from report.
+
+    -Display instance name/ip from report.data.
+        report.data example:
+            {'devices':[{'instance_name': 'ins-f6a34397-none-5043363',
+                         'ip': u'35.234.10.162'}]}
+    -Display error message from report.error.
+
+    Args:
+        report: A Report instance.
+    """
+    if report.data.get("devices"):
+        device_data = report.data.get("devices")
+        for device in device_data:
+            utils.PrintColorString("instance name: %s" %
+                                   device.get("instance_name"),
+                                   utils.TextColors.OKGREEN)
+            utils.PrintColorString("device IP: %s" % device.get("ip"),
+                                   utils.TextColors.OKGREEN)
+
+    # TODO(b/117245508): Help user to delete instance if it got created.
+    if report.errors:
+        error_msg = "\n".join(report.errors)
+        utils.PrintColorString("Fail in:\n%s\n" % error_msg,
+                               utils.TextColors.FAIL)
diff --git a/create/local_image_remote_instance.py b/create/local_image_remote_instance.py
index eadc2c8..af9d6ea 100644
--- a/create/local_image_remote_instance.py
+++ b/create/local_image_remote_instance.py
@@ -19,17 +19,150 @@
 local image.
 """
 
+from distutils.spawn import find_executable
+import getpass
 import logging
 import os
+import subprocess
 
 from acloud import errors
 from acloud.create import create_common
 from acloud.create import base_avd_create
 from acloud.internal import constants
+from acloud.internal.lib import auth
+from acloud.internal.lib import cvd_compute_client
+from acloud.internal.lib import utils
+from acloud.public.actions import base_device_factory
+from acloud.public.actions import common_operations
 
 logger = logging.getLogger(__name__)
 
+_ALL_SCOPES = [cvd_compute_client.CvdComputeClient.SCOPE]
 _CVD_HOST_PACKAGE = "cvd-host_package.tar.gz"
+_CVD_USER = getpass.getuser()
+_CMD_LAUNCH_CVD_ARGS = (" -cpus %s -x_res %s -y_res %s -dpi %s "
+                        "-memory_mb %s -blank_data_image_mb %s "
+                        "-data_policy always_create ")
+
+#Output to Serial port 1 (console) group in the instance
+_OUTPUT_CONSOLE_GROUPS = "tty"
+SSH_BIN = "ssh"
+_SSH_CMD = (" -i %(rsa_key_file)s "
+            "-q -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "
+            "-l %(login_user)s %(ip_addr)s ")
+
+class RemoteInstanceDeviceFactory(base_device_factory.BaseDeviceFactory):
+    """A class that can produce a cuttlefish device.
+
+    Attributes:
+        avd_spec: AVDSpec object that tells us what we're going to create.
+        cfg: An AcloudConfig instance.
+        image_path: A string, upload image artifact to instance.
+        cvd_host_package: A string, upload host package artifact to instance.
+        credentials: An oauth2client.OAuth2Credentials instance.
+        compute_client: An object of cvd_compute_client.CvdComputeClient.
+    """
+    def __init__(self, avd_spec, local_image_artifact, cvd_host_package_artifact):
+        """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._report_internal_ip = avd_spec.report_internal_ip
+        self.credentials = auth.CreateCredentials(avd_spec.cfg, _ALL_SCOPES)
+        compute_client = cvd_compute_client.CvdComputeClient(
+            avd_spec.cfg, self.credentials)
+        super(RemoteInstanceDeviceFactory, self).__init__(compute_client)
+        # Private creation parameters
+        self._ssh_cmd = None
+
+    def CreateInstance(self):
+        """Create a single configured cuttlefish device.
+
+        1. Create gcp instance.
+        2. setup the AVD env in the instance.
+        3. upload the artifacts to instance.
+        4. Launch CVD.
+
+        Returns:
+            A string, representing instance name.
+        """
+        instance = self._CreateGceInstance()
+        self._SetAVDenv(_CVD_USER)
+        self._UploadArtifacts(_CVD_USER,
+                              self._local_image_artifact,
+                              self._cvd_host_package_artifact)
+        self._LaunchCvd(_CVD_USER, self._avd_spec.hw_property)
+        return instance
+
+    def _CreateGceInstance(self):
+        """Create a single configured cuttlefish device.
+
+        Override method from parent class.
+
+        Returns:
+            A string, representing instance name.
+        """
+        #TODO(117487673): Grab the build target name from the image name.
+        instance = self._compute_client.GenerateInstanceName(
+            build_target=self._avd_spec.flavor, build_id="local")
+        # Create an instance from Stable Host Image
+        self._compute_client.CreateInstance(
+            instance=instance,
+            image_name=self._cfg.stable_host_image_name,
+            image_project=self._cfg.stable_host_image_project,
+            blank_data_disk_size_gb=self._cfg.extra_data_disk_size_gb,
+            avd_spec=self._avd_spec)
+        ip = self._compute_client.GetInstanceIP(instance)
+        self._ssh_cmd = find_executable(SSH_BIN) + _SSH_CMD % {
+            "login_user": getpass.getuser(),
+            "rsa_key_file": self._cfg.ssh_private_key_path,
+            "ip_addr": (ip.internal if self._report_internal_ip
+                        else ip.external)}
+        return instance
+
+    @utils.TimeExecute(function_description="Setup GCE environment")
+    def _SetAVDenv(self, cvd_user):
+        """set the user to run AVD in the instance."""
+        avd_list_of_groups = []
+        avd_list_of_groups.extend(constants.LIST_CF_USER_GROUPS)
+        avd_list_of_groups.append(_OUTPUT_CONSOLE_GROUPS)
+        for group in avd_list_of_groups:
+            remote_cmd = "\"sudo usermod -aG %s %s\"" %(group, cvd_user)
+            logger.debug("remote_cmd:\n %s", remote_cmd)
+            subprocess.check_call(self._ssh_cmd + remote_cmd, shell=True)
+
+    @utils.TimeExecute(function_description="Uploading local image")
+    def _UploadArtifacts(self,
+                         cvd_user,
+                         local_image_artifact,
+                         cvd_host_package_artifact):
+        """Upload local image and avd local host package to instance."""
+        # local image
+        remote_cmd = ("\"sudo su -c '/usr/bin/install_zip.sh .' - '%s'\" < %s" %
+                      (cvd_user, local_image_artifact))
+        logger.debug("remote_cmd:\n %s", remote_cmd)
+        subprocess.check_call(self._ssh_cmd + remote_cmd, shell=True)
+
+        # host_package
+        remote_cmd = ("\"sudo su -c 'tar -x -z -f -' - '%s'\" < %s" %
+                      (cvd_user, cvd_host_package_artifact))
+        logger.debug("remote_cmd:\n %s", remote_cmd)
+        subprocess.check_call(self._ssh_cmd + remote_cmd, shell=True)
+
+    def _LaunchCvd(self, cvd_user, hw_property):
+        """Launch CVD."""
+        lunch_cvd_args = _CMD_LAUNCH_CVD_ARGS % (
+            hw_property["cpu"],
+            hw_property["x_res"],
+            hw_property["y_res"],
+            hw_property["dpi"],
+            hw_property["memory"],
+            hw_property["disk"])
+        remote_cmd = ("\"sudo su -c 'bin/launch_cvd %s>&/dev/ttyS0&' - '%s'\"" %
+                      (lunch_cvd_args, cvd_user))
+        logger.debug("remote_cmd:\n %s", remote_cmd)
+        subprocess.Popen(self._ssh_cmd + remote_cmd, shell=True)
 
 
 class LocalImageRemoteInstance(base_avd_create.BaseAVDCreate):
@@ -93,12 +226,22 @@
             "Can't find the cvd host package: \n%s" %
             '\n'.join(paths))
 
+    @utils.TimeExecute(function_description="Total time: ",
+                       print_before_call=False, print_status=False)
     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 remote instance AVD with a local image: %s" %
-              avd_spec)
+        self.PrintAvdDetails(avd_spec)
         self.VerifyArtifactsExist(avd_spec.local_image_dir)
+        device_factory = RemoteInstanceDeviceFactory(
+            avd_spec,
+            self.local_image_artifact,
+            self.cvd_host_package_artifact)
+        report = common_operations.CreateDevices("create_cf", avd_spec.cfg,
+                                                 device_factory, avd_spec.num,
+                                                 avd_spec.report_internal_ip)
+        create_common.DisplayJobResult(report)
+        return report
diff --git a/create/remote_image_remote_instance.py b/create/remote_image_remote_instance.py
index f50ad79..0937acd 100644
--- a/create/remote_image_remote_instance.py
+++ b/create/remote_image_remote_instance.py
@@ -19,9 +19,8 @@
 remote image.
 """
 
-import time
-
 from acloud.create import base_avd_create
+from acloud.create import create_common
 from acloud.internal.lib import utils
 from acloud.public.actions import create_cuttlefish_action
 
@@ -29,6 +28,8 @@
 class RemoteImageRemoteInstance(base_avd_create.BaseAVDCreate):
     """Create class for a remote image remote instance AVD."""
 
+    @utils.TimeExecute(function_description="Total time: ",
+                       print_before_call=False, print_status=False)
     def Create(self, avd_spec):
         """Create the AVD.
 
@@ -39,38 +40,6 @@
             A Report instance.
         """
         self.PrintAvdDetails(avd_spec)
-        start = time.time()
         report = create_cuttlefish_action.CreateDevices(avd_spec=avd_spec)
-        utils.PrintColorString("\n")
-        utils.PrintColorString("Total time: %ds" % (time.time() - start),
-                               utils.TextColors.WARNING)
-        self.DisplayJobResult(report)
+        create_common.DisplayJobResult(report)
         return report
-
-    @staticmethod
-    def DisplayJobResult(report):
-        """Get job result from report.
-
-        -Display instance name/ip from report.data.
-            report.data example:
-                {'devices':[{'instance_name': 'ins-f6a34397-none-5043363',
-                             'ip': u'35.234.10.162'}]}
-        -Display error message from report.error.
-
-        Args:
-            report: A Report instance.
-        """
-        if report.data.get("devices"):
-            device_data = report.data.get("devices")
-            for device in device_data:
-                utils.PrintColorString("instance name: %s" %
-                                       device.get("instance_name"),
-                                       utils.TextColors.OKGREEN)
-                utils.PrintColorString("device IP: %s" % device.get("ip"),
-                                       utils.TextColors.OKGREEN)
-
-        # TODO(b/117245508): Help user to delete instance if it got created.
-        if report.errors:
-            error_msg = "\n".join(report.errors)
-            utils.PrintColorString("Fail in:\n%s\n" % error_msg,
-                                   utils.TextColors.FAIL)
diff --git a/internal/lib/cvd_compute_client.py b/internal/lib/cvd_compute_client.py
index 8c17521..fffd859 100644
--- a/internal/lib/cvd_compute_client.py
+++ b/internal/lib/cvd_compute_client.py
@@ -13,7 +13,6 @@
 # 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.
-
 """A client that manages Cuttlefish Virtual Device on compute engine.
 
 ** CvdComputeClient **
@@ -44,6 +43,7 @@
 from acloud.internal import constants
 from acloud.internal.lib import android_compute_client
 from acloud.internal.lib import gcompute_client
+from acloud.internal.lib import utils
 
 logger = logging.getLogger(__name__)
 
@@ -57,10 +57,11 @@
     # args, this method differs too and holds way too cf-specific args to put in
     # the parent method.
     # pylint: disable=arguments-differ,too-many-locals
-    def CreateInstance(self, instance, image_name, image_project, build_target,
-                       branch, build_id, kernel_branch=None,
-                       kernel_build_id=None, blank_data_disk_size_gb=None,
-                       avd_spec=None):
+    @utils.TimeExecute(function_description="Creating GCE instance")
+    def CreateInstance(self, instance, image_name, image_project,
+                       build_target=None, branch=None, build_id=None,
+                       kernel_branch=None, kernel_build_id=None,
+                       blank_data_disk_size_gb=None, avd_spec=None):
         """Create a cuttlefish instance given stable host image and build id.
 
         Args:
@@ -101,7 +102,9 @@
         if kernel_branch and kernel_build_id:
             metadata["cvd_01_fetch_kernel_bid"] = "{branch}/{build_id}".format(
                 branch=kernel_branch, build_id=kernel_build_id)
-        metadata["cvd_01_launch"] = "1"
+        metadata["cvd_01_launch"] = "0" if (
+            avd_spec
+            and avd_spec.image_source == constants.IMAGE_SRC_LOCAL) else "1"
         metadata["cvd_01_x_res"] = resolution[0]
         metadata["cvd_01_y_res"] = resolution[1]
         if blank_data_disk_size_gb > 0:
diff --git a/internal/lib/cvd_compute_client_test.py b/internal/lib/cvd_compute_client_test.py
index 96476be..c5afdc3 100644
--- a/internal/lib/cvd_compute_client_test.py
+++ b/internal/lib/cvd_compute_client_test.py
@@ -19,6 +19,8 @@
 import unittest
 import mock
 
+from acloud.create import avd_spec
+from acloud.internal import constants
 from acloud.internal.lib import cvd_compute_client
 from acloud.internal.lib import driver_test_lib
 from acloud.internal.lib import gcompute_client
@@ -115,6 +117,35 @@
             network=self.NETWORK,
             zone=self.ZONE)
 
+        #test use local image in the remote instance.
+        args = mock.MagicMock()
+        args.local_image = "/tmp/path"
+        args.config_file = ""
+        args.avd_type = "cf"
+        args.flavor = "phone"
+        fake_avd_spec = avd_spec.AVDSpec(args)
+        fake_avd_spec.hw_property[constants.HW_X_RES] = str(self.X_RES)
+        fake_avd_spec.hw_property[constants.HW_Y_RES] = str(self.Y_RES)
+        fake_avd_spec.hw_property[constants.HW_ALIAS_DPI] = str(self.DPI)
+        fake_avd_spec.hw_property[constants.HW_ALIAS_DISK] = str(
+            self.EXTRA_DATA_DISK_SIZE_GB * 1024)
+        expected_metadata["cvd_01_launch"] = "0"
+        expected_metadata["avd_type"] = "cf"
+        expected_metadata["flavor"] = "phone"
+        self.cvd_compute_client.CreateInstance(
+            self.INSTANCE, self.IMAGE, self.IMAGE_PROJECT, self.TARGET, self.BRANCH,
+            self.BUILD_ID, self.KERNEL_BRANCH, self.KERNEL_BUILD_ID,
+            self.EXTRA_DATA_DISK_SIZE_GB, fake_avd_spec)
 
+        mock_create.assert_called_with(
+            self.cvd_compute_client,
+            instance=self.INSTANCE,
+            image_name=self.IMAGE,
+            image_project=self.IMAGE_PROJECT,
+            disk_args=expected_disk_args,
+            metadata=expected_metadata,
+            machine_type=self.MACHINE_TYPE,
+            network=self.NETWORK,
+            zone=self.ZONE)
 if __name__ == "__main__":
     unittest.main()
diff --git a/internal/lib/utils.py b/internal/lib/utils.py
index ae334ac..241c3c8 100755
--- a/internal/lib/utils.py
+++ b/internal/lib/utils.py
@@ -575,3 +575,53 @@
             exception is an instance of DriverError or None if no error.
         """
         return self._final_results
+
+
+class TimeExecute(object):
+    """Count the function execute time."""
+
+    def __init__(self, function_description=None, print_before_call=True, print_status=True):
+        """Initializes the class.
+
+        Args:
+            function_description: String that describes function (e.g."Creating
+                                  Instance...")
+            print_before_call: Boolean, print the function description before
+                               calling the function, default True.
+            print_status: Boolean, print the status of the function after the
+                          function has completed, default True ("OK" or "Fail").
+        """
+        self._function_description = function_description
+        self._print_before_call = print_before_call
+        self._print_status = print_status
+
+    def __call__(self, func):
+        def DecoratorFunction(*args, **kargs):
+            """Decorator function.
+
+            Args:
+                *args: Arguments to pass to the functor.
+                **kwargs: Key-val based arguments to pass to the functor.
+
+            Raises:
+                Exception: The exception that functor(*args, **kwargs) throws.
+            """
+            timestart = time.time()
+            if self._print_before_call:
+                PrintColorString("%s ..."% self._function_description, end="")
+            try:
+                result = func(*args, **kargs)
+                if not self._print_before_call:
+                    PrintColorString("%s (%ds)" % (self._function_description,
+                                                   time.time()-timestart),
+                                     TextColors.OKGREEN)
+                if self._print_status:
+                    PrintColorString("OK! (%ds)" % (time.time()-timestart),
+                                     TextColors.OKGREEN)
+                return result
+            except:
+                if self._print_status:
+                    PrintColorString("Fail! (%ds)" % (time.time()-timestart),
+                                     TextColors.FAIL)
+                raise
+        return DecoratorFunction
diff --git a/public/actions/common_operations.py b/public/actions/common_operations.py
index 09f6495..e3a4035 100644
--- a/public/actions/common_operations.py
+++ b/public/actions/common_operations.py
@@ -22,7 +22,6 @@
 
 from __future__ import print_function
 import logging
-import time
 
 from acloud.public import avd
 from acloud.public import errors
@@ -100,6 +99,7 @@
             self.devices.append(
                 avd.AndroidVirtualDevice(ip=ip, instance_name=instance))
 
+    @utils.TimeExecute(function_description="Waiting for AVD(s) to boot up")
     def WaitForBoot(self):
         """Waits for all devices to boot up.
 
@@ -149,32 +149,14 @@
     """
     reporter = report.Report(command=command)
     try:
-        gce_start_time = time.time()
-        utils.PrintColorString("Creating GCE instance%s..." %
-                               ("s" if num > 1 else ""), end="")
         CreateSshKeyPairIfNecessary(cfg)
         device_pool = DevicePool(device_factory)
-        try:
-            device_pool.CreateDevices(num)
-        except:
-            utils.PrintColorString("Fail (%ds)" % (time.time() - gce_start_time),
-                                   utils.TextColors.FAIL)
-            raise
-        utils.PrintColorString("OK (%ds)" % (time.time() - gce_start_time),
-                               utils.TextColors.OKGREEN)
-
-        utils.PrintColorString("Starting up AVD%s..." %
-                               ("s" if num > 1 else ""), end="")
-        start_boot_time = time.time()
+        device_pool.CreateDevices(num)
         failures = device_pool.WaitForBoot()
         if failures:
             reporter.SetStatus(report.Status.BOOT_FAIL)
-            utils.PrintColorString("Fail (%ds)" % (time.time() - start_boot_time),
-                                   utils.TextColors.FAIL)
         else:
             reporter.SetStatus(report.Status.SUCCESS)
-            utils.PrintColorString("OK (%ds)" % (time.time() - start_boot_time),
-                                   utils.TextColors.OKGREEN)
         # Write result to report.
         for device in device_pool.devices:
             device_dict = {