1#!/usr/bin/env python 2# 3# Copyright 2016 - The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17"""Command report. 18 19Report class holds the results of a command execution. 20Each driver API call will generate a report instance. 21 22If running the CLI of the driver, a report will 23be printed as logs. And it will also be dumped to a json file 24if requested via command line option. 25 26The json format of a report dump looks like: 27 28 - A failed "delete" command: 29 { 30 "command": "delete", 31 "data": {}, 32 "errors": [ 33 "Can't find instances: ['104.197.110.255']" 34 ], 35 "error_type": "error_type_1", 36 "status": "FAIL" 37 } 38 39 - A successful "create" command: 40 { 41 "command": "create", 42 "data": { 43 "devices": [ 44 { 45 "instance_name": "instance_1", 46 "ip": "104.197.62.36" 47 }, 48 { 49 "instance_name": "instance_2", 50 "ip": "104.197.62.37" 51 } 52 ] 53 }, 54 "errors": [], 55 "status": "SUCCESS" 56 } 57""" 58 59import json 60import logging 61import os 62 63from acloud.internal import constants 64 65 66logger = logging.getLogger(__name__) 67 68 69class Status(): 70 """Status of acloud command.""" 71 72 SUCCESS = "SUCCESS" 73 FAIL = "FAIL" 74 BOOT_FAIL = "BOOT_FAIL" 75 UNKNOWN = "UNKNOWN" 76 77 SEVERITY_ORDER = {UNKNOWN: 0, SUCCESS: 1, FAIL: 2, BOOT_FAIL: 3} 78 79 @classmethod 80 def IsMoreSevere(cls, candidate, reference): 81 """Compare the severity of two statuses. 82 83 Args: 84 candidate: One of the statuses. 85 reference: One of the statuses. 86 87 Returns: 88 True if candidate is more severe than reference, 89 False otherwise. 90 91 Raises: 92 ValueError: if candidate or reference is not a known state. 93 """ 94 if (candidate not in cls.SEVERITY_ORDER or 95 reference not in cls.SEVERITY_ORDER): 96 raise ValueError( 97 "%s or %s is not recognized." % (candidate, reference)) 98 return cls.SEVERITY_ORDER[candidate] > cls.SEVERITY_ORDER[reference] 99 100 101def LogFile(path, log_type, name=None): 102 """Create a log entry that can be added to the report. 103 104 Args: 105 path: A string, the local or remote path to the log file. 106 log_type: A string, the type of the log file. 107 name: A string, the optional entry name. 108 109 Returns: 110 The log entry as a dictionary. 111 """ 112 log = {"path": path, "type": log_type} 113 if name: 114 log["name"] = name 115 return log 116 117 118class Report(): 119 """A class that stores and generates report.""" 120 121 def __init__(self, command): 122 """Initialize. 123 124 Args: 125 command: A string, name of the command. 126 """ 127 self.command = command 128 self.status = Status.UNKNOWN 129 self.errors = [] 130 self.error_type = "" 131 self.data = {} 132 133 def AddData(self, key, value): 134 """Add a key-val to the report. 135 136 Args: 137 key: A key of basic type. 138 value: A value of any json compatible type. 139 """ 140 self.data.setdefault(key, []).append(value) 141 142 def UpdateData(self, dict_data): 143 """Update a dict data to the report. 144 145 Args: 146 dict_data: A dict of report data. 147 """ 148 self.data.update(dict_data) 149 150 def AddError(self, error): 151 """Add error message. 152 153 Args: 154 error: A string. 155 """ 156 self.errors.append(error) 157 158 def AddErrors(self, errors): 159 """Add a list of error messages. 160 161 Args: 162 errors: A list of string. 163 """ 164 self.errors.extend(errors) 165 166 def SetErrorType(self, error_type): 167 """Set error type. 168 169 Args: 170 error_type: String of error type. 171 """ 172 self.error_type = error_type 173 174 def SetStatus(self, status): 175 """Set status. 176 177 Args: 178 status: One of the status in Status. 179 """ 180 if Status.IsMoreSevere(status, self.status): 181 self.status = status 182 else: 183 logger.debug( 184 "report: Current status is %s, " 185 "requested to update to a status with lower severity %s, ignored.", 186 self.status, status) 187 188 def AddDevice(self, instance_name, ip_address, adb_port, vnc_port, 189 webrtc_port=None, device_serial=None, logs=None, 190 key="devices", update_data=None): 191 """Add a record of a device. 192 193 Args: 194 instance_name: A string. 195 ip_address: A string. 196 adb_port: An integer. 197 vnc_port: An integer. 198 webrtc_port: An integer, the port to display device screen. 199 device_serial: String of device serial. 200 logs: A list of LogFile. 201 key: A string, the data entry where the record is added. 202 update_data: A dict to update device data. 203 """ 204 device = {constants.INSTANCE_NAME: instance_name} 205 if adb_port: 206 device[constants.ADB_PORT] = adb_port 207 device[constants.IP] = "%s:%d" % (ip_address, adb_port) 208 else: 209 device[constants.IP] = ip_address 210 211 if device_serial: 212 device[constants.DEVICE_SERIAL] = device_serial 213 214 if vnc_port: 215 device[constants.VNC_PORT] = vnc_port 216 217 if webrtc_port: 218 device[constants.WEBRTC_PORT] = webrtc_port 219 220 if logs: 221 device[constants.LOGS] = logs 222 223 if update_data: 224 device.update(update_data) 225 self.AddData(key=key, value=device) 226 227 def AddDeviceBootFailure(self, instance_name, ip_address, adb_port, 228 vnc_port, error, device_serial=None, 229 webrtc_port=None, logs=None): 230 """Add a record of device boot failure. 231 232 Args: 233 instance_name: A string. 234 ip_address: A string. 235 adb_port: An integer. 236 vnc_port: An integer. Can be None if the device doesn't support it. 237 error: A string, the error message. 238 device_serial: String of device serial. 239 webrtc_port: An integer. 240 logs: A list of LogFile. 241 """ 242 self.AddDevice(instance_name, ip_address, adb_port, vnc_port, 243 webrtc_port, device_serial, logs, 244 "devices_failing_boot") 245 self.AddError(error) 246 247 def UpdateFailure(self, error, error_type=None): 248 """Update the falure information of report. 249 250 Args: 251 error: String, the error message. 252 error_type: String, the error type. 253 """ 254 self.AddError(error) 255 self.SetStatus(Status.FAIL) 256 if error_type: 257 self.SetErrorType(error_type) 258 259 def Dump(self, report_file): 260 """Dump report content to a file. 261 262 Args: 263 report_file: A path to a file where result will be dumped to. 264 If None, will only output result as logs. 265 """ 266 result = dict( 267 command=self.command, 268 status=self.status, 269 errors=self.errors, 270 error_type=self.error_type, 271 data=self.data) 272 logger.info("Report: %s", json.dumps(result, indent=2, sort_keys=True)) 273 if not report_file: 274 return 275 try: 276 with open(report_file, "w") as f: 277 json.dump(result, f, indent=2, sort_keys=True) 278 logger.info("Report file generated at %s", 279 os.path.abspath(report_file)) 280 except OSError as e: 281 logger.error("Failed to dump report to file: %s", str(e)) 282