1# Copyright 2019 - The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14r"""Pull entry point. 15 16This command will pull the log files from a remote instance for AVD troubleshooting. 17""" 18 19from __future__ import print_function 20import logging 21import os 22import subprocess 23import tempfile 24 25from acloud import errors 26from acloud.internal import constants 27from acloud.internal.lib import utils 28from acloud.internal.lib.ssh import Ssh 29from acloud.internal.lib.ssh import IP 30from acloud.list import list as list_instances 31from acloud.public import config 32from acloud.public import report 33 34 35logger = logging.getLogger(__name__) 36 37_REMOTE_LOG_FOLDER = "/home/%s/cuttlefish_runtime" % constants.GCE_USER 38_FIND_LOG_FILE_CMD = "find %s -type f" % _REMOTE_LOG_FOLDER 39# Black list for log files. 40_KERNEL = "kernel" 41_IMG_FILE_EXTENSION = ".img" 42 43 44def PullFileFromInstance(cfg, instance, file_name=None, no_prompts=False): 45 """Pull file from remote CF instance. 46 47 1. Download log files to temp folder. 48 2. If only one file selected, display it on screen. 49 3. Show the download folder for users. 50 51 Args: 52 cfg: AcloudConfig object. 53 instance: list.Instance() object. 54 file_name: String of file name. 55 no_prompts: Boolean, True to skip the prompt about file streaming. 56 57 Returns: 58 A Report instance. 59 """ 60 ssh = Ssh(ip=IP(ip=instance.ip), 61 user=constants.GCE_USER, 62 ssh_private_key_path=cfg.ssh_private_key_path, 63 extra_args_ssh_tunnel=cfg.extra_args_ssh_tunnel) 64 log_files = SelectLogFileToPull(ssh, file_name) 65 download_folder = GetDownloadLogFolder(instance.name) 66 PullLogs(ssh, log_files, download_folder) 67 if len(log_files) == 1: 68 DisplayLog(ssh, log_files[0], no_prompts) 69 return report.Report(command="pull") 70 71 72def PullLogs(ssh, log_files, download_folder): 73 """Pull log files from remote instance. 74 75 Args: 76 ssh: Ssh object. 77 log_files: List of file path in the remote instance. 78 download_folder: String of download folder path. 79 """ 80 for log_file in log_files: 81 target_file = os.path.join(download_folder, os.path.basename(log_file)) 82 ssh.ScpPullFile(log_file, target_file) 83 _DisplayPullResult(download_folder) 84 85 86def DisplayLog(ssh, log_file, no_prompts=False): 87 """Display the content of log file in the screen. 88 89 Args: 90 ssh: Ssh object. 91 log_file: String of the log file path. 92 no_prompts: Boolean, True to skip all prompts. 93 """ 94 warning_msg = ("It will stream log to show on screen. If you want to stop " 95 "streaming, please press CTRL-C to exit.\nPress 'y' to show " 96 "log or read log by myself[y/N]:") 97 if no_prompts or utils.GetUserAnswerYes(warning_msg): 98 ssh.Run("tail -f -n +1 %s" % log_file, show_output=True) 99 100 101def _DisplayPullResult(download_folder): 102 """Display messages to user after pulling log files. 103 104 Args: 105 download_folder: String of download folder path. 106 """ 107 utils.PrintColorString( 108 "Download logs to folder: %s \nYou can look into log files to check " 109 "AVD issues." % download_folder) 110 111 112def GetDownloadLogFolder(instance): 113 """Get the download log folder accroding to instance name. 114 115 Args: 116 instance: String, the name of instance. 117 118 Returns: 119 String of the download folder path. 120 """ 121 log_folder = os.path.join(tempfile.gettempdir(), instance) 122 if not os.path.exists(log_folder): 123 os.makedirs(log_folder) 124 logger.info("Download logs to folder: %s", log_folder) 125 return log_folder 126 127 128def SelectLogFileToPull(ssh, file_name=None): 129 """Select one log file or all log files to downalod. 130 131 1. Get all log file paths as selection list 132 2. Get user selected file path or user provided file name. 133 134 Args: 135 ssh: Ssh object. 136 file_name: String of file name. 137 138 Returns: 139 List of selected file paths. 140 141 Raises: 142 errors.CheckPathError: Can't find log files. 143 """ 144 log_files = GetAllLogFilePaths(ssh) 145 if file_name: 146 file_path = os.path.join(_REMOTE_LOG_FOLDER, file_name) 147 if file_path in log_files: 148 return [file_path] 149 raise errors.CheckPathError("Can't find this log file(%s) from remote " 150 "instance." % file_path) 151 152 if len(log_files) == 1: 153 return log_files 154 155 if len(log_files) > 1: 156 print("Multiple log files detected, choose any one to proceed:") 157 return utils.GetAnswerFromList(log_files, enable_choose_all=True) 158 159 raise errors.CheckPathError("Can't find any log file in folder(%s) from " 160 "remote instance." % _REMOTE_LOG_FOLDER) 161 162 163def GetAllLogFilePaths(ssh): 164 """Get the file paths of all log files. 165 166 Args: 167 ssh: Ssh object. 168 169 Returns: 170 List of all log file paths. 171 """ 172 ssh_cmd = [ssh.GetBaseCmd(constants.SSH_BIN), _FIND_LOG_FILE_CMD] 173 log_files = [] 174 try: 175 files_output = subprocess.check_output(" ".join(ssh_cmd), shell=True) 176 log_files = FilterLogfiles(files_output.splitlines()) 177 except subprocess.CalledProcessError: 178 logger.debug("The folder(%s) that running launch_cvd doesn't exist.", 179 _REMOTE_LOG_FOLDER) 180 return log_files 181 182 183def FilterLogfiles(files): 184 """Filter some unused files. 185 186 Two rules to filter out files. 187 1. File name is "kernel". 188 2. File type is image "*.img". 189 190 Args: 191 files: List of file paths in the remote instance. 192 193 Return: 194 List of log files. 195 """ 196 log_files = list(files) 197 for file_path in files: 198 file_name = os.path.basename(file_path) 199 if file_name == _KERNEL or file_name.endswith(_IMG_FILE_EXTENSION): 200 log_files.remove(file_path) 201 return log_files 202 203 204def Run(args): 205 """Run pull. 206 207 After pull command executed, tool will return one Report instance. 208 If there is no instance to pull, just return empty Report. 209 210 Args: 211 args: Namespace object from argparse.parse_args. 212 213 Returns: 214 A Report instance. 215 """ 216 cfg = config.GetAcloudConfig(args) 217 if args.instance_name: 218 instance = list_instances.GetInstancesFromInstanceNames( 219 cfg, [args.instance_name]) 220 return PullFileFromInstance(cfg, instance[0], args.file_name, args.no_prompt) 221 return PullFileFromInstance(cfg, 222 list_instances.ChooseOneRemoteInstance(cfg), 223 args.file_name, 224 args.no_prompt) 225