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