Merge "Retry for checking ssh connection."
diff --git a/create/cheeps_remote_image_remote_instance_test.py b/create/cheeps_remote_image_remote_instance_test.py
index 0837401..626da4a 100644
--- a/create/cheeps_remote_image_remote_instance_test.py
+++ b/create/cheeps_remote_image_remote_instance_test.py
@@ -11,13 +11,13 @@
 from acloud.internal.lib import auth
 from acloud.internal.lib import cheeps_compute_client
 from acloud.internal.lib import driver_test_lib
-from acloud.internal.lib import gcompute_client
+from acloud.internal.lib import ssh
 
 
 class CheepsRemoteImageRemoteInstanceTest(driver_test_lib.BaseDriverTest):
     """Test cheeps_remote_image_remote_instance."""
 
-    IP = gcompute_client.IP(external="127.0.0.1", internal="10.0.0.1")
+    IP = ssh.IP(external="127.0.0.1", internal="10.0.0.1")
     INSTANCE = "fake-instance"
     IMAGE = "fake-image"
     GPU = "nvidia-tesla-k80"
diff --git a/internal/lib/android_compute_client.py b/internal/lib/android_compute_client.py
index b32c1ab..1bc69bc 100755
--- a/internal/lib/android_compute_client.py
+++ b/internal/lib/android_compute_client.py
@@ -375,7 +375,7 @@
             zone: String, representing zone name, e.g. "us-central1-f"
 
         Returns:
-            NamedTuple of (internal, external) IP of the instance.
+            ssh.IP object, that stores internal and external ip of the instance.
         """
         return super(AndroidComputeClient, self).GetInstanceIP(
             instance, zone or self._zone)
diff --git a/internal/lib/cvd_compute_client_multi_stage.py b/internal/lib/cvd_compute_client_multi_stage.py
index 17f2379..44c0e90 100644
--- a/internal/lib/cvd_compute_client_multi_stage.py
+++ b/internal/lib/cvd_compute_client_multi_stage.py
@@ -49,7 +49,6 @@
 from acloud.internal.lib import gcompute_client
 from acloud.internal.lib import utils
 from acloud.internal.lib.ssh import Ssh
-from acloud.internal.lib.ssh import IP
 from acloud.pull import pull
 
 
@@ -166,7 +165,7 @@
             ip = self._CreateGceInstance(instance, image_name, image_project,
                                          extra_scopes, boot_disk_size_gb,
                                          avd_spec)
-        self._ssh = Ssh(ip=IP(internal=ip.internal, external=ip.external),
+        self._ssh = Ssh(ip=ip,
                         gce_user=constants.GCE_USER,
                         ssh_private_key_path=self._ssh_private_key_path,
                         extra_args_ssh_tunnel=self._extra_args_ssh_tunnel,
@@ -298,7 +297,7 @@
         """Launch CVD.
 
         Launch AVD with launch_cvd. If the process is failed, acloud would show
-        error messages and atuo download log files from remote instance.
+        error messages and auto download log files from remote instance.
 
         Args:
             instance: String, instance name.
@@ -379,7 +378,7 @@
             avd_spec: An AVDSpec instance.
 
         Returns:
-            Namedtuple of (internal, external) IP of the instance.
+            ssh.IP object, that stores internal and external ip of the instance.
         """
         timestart = time.time()
         metadata = self._metadata.copy()
diff --git a/internal/lib/gcompute_client.py b/internal/lib/gcompute_client.py
index 945a14c..8c56ea7 100755
--- a/internal/lib/gcompute_client.py
+++ b/internal/lib/gcompute_client.py
@@ -26,7 +26,6 @@
 generic, and only knows how to talk to Compute Engine APIs.
 """
 # pylint: disable=too-many-lines
-import collections
 import copy
 import functools
 import getpass
@@ -37,6 +36,7 @@
 from acloud.internal import constants
 from acloud.internal.lib import base_cloud_client
 from acloud.internal.lib import utils
+from acloud.internal.lib.ssh import IP
 
 
 logger = logging.getLogger(__name__)
@@ -56,8 +56,6 @@
     "initializeParams": {},
 }
 
-IP = collections.namedtuple("IP", ["external", "internal"])
-
 
 class OperationScope(object):
     """Represents operation scope enum."""
@@ -1382,7 +1380,7 @@
             zone: String, name of the zone.
 
         Returns:
-            NamedTuple of (internal, external) IP of the instance.
+            ssh.IP object, that stores internal and external ip of the instance.
         """
         instance = self.GetInstance(instance, zone)
         internal_ip = instance["networkInterfaces"][0]["networkIP"]
diff --git a/public/actions/common_operations_test.py b/public/actions/common_operations_test.py
index 18275c1..eba74b8 100644
--- a/public/actions/common_operations_test.py
+++ b/public/actions/common_operations_test.py
@@ -26,14 +26,14 @@
 from acloud.internal.lib import android_compute_client
 from acloud.internal.lib import auth
 from acloud.internal.lib import driver_test_lib
-from acloud.internal.lib import gcompute_client
+from acloud.internal.lib import ssh
 from acloud.public import report
 from acloud.public.actions import common_operations
 
 
 class CommonOperationsTest(driver_test_lib.BaseDriverTest):
     """Test Common Operations."""
-    IP = gcompute_client.IP(external="127.0.0.1", internal="10.0.0.1")
+    IP = ssh.IP(external="127.0.0.1", internal="10.0.0.1")
     INSTANCE = "fake-instance"
     CMD = "test-cmd"
     AVD_TYPE = "fake-type"
diff --git a/public/actions/create_cuttlefish_action_test.py b/public/actions/create_cuttlefish_action_test.py
index 728d6e0..f4788c5 100644
--- a/public/actions/create_cuttlefish_action_test.py
+++ b/public/actions/create_cuttlefish_action_test.py
@@ -29,14 +29,14 @@
 from acloud.internal.lib import cvd_compute_client
 from acloud.internal.lib import cvd_compute_client_multi_stage
 from acloud.internal.lib import driver_test_lib
-from acloud.internal.lib import gcompute_client
+from acloud.internal.lib import ssh
 from acloud.public.actions import create_cuttlefish_action
 
 
 class CreateCuttlefishActionTest(driver_test_lib.BaseDriverTest):
     """Test create_cuttlefish_action."""
 
-    IP = gcompute_client.IP(external="127.0.0.1", internal="10.0.0.1")
+    IP = ssh.IP(external="127.0.0.1", internal="10.0.0.1")
     INSTANCE = "fake-instance"
     IMAGE = "fake-image"
     BUILD_TARGET = "fake-build-target"
diff --git a/public/actions/create_goldfish_action_test.py b/public/actions/create_goldfish_action_test.py
index 35f96ab..fa04ee2 100644
--- a/public/actions/create_goldfish_action_test.py
+++ b/public/actions/create_goldfish_action_test.py
@@ -23,15 +23,15 @@
 from acloud.internal.lib import android_compute_client
 from acloud.internal.lib import auth
 from acloud.internal.lib import driver_test_lib
-from acloud.internal.lib import gcompute_client
 from acloud.internal.lib import goldfish_compute_client
+from acloud.internal.lib import ssh
 from acloud.public.actions import create_goldfish_action
 
 
 class CreateGoldfishActionTest(driver_test_lib.BaseDriverTest):
     """Tests create_goldfish_action."""
 
-    IP = gcompute_client.IP(external="127.0.0.1", internal="10.0.0.1")
+    IP = ssh.IP(external="127.0.0.1", internal="10.0.0.1")
     INSTANCE = "fake-instance"
     IMAGE = "fake-image"
     BUILD_TARGET = "fake-build-target"
diff --git a/public/actions/remote_instance_cf_device_factory.py b/public/actions/remote_instance_cf_device_factory.py
index 57447cf..8a539c1 100644
--- a/public/actions/remote_instance_cf_device_factory.py
+++ b/public/actions/remote_instance_cf_device_factory.py
@@ -44,7 +44,7 @@
                             connecting from another GCE instance.
         credentials: An oauth2client.OAuth2Credentials instance.
         compute_client: An object of cvd_compute_client.CvdComputeClient.
-        ip: Namedtuple of (internal, external) IP of the instance.
+        ssh: An Ssh object.
     """
     def __init__(self, avd_spec, local_image_artifact=None,
                  cvd_host_package_artifact=None):
@@ -166,7 +166,7 @@
             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 = ssh.Ssh(ip=ssh.IP(internal=ip.internal, external=ip.external),
+        self._ssh = ssh.Ssh(ip=ip,
                             gce_user=constants.GCE_USER,
                             ssh_private_key_path=self._cfg.ssh_private_key_path,
                             extra_args_ssh_tunnel=self._cfg.extra_args_ssh_tunnel,
diff --git a/public/device_driver_test.py b/public/device_driver_test.py
index 42bb5c9..237d3ea 100644
--- a/public/device_driver_test.py
+++ b/public/device_driver_test.py
@@ -29,8 +29,8 @@
 from acloud.internal.lib import android_build_client
 from acloud.internal.lib import android_compute_client
 from acloud.internal.lib import driver_test_lib
-from acloud.internal.lib import gcompute_client
 from acloud.internal.lib import gstorage_client
+from acloud.internal.lib import ssh
 from acloud.public import device_driver
 
 
@@ -81,7 +81,7 @@
         """Test CreateGCETypeAVD."""
         cfg = _CreateCfg()
         fake_gs_url = "fake_gs_url"
-        fake_ip = gcompute_client.IP(external="140.1.1.1", internal="10.1.1.1")
+        fake_ip = ssh.IP(external="140.1.1.1", internal="10.1.1.1")
         fake_instance = "fake-instance"
         fake_image = "fake-image"
         fake_build_target = "fake_target"
@@ -136,7 +136,7 @@
     def testCreateGCETypeAVDInternalIP(self):
         """Test CreateGCETypeAVD with internal IP."""
         cfg = _CreateCfg()
-        fake_ip = gcompute_client.IP(external="140.1.1.1", internal="10.1.1.1")
+        fake_ip = ssh.IP(external="140.1.1.1", internal="10.1.1.1")
         fake_instance = "fake-instance"
         fake_build_target = "fake_target"
         fake_build_id = "12345"
diff --git a/pull/pull.py b/pull/pull.py
index 60c2074..1f99c5c 100644
--- a/pull/pull.py
+++ b/pull/pull.py
@@ -41,7 +41,7 @@
 _IMG_FILE_EXTENSION = ".img"
 
 
-def PullFileFromInstance(cfg, instance):
+def PullFileFromInstance(cfg, instance, file_name=None, no_prompts=False):
     """Pull file from remote CF instance.
 
     1. Download log files to temp folder.
@@ -51,6 +51,8 @@
     Args:
         cfg: AcloudConfig object.
         instance: list.Instance() object.
+        file_name: String of file name.
+        no_prompts: Boolean, True to skip the prompt about file streaming.
 
     Returns:
         A Report instance.
@@ -59,11 +61,11 @@
               gce_user=constants.GCE_USER,
               ssh_private_key_path=cfg.ssh_private_key_path,
               extra_args_ssh_tunnel=cfg.extra_args_ssh_tunnel)
-    log_files = SelectLogFileToPull(ssh)
+    log_files = SelectLogFileToPull(ssh, file_name)
     download_folder = GetDownloadLogFolder(instance.name)
     PullLogs(ssh, log_files, download_folder)
     if len(log_files) == 1:
-        DisplayLog(ssh, log_files[0])
+        DisplayLog(ssh, log_files[0], no_prompts)
     return report.Report(command="pull")
 
 
@@ -81,17 +83,18 @@
     _DisplayPullResult(download_folder)
 
 
-def DisplayLog(ssh, log_file):
+def DisplayLog(ssh, log_file, no_prompts=False):
     """Display the content of log file in the screen.
 
     Args:
         ssh: Ssh object.
         log_file: String of the log file path.
+        no_prompts: Boolean, True to skip all prompts.
     """
     warning_msg = ("It will stream log to show on screen. If you want to stop "
                    "streaming, please press CTRL-C to exit.\nPress 'y' to show "
                    "log or read log by myself[y/N]:")
-    if utils.GetUserAnswerYes(warning_msg):
+    if no_prompts or utils.GetUserAnswerYes(warning_msg):
         ssh.Run("tail -f -n +1 %s" % log_file, show_output=True)
 
 
@@ -122,22 +125,29 @@
     return log_folder
 
 
-def SelectLogFileToPull(ssh):
+def SelectLogFileToPull(ssh, file_name=None):
     """Select one log file or all log files to downalod.
 
     1. Get all log file paths as selection list
-    2. Get user selected file path
+    2. Get user selected file path or user provided file name.
 
     Args:
         ssh: Ssh object.
-
-    Raises:
-        errors.CheckPathError: Can't find log files.
+        file_name: String of file name.
 
     Returns:
         List of selected file paths.
+
+    Raises:
+        errors.CheckPathError: Can't find log files.
     """
     log_files = GetAllLogFilePaths(ssh)
+    if file_name:
+        file_path = os.path.join(_REMOTE_LOG_FOLDER, file_name)
+        if file_path in log_files:
+            return [file_path]
+        raise errors.CheckPathError("Can't find this log file(%s) from remote "
+                                    "instance." % file_path)
 
     if len(log_files) == 1:
         return log_files
@@ -147,7 +157,7 @@
         return utils.GetAnswerFromList(log_files, enable_choose_all=True)
 
     raise errors.CheckPathError("Can't find any log file in folder(%s) from "
-                                "remote instance" % _REMOTE_LOG_FOLDER)
+                                "remote instance." % _REMOTE_LOG_FOLDER)
 
 
 def GetAllLogFilePaths(ssh):
@@ -207,5 +217,8 @@
     if args.instance_name:
         instance = list_instances.GetInstancesFromInstanceNames(
             cfg, [args.instance_name])
-        return PullFileFromInstance(cfg, instance[0])
-    return PullFileFromInstance(cfg, list_instances.ChooseOneRemoteInstance(cfg))
+        return PullFileFromInstance(cfg, instance[0], args.file_name, args.no_prompt)
+    return PullFileFromInstance(cfg,
+                                list_instances.ChooseOneRemoteInstance(cfg),
+                                args.file_name,
+                                args.no_prompt)
diff --git a/pull/pull_args.py b/pull/pull_args.py
index 62a28d4..7ab6172 100644
--- a/pull/pull_args.py
+++ b/pull/pull_args.py
@@ -39,5 +39,19 @@
         type=str,
         required=False,
         help="The name of the remote instance that need to pull log files.")
+    pull_parser.add_argument(
+        "--file-name",
+        dest="file_name",
+        type=str,
+        required=False,
+        help="The log file name to pull from the remote instance, "
+             "e.g. launcher.log, kernel.log.")
+    pull_parser.add_argument(
+        "--yes", "-y",
+        action="store_true",
+        dest="no_prompt",
+        required=False,
+        help="Assume 'yes' as an answer to the prompt when asking users about "
+             "file streaming.")
 
     return pull_parser
diff --git a/pull/pull_test.py b/pull/pull_test.py
index 458bb87..c82f12f 100644
--- a/pull/pull_test.py
+++ b/pull/pull_test.py
@@ -48,6 +48,22 @@
         expected_result = ["file2.log"]
         self.assertEqual(pull.SelectLogFileToPull(ssh), expected_result)
 
+        # Test user provided file name exist.
+        log_files = ["/home/vsoc-01/cuttlefish_runtime/file1.log",
+                     "/home/vsoc-01/cuttlefish_runtime/file2.log"]
+        input_file = "file1.log"
+        self.Patch(pull, "GetAllLogFilePaths", return_value=log_files)
+        expected_result = ["/home/vsoc-01/cuttlefish_runtime/file1.log"]
+        self.assertEqual(pull.SelectLogFileToPull(ssh, input_file), expected_result)
+
+        # Test user provided file name not exist.
+        log_files = ["/home/vsoc-01/cuttlefish_runtime/file1.log",
+                     "/home/vsoc-01/cuttlefish_runtime/file2.log"]
+        input_file = "not_exist.log"
+        self.Patch(pull, "GetAllLogFilePaths", return_value=log_files)
+        with self.assertRaises(errors.CheckPathError):
+            pull.SelectLogFileToPull(ssh, input_file)
+
     def testFilterLogfiles(self):
         """test filer log file from black list."""
         # Filter out file name is "kernel".