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 and the log files can be symbolic links. The -H flag makes 38# the command skip the links except REMOTE_LOG_FOLDER. The returned logs are 39# unique. 40_FIND_LOG_FILE_CMD = "find -H %s -type f" % constants.REMOTE_LOG_FOLDER 41# Black list for log files. 42_KERNEL = "kernel" 43_IMG_FILE_EXTENSION = ".img" 44 45 46def PullFileFromInstance(cfg, instance, file_name=None, no_prompts=False): 47 """Pull file from remote CF instance. 48 49 1. Download log files to temp folder. 50 2. If only one file selected, display it on screen. 51 3. Show the download folder for users. 52 53 Args: 54 cfg: AcloudConfig object. 55 instance: list.Instance() object. 56 file_name: String of file name. 57 no_prompts: Boolean, True to skip the prompt about file streaming. 58 59 Returns: 60 A Report instance. 61 """ 62 ssh = Ssh(ip=IP(ip=instance.ip), 63 user=constants.GCE_USER, 64 ssh_private_key_path=cfg.ssh_private_key_path, 65 extra_args_ssh_tunnel=cfg.extra_args_ssh_tunnel) 66 log_files = SelectLogFileToPull(ssh, file_name) 67 PullLogs(ssh, log_files, instance.name) 68 if len(log_files) == 1: 69 DisplayLog(ssh, log_files[0], no_prompts) 70 return report.Report(command="pull") 71 72 73def PullLogs(ssh, log_files, instance_name): 74 """Pull log files from remote instance. 75 76 Args: 77 ssh: Ssh object. 78 log_files: List of file path in the remote instance. 79 instance_name: The instance name that is used to create the download 80 folder. 81 82 Returns: 83 The download folder path. 84 """ 85 download_folder = _GetDownloadLogFolder(instance_name) 86 for log_file in log_files: 87 target_file = os.path.join(download_folder, os.path.basename(log_file)) 88 ssh.ScpPullFile(log_file, target_file) 89 _DisplayPullResult(download_folder) 90 return download_folder 91 92 93def DisplayLog(ssh, log_file, no_prompts=False): 94 """Display the content of log file in the screen. 95 96 Args: 97 ssh: Ssh object. 98 log_file: String of the log file path. 99 no_prompts: Boolean, True to skip all prompts. 100 """ 101 warning_msg = ("It will stream log to show on screen. If you want to stop " 102 "streaming, please press CTRL-C to exit.\nPress 'y' to show " 103 "log or read log by myself[y/N]:") 104 if no_prompts or utils.GetUserAnswerYes(warning_msg): 105 ssh.Run("tail -f -n +1 %s" % log_file, show_output=True) 106 107 108def _DisplayPullResult(download_folder): 109 """Display messages to user after pulling log files. 110 111 Args: 112 download_folder: String of download folder path. 113 """ 114 utils.PrintColorString( 115 "Download logs to folder: %s \nYou can look into log files to check " 116 "AVD issues." % download_folder) 117 118 119def _GetDownloadLogFolder(instance): 120 """Get the download log folder accroding to instance name. 121 122 Args: 123 instance: String, the name of instance. 124 125 Returns: 126 String of the download folder path. 127 """ 128 log_folder = os.path.join(tempfile.gettempdir(), instance) 129 if not os.path.exists(log_folder): 130 os.makedirs(log_folder) 131 logger.info("Download logs to folder: %s", log_folder) 132 return log_folder 133 134 135def SelectLogFileToPull(ssh, file_name=None): 136 """Select one log file or all log files to downalod. 137 138 1. Get all log file paths as selection list 139 2. Get user selected file path or user provided file name. 140 141 Args: 142 ssh: Ssh object. 143 file_name: String of file name. 144 145 Returns: 146 List of selected file paths. 147 148 Raises: 149 errors.CheckPathError: Can't find log files. 150 """ 151 log_files = GetAllLogFilePaths(ssh) 152 if file_name: 153 file_path = os.path.join(constants.REMOTE_LOG_FOLDER, file_name) 154 if file_path in log_files: 155 return [file_path] 156 raise errors.CheckPathError("Can't find this log file(%s) from remote " 157 "instance." % file_path) 158 159 if len(log_files) == 1: 160 return log_files 161 162 if len(log_files) > 1: 163 print("Multiple log files detected, choose any one to proceed:") 164 return utils.GetAnswerFromList(log_files, enable_choose_all=True) 165 166 raise errors.CheckPathError("Can't find any log file in folder(%s) from " 167 "remote instance." % constants.REMOTE_LOG_FOLDER) 168 169 170def GetAllLogFilePaths(ssh): 171 """Get the file paths of all log files. 172 173 Args: 174 ssh: Ssh object. 175 176 Returns: 177 List of all log file paths. 178 """ 179 ssh_cmd = [ssh.GetBaseCmd(constants.SSH_BIN), _FIND_LOG_FILE_CMD] 180 log_files = [] 181 try: 182 files_output = utils.CheckOutput(" ".join(ssh_cmd), shell=True) 183 log_files = FilterLogfiles(files_output.splitlines()) 184 except subprocess.CalledProcessError: 185 logger.debug("The folder(%s) that running launch_cvd doesn't exist.", 186 constants.REMOTE_LOG_FOLDER) 187 return log_files 188 189 190def FilterLogfiles(files): 191 """Filter some unused files. 192 193 Two rules to filter out files. 194 1. File name is "kernel". 195 2. File type is image "*.img". 196 197 Args: 198 files: List of file paths in the remote instance. 199 200 Return: 201 List of log files. 202 """ 203 log_files = list(files) 204 for file_path in files: 205 file_name = os.path.basename(file_path) 206 if file_name == _KERNEL or file_name.endswith(_IMG_FILE_EXTENSION): 207 log_files.remove(file_path) 208 return log_files 209 210 211def Run(args): 212 """Run pull. 213 214 After pull command executed, tool will return one Report instance. 215 If there is no instance to pull, just return empty Report. 216 217 Args: 218 args: Namespace object from argparse.parse_args. 219 220 Returns: 221 A Report instance. 222 """ 223 cfg = config.GetAcloudConfig(args) 224 if args.instance_name: 225 instance = list_instances.GetInstancesFromInstanceNames( 226 cfg, [args.instance_name]) 227 return PullFileFromInstance(cfg, instance[0], args.file_name, args.no_prompt) 228 return PullFileFromInstance(cfg, 229 list_instances.ChooseOneRemoteInstance(cfg), 230 args.file_name, 231 args.no_prompt) 232