Refactor RemoteInstanceDeviceFactory out of local_image_remote_instance.py

We're extracting the factory so that it can be utilized by moving
RemoteInstanceDeviceFactory to remote_instance_cf_device_factory.py

Bug:134919522
Test: acloud-dev create --local-image
Change-Id: Ife131c6570566dbe69c8ce6296b366078b0c5104
diff --git a/create/local_image_remote_instance.py b/create/local_image_remote_instance.py
index e8c8afa..9e00751 100644
--- a/create/local_image_remote_instance.py
+++ b/create/local_image_remote_instance.py
@@ -19,233 +19,20 @@
 local image.
 """
 
-from distutils.spawn import find_executable
-import getpass
-import glob
 import logging
 import os
-import subprocess
 
 from acloud import errors
 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
+from acloud.public.actions import remote_instance_cf_device_factory
 
 
 logger = logging.getLogger(__name__)
 
 _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 ")
-_CMD_LAUNCH_CVD_DISK_ARGS = ("-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 ")
-_SSH_CMD_MAX_RETRY = 2
-_SSH_CMD_RETRY_SLEEP = 3
-_USER_BUILD = "userbuild"
-
-
-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.
-        local_image_artifact: A string, path to local image.
-        cvd_host_package_artifact: A string, path to cvd host package.
-        report_internal_ip: Boolean, True for the internal ip is used when
-                            connecting from another GCE instance.
-        credentials: An oauth2client.OAuth2Credentials instance.
-        compute_client: An object of cvd_compute_client.CvdComputeClient.
-        ssh_cmd: Sting, ssh command to connect GCE instance.
-    """
-    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)
-        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._avd_spec.local_image_dir)
-        self._LaunchCvd(_CVD_USER, self._avd_spec.hw_property)
-        return instance
-
-    @staticmethod
-    def _ShellCmdWithRetry(remote_cmd):
-        """Runs a shell command on remote device.
-
-        If the network is unstable and causes SSH connect fail, it will retry.
-        When it retry in a short time, you may encounter unstable network. We
-        will use the mechanism of RETRY_BACKOFF_FACTOR. The retry time for each
-        failure is times * retries.
-
-        Args:
-            remote_cmd: A string, shell command to be run on remote.
-
-        Raises:
-            subprocess.CalledProcessError: For any non-zero return code of
-                                           remote_cmd.
-
-        Returns:
-            Boolean, True if the command was successfully executed. False otherwise.
-        """
-        return utils.RetryExceptionType(
-            exception_types=subprocess.CalledProcessError,
-            max_retries=_SSH_CMD_MAX_RETRY,
-            functor=lambda cmd: subprocess.check_call(cmd, shell=True),
-            sleep_multiplier=_SSH_CMD_RETRY_SLEEP,
-            retry_backoff_factor=utils.DEFAULT_RETRY_BACKOFF_FACTOR,
-            cmd=remote_cmd)
-
-    def _CreateGceInstance(self):
-        """Create a single configured cuttlefish device.
-
-        Override method from parent class.
-        build_target: The format is like "aosp_cf_x86_phone". We only get info
-                      from the user build image file name. If the file name is
-                      not custom format (no "-"), we will use $TARGET_PRODUCT
-                      from environment variable as build_target.
-
-        Returns:
-            A string, representing instance name.
-        """
-        image_name = os.path.basename(
-            self._local_image_artifact) if self._local_image_artifact else ""
-        build_target = (os.environ.get(constants.ENV_BUILD_TARGET) if "-" not
-                        in image_name else image_name.split("-")[0])
-        instance = self._compute_client.GenerateInstanceName(
-            build_target=build_target, build_id=_USER_BUILD)
-        # 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="Setting up GCE environment")
-    def _SetAVDenv(self, cvd_user):
-        """set the user to run AVD in the instance.
-
-        Args:
-            cvd_user: A string, user run the cvd 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)
-        remote_cmd = ""
-        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)
-        self._ShellCmdWithRetry(self._ssh_cmd + remote_cmd)
-
-    @utils.TimeExecute(function_description="Processing and uploading local images")
-    def _UploadArtifacts(self,
-                         cvd_user,
-                         local_image_zip,
-                         cvd_host_package_artifact,
-                         images_dir):
-        """Upload local images and avd local host package to instance.
-
-        There are two ways to upload local images.
-        1. Using local image zip, it would be decompressed by install_zip.sh.
-        2. Using local image directory, this directory contains all images.
-           Images are compressed/decompressed by lzop during upload process.
-
-        Args:
-            cvd_user: String, user upload the artifacts to instance.
-            local_image_zip: String, path to zip of local images which
-                             build from 'm dist'.
-            cvd_host_package_artifact: String, path to cvd host package.
-            images_dir: String, directory of local images which build
-                        from 'm'.
-        """
-        # TODO(b/133461252) Deprecate acloud create with local image zip.
-        # Upload local image zip file
-        if local_image_zip:
-            remote_cmd = ("\"sudo su -c '/usr/bin/install_zip.sh .' - '%s'\" < %s"
-                          % (cvd_user, local_image_zip))
-            logger.debug("remote_cmd:\n %s", remote_cmd)
-            self._ShellCmdWithRetry(self._ssh_cmd + remote_cmd)
-        else:
-            # Compress image files for faster upload.
-            artifact_files = [os.path.basename(image) for image in glob.glob(
-                os.path.join(images_dir, "*.img"))]
-            cmd = ("tar -cf - --lzop -S -C {images_dir} {artifact_files} | "
-                   "{ssh_cmd} -- tar -xf - --lzop -S".format(
-                       images_dir=images_dir,
-                       artifact_files=" ".join(artifact_files),
-                       ssh_cmd=self._ssh_cmd))
-            logger.debug("cmd:\n %s", cmd)
-            self._ShellCmdWithRetry(cmd)
-
-        # 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)
-        self._ShellCmdWithRetry(self._ssh_cmd + remote_cmd)
-
-    def _LaunchCvd(self, cvd_user, hw_property):
-        """Launch CVD.
-
-        Args:
-            cvd_user: A string, user run the cvd in the instance.
-            hw_property: dict object of hw property.
-        """
-        launch_cvd_args = _CMD_LAUNCH_CVD_ARGS % (
-            hw_property["cpu"],
-            hw_property["x_res"],
-            hw_property["y_res"],
-            hw_property["dpi"],
-            hw_property["memory"])
-        if constants.HW_ALIAS_DISK in hw_property:
-            launch_cvd_args = (launch_cvd_args + _CMD_LAUNCH_CVD_DISK_ARGS %
-                               hw_property[constants.HW_ALIAS_DISK])
-        remote_cmd = ("\"sudo su -c 'bin/launch_cvd %s>&/dev/ttyS0&' - '%s'\"" %
-                      (launch_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):
@@ -312,7 +99,7 @@
             A Report instance.
         """
         self.cvd_host_package_artifact = self.VerifyHostPackageArtifactsExist()
-        device_factory = RemoteInstanceDeviceFactory(
+        device_factory = remote_instance_cf_device_factory.RemoteInstanceDeviceFactory(
             avd_spec,
             avd_spec.local_image_artifact,
             self.cvd_host_package_artifact)
diff --git a/create/local_image_remote_instance_test.py b/create/local_image_remote_instance_test.py
index 821be68..8764ff6 100644
--- a/create/local_image_remote_instance_test.py
+++ b/create/local_image_remote_instance_test.py
@@ -18,22 +18,14 @@
 Create class that is responsible for creating a remote instance AVD with a
 local image.
 """
-import uuid
 
-import glob
 import os
-import subprocess
-import time
 import unittest
 
 import mock
 
 from acloud import errors
-from acloud.create import avd_spec
 from acloud.create import local_image_remote_instance
-from acloud.internal import constants
-from acloud.internal.lib import auth
-from acloud.internal.lib import cvd_compute_client
 from acloud.internal.lib import driver_test_lib
 from acloud.internal.lib import utils
 
@@ -76,75 +68,5 @@
                 "/fake_dir2/cvd-host_package.tar.gz")
 
 
-class RemoteInstanceDeviceFactoryTest(driver_test_lib.BaseDriverTest):
-    """Test RemoteInstanceDeviceFactory method."""
-
-    def setUp(self):
-        """Set up the test."""
-        super(RemoteInstanceDeviceFactoryTest, self).setUp()
-        self.Patch(auth, "CreateCredentials", return_value=mock.MagicMock())
-        self.Patch(cvd_compute_client.CvdComputeClient, "InitResourceHandle")
-
-    # pylint: disable=protected-access
-    def testSSHExecuteWithRetry(self):
-        """test SSHExecuteWithRetry method."""
-        self.Patch(time, "sleep")
-        factory = local_image_remote_instance.RemoteInstanceDeviceFactory
-        self.Patch(subprocess, "check_call",
-                   side_effect=subprocess.CalledProcessError(
-                       None, "ssh command fail."))
-        self.assertRaises(subprocess.CalledProcessError,
-                          factory._ShellCmdWithRetry,
-                          "fake cmd")
-        self.assertEqual(subprocess.check_call.call_count, #pylint: disable=no-member
-                         local_image_remote_instance._SSH_CMD_MAX_RETRY + 1)
-        self.Patch(subprocess, "check_call", return_value=True)
-        self.assertEqual(factory._ShellCmdWithRetry("fake cmd"), True)
-
-    # pylint: disable=protected-access
-    @mock.patch.dict(os.environ, {constants.ENV_BUILD_TARGET:'fake-target'})
-    def testCreateGceInstanceName(self):
-        """test create gce instance."""
-        self.Patch(utils, "GetBuildEnvironmentVariable",
-                   return_value="test_environ")
-        self.Patch(glob, "glob", return_vale=["fake.img"])
-        # Mock uuid
-        args = mock.MagicMock()
-        args.config_file = ""
-        args.avd_type = constants.TYPE_CF
-        args.flavor = "phone"
-        args.local_image = None
-        args.adb_port = None
-        fake_avd_spec = avd_spec.AVDSpec(args)
-
-        fake_uuid = mock.MagicMock(hex="1234")
-        self.Patch(uuid, "uuid4", return_value=fake_uuid)
-        self.Patch(cvd_compute_client.CvdComputeClient, "CreateInstance")
-        fake_host_package_name = "/fake/host_package.tar.gz"
-        fake_image_name = "/fake/aosp_cf_x86_phone-img-eng.username.zip"
-
-        factory = local_image_remote_instance.RemoteInstanceDeviceFactory(
-            fake_avd_spec,
-            fake_image_name,
-            fake_host_package_name)
-        self.assertEqual(factory._CreateGceInstance(), "ins-1234-userbuild-aosp-cf-x86-phone")
-
-        # Can't get target name from zip file name.
-        fake_image_name = "/fake/aosp_cf_x86_phone.username.zip"
-        factory = local_image_remote_instance.RemoteInstanceDeviceFactory(
-            fake_avd_spec,
-            fake_image_name,
-            fake_host_package_name)
-        self.assertEqual(factory._CreateGceInstance(), "ins-1234-userbuild-fake-target")
-
-        # No image zip path, it uses local build images.
-        fake_image_name = ""
-        factory = local_image_remote_instance.RemoteInstanceDeviceFactory(
-            fake_avd_spec,
-            fake_image_name,
-            fake_host_package_name)
-        self.assertEqual(factory._CreateGceInstance(), "ins-1234-userbuild-fake-target")
-
-
 if __name__ == "__main__":
     unittest.main()
diff --git a/public/actions/remote_instance_cf_device_factory.py b/public/actions/remote_instance_cf_device_factory.py
new file mode 100644
index 0000000..a4ab76f
--- /dev/null
+++ b/public/actions/remote_instance_cf_device_factory.py
@@ -0,0 +1,240 @@
+# Copyright 2019 - 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.
+
+"""RemoteInstanceDeviceFactory provides basic interface to create a cuttlefish
+device factory."""
+
+from distutils.spawn import find_executable
+import getpass
+import glob
+import logging
+import os
+import subprocess
+
+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
+
+
+logger = logging.getLogger(__name__)
+
+_CVD_USER = getpass.getuser()
+_CMD_LAUNCH_CVD_ARGS = ("-cpus %s -x_res %s -y_res %s -dpi %s "
+                        "-memory_mb %s ")
+_CMD_LAUNCH_CVD_DISK_ARGS = ("-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 ")
+_SSH_CMD_MAX_RETRY = 2
+_SSH_CMD_RETRY_SLEEP = 3
+_USER_BUILD = "userbuild"
+
+
+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.
+        local_image_artifact: A string, path to local image.
+        cvd_host_package_artifact: A string, path to cvd host package.
+        report_internal_ip: Boolean, True for the internal ip is used when
+                            connecting from another GCE instance.
+        credentials: An oauth2client.OAuth2Credentials instance.
+        compute_client: An object of cvd_compute_client.CvdComputeClient.
+        ssh_cmd: Sting, ssh command to connect GCE instance.
+    """
+    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)
+        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._avd_spec.local_image_dir)
+        self._LaunchCvd(_CVD_USER, self._avd_spec.hw_property)
+        return instance
+
+    @staticmethod
+    def _ShellCmdWithRetry(remote_cmd):
+        """Runs a shell command on remote device.
+
+        If the network is unstable and causes SSH connect fail, it will retry.
+        When it retry in a short time, you may encounter unstable network. We
+        will use the mechanism of RETRY_BACKOFF_FACTOR. The retry time for each
+        failure is times * retries.
+
+        Args:
+            remote_cmd: A string, shell command to be run on remote.
+
+        Raises:
+            subprocess.CalledProcessError: For any non-zero return code of
+                                           remote_cmd.
+
+        Returns:
+            Boolean, True if the command was successfully executed. False otherwise.
+        """
+        return utils.RetryExceptionType(
+            exception_types=subprocess.CalledProcessError,
+            max_retries=_SSH_CMD_MAX_RETRY,
+            functor=lambda cmd: subprocess.check_call(cmd, shell=True),
+            sleep_multiplier=_SSH_CMD_RETRY_SLEEP,
+            retry_backoff_factor=utils.DEFAULT_RETRY_BACKOFF_FACTOR,
+            cmd=remote_cmd)
+
+    def _CreateGceInstance(self):
+        """Create a single configured cuttlefish device.
+
+        Override method from parent class.
+        build_target: The format is like "aosp_cf_x86_phone". We only get info
+                      from the user build image file name. If the file name is
+                      not custom format (no "-"), we will use $TARGET_PRODUCT
+                      from environment variable as build_target.
+
+        Returns:
+            A string, representing instance name.
+        """
+        image_name = os.path.basename(
+            self._local_image_artifact) if self._local_image_artifact else ""
+        build_target = (os.environ.get(constants.ENV_BUILD_TARGET) if "-" not
+                        in image_name else image_name.split("-")[0])
+        instance = self._compute_client.GenerateInstanceName(
+            build_target=build_target, build_id=_USER_BUILD)
+        # 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="Setting up GCE environment")
+    def _SetAVDenv(self, cvd_user):
+        """set the user to run AVD in the instance.
+
+        Args:
+            cvd_user: A string, user run the cvd 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)
+        remote_cmd = ""
+        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)
+        self._ShellCmdWithRetry(self._ssh_cmd + remote_cmd)
+
+    @utils.TimeExecute(function_description="Processing and uploading local images")
+    def _UploadArtifacts(self,
+                         cvd_user,
+                         local_image_zip,
+                         cvd_host_package_artifact,
+                         images_dir):
+        """Upload local images and avd local host package to instance.
+
+        There are two ways to upload local images.
+        1. Using local image zip, it would be decompressed by install_zip.sh.
+        2. Using local image directory, this directory contains all images.
+           Images are compressed/decompressed by lzop during upload process.
+
+        Args:
+            cvd_user: String, user upload the artifacts to instance.
+            local_image_zip: String, path to zip of local images which
+                             build from 'm dist'.
+            cvd_host_package_artifact: String, path to cvd host package.
+            images_dir: String, directory of local images which build
+                        from 'm'.
+        """
+        # TODO(b/133461252) Deprecate acloud create with local image zip.
+        # Upload local image zip file
+        if local_image_zip:
+            remote_cmd = ("\"sudo su -c '/usr/bin/install_zip.sh .' - '%s'\" < %s"
+                          % (cvd_user, local_image_zip))
+            logger.debug("remote_cmd:\n %s", remote_cmd)
+            self._ShellCmdWithRetry(self._ssh_cmd + remote_cmd)
+        else:
+            # Compress image files for faster upload.
+            artifact_files = [os.path.basename(image) for image in glob.glob(
+                os.path.join(images_dir, "*.img"))]
+            cmd = ("tar -cf - --lzop -S -C {images_dir} {artifact_files} | "
+                   "{ssh_cmd} -- tar -xf - --lzop -S".format(
+                       images_dir=images_dir,
+                       artifact_files=" ".join(artifact_files),
+                       ssh_cmd=self._ssh_cmd))
+            logger.debug("cmd:\n %s", cmd)
+            self._ShellCmdWithRetry(cmd)
+
+        # 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)
+        self._ShellCmdWithRetry(self._ssh_cmd + remote_cmd)
+
+    def _LaunchCvd(self, cvd_user, hw_property):
+        """Launch CVD.
+
+        Args:
+            cvd_user: A string, user run the cvd in the instance.
+            hw_property: dict object of hw property.
+        """
+        launch_cvd_args = _CMD_LAUNCH_CVD_ARGS % (
+            hw_property["cpu"],
+            hw_property["x_res"],
+            hw_property["y_res"],
+            hw_property["dpi"],
+            hw_property["memory"])
+        if constants.HW_ALIAS_DISK in hw_property:
+            launch_cvd_args = (launch_cvd_args + _CMD_LAUNCH_CVD_DISK_ARGS %
+                               hw_property[constants.HW_ALIAS_DISK])
+        remote_cmd = ("\"sudo su -c 'bin/launch_cvd %s>&/dev/ttyS0&' - '%s'\"" %
+                      (launch_cvd_args, cvd_user))
+        logger.debug("remote_cmd:\n %s", remote_cmd)
+        subprocess.Popen(self._ssh_cmd + remote_cmd, shell=True)
diff --git a/public/actions/remote_instance_cf_device_factory_test.py b/public/actions/remote_instance_cf_device_factory_test.py
new file mode 100644
index 0000000..2a68a39
--- /dev/null
+++ b/public/actions/remote_instance_cf_device_factory_test.py
@@ -0,0 +1,105 @@
+# Copyright 2019 - 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_instance_cf_device_factory."""
+
+import glob
+import os
+import subprocess
+import time
+import unittest
+import uuid
+
+import mock
+
+from acloud.create import avd_spec
+from acloud.internal import constants
+from acloud.internal.lib import auth
+from acloud.internal.lib import cvd_compute_client
+from acloud.internal.lib import driver_test_lib
+from acloud.internal.lib import utils
+from acloud.public.actions import remote_instance_cf_device_factory
+
+
+class RemoteInstanceDeviceFactoryTest(driver_test_lib.BaseDriverTest):
+    """Test RemoteInstanceDeviceFactory method."""
+
+    def setUp(self):
+        """Set up the test."""
+        super(RemoteInstanceDeviceFactoryTest, self).setUp()
+        self.Patch(auth, "CreateCredentials", return_value=mock.MagicMock())
+        self.Patch(cvd_compute_client.CvdComputeClient, "InitResourceHandle")
+
+    # pylint: disable=protected-access
+    def testSSHExecuteWithRetry(self):
+        """test SSHExecuteWithRetry method."""
+        self.Patch(time, "sleep")
+        factory = remote_instance_cf_device_factory.RemoteInstanceDeviceFactory
+        self.Patch(subprocess, "check_call",
+                   side_effect=subprocess.CalledProcessError(
+                       None, "ssh command fail."))
+        self.assertRaises(subprocess.CalledProcessError,
+                          factory._ShellCmdWithRetry,
+                          "fake cmd")
+        self.assertEqual(subprocess.check_call.call_count, #pylint: disable=no-member
+                         remote_instance_cf_device_factory._SSH_CMD_MAX_RETRY + 1)
+        self.Patch(subprocess, "check_call", return_value=True)
+        self.assertEqual(factory._ShellCmdWithRetry("fake cmd"), True)
+
+    # pylint: disable=protected-access
+    @mock.patch.dict(os.environ, {constants.ENV_BUILD_TARGET:'fake-target'})
+    def testCreateGceInstanceName(self):
+        """test create gce instance."""
+        self.Patch(utils, "GetBuildEnvironmentVariable",
+                   return_value="test_environ")
+        self.Patch(glob, "glob", return_vale=["fake.img"])
+        # Mock uuid
+        args = mock.MagicMock()
+        args.config_file = ""
+        args.avd_type = constants.TYPE_CF
+        args.flavor = "phone"
+        args.local_image = None
+        args.adb_port = None
+        fake_avd_spec = avd_spec.AVDSpec(args)
+
+        fake_uuid = mock.MagicMock(hex="1234")
+        self.Patch(uuid, "uuid4", return_value=fake_uuid)
+        self.Patch(cvd_compute_client.CvdComputeClient, "CreateInstance")
+        fake_host_package_name = "/fake/host_package.tar.gz"
+        fake_image_name = "/fake/aosp_cf_x86_phone-img-eng.username.zip"
+
+        factory = remote_instance_cf_device_factory.RemoteInstanceDeviceFactory(
+            fake_avd_spec,
+            fake_image_name,
+            fake_host_package_name)
+        self.assertEqual(factory._CreateGceInstance(), "ins-1234-userbuild-aosp-cf-x86-phone")
+
+        # Can't get target name from zip file name.
+        fake_image_name = "/fake/aosp_cf_x86_phone.username.zip"
+        factory = remote_instance_cf_device_factory.RemoteInstanceDeviceFactory(
+            fake_avd_spec,
+            fake_image_name,
+            fake_host_package_name)
+        self.assertEqual(factory._CreateGceInstance(), "ins-1234-userbuild-fake-target")
+
+        # No image zip path, it uses local build images.
+        fake_image_name = ""
+        factory = remote_instance_cf_device_factory.RemoteInstanceDeviceFactory(
+            fake_avd_spec,
+            fake_image_name,
+            fake_host_package_name)
+        self.assertEqual(factory._CreateGceInstance(), "ins-1234-userbuild-fake-target")
+
+
+if __name__ == "__main__":
+    unittest.main()