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 101class Report(): 102 """A class that stores and generates report.""" 103 104 def __init__(self, command): 105 """Initialize. 106 107 Args: 108 command: A string, name of the command. 109 """ 110 self.command = command 111 self.status = Status.UNKNOWN 112 self.errors = [] 113 self.error_type = "" 114 self.data = {} 115 116 def AddData(self, key, value): 117 """Add a key-val to the report. 118 119 Args: 120 key: A key of basic type. 121 value: A value of any json compatible type. 122 """ 123 self.data.setdefault(key, []).append(value) 124 125 def UpdateData(self, dict_data): 126 """Update a dict data to the report. 127 128 Args: 129 dict_data: A dict of report data. 130 """ 131 self.data.update(dict_data) 132 133 def AddError(self, error): 134 """Add error message. 135 136 Args: 137 error: A string. 138 """ 139 self.errors.append(error) 140 141 def AddErrors(self, errors): 142 """Add a list of error messages. 143 144 Args: 145 errors: A list of string. 146 """ 147 self.errors.extend(errors) 148 149 def SetErrorType(self, error_type): 150 """Set error type. 151 152 Args: 153 error_type: String of error type. 154 """ 155 self.error_type = error_type 156 157 def SetStatus(self, status): 158 """Set status. 159 160 Args: 161 status: One of the status in Status. 162 """ 163 if Status.IsMoreSevere(status, self.status): 164 self.status = status 165 else: 166 logger.debug( 167 "report: Current status is %s, " 168 "requested to update to a status with lower severity %s, ignored.", 169 self.status, status) 170 171 def AddDevice(self, instance_name, ip_address, adb_port, vnc_port, 172 webrtc_port=None, device_serial=None, key="devices"): 173 """Add a record of a device. 174 175 Args: 176 instance_name: A string. 177 ip_address: A string. 178 adb_port: An integer. 179 vnc_port: An integer. 180 webrtc_port: An integer, the port to display device screen. 181 device_serial: String of device serial. 182 key: A string, the data entry where the record is added. 183 """ 184 device = {constants.INSTANCE_NAME: instance_name} 185 if adb_port: 186 device[constants.ADB_PORT] = adb_port 187 device[constants.IP] = "%s:%d" % (ip_address, adb_port) 188 else: 189 device[constants.IP] = ip_address 190 191 if device_serial: 192 device[constants.DEVICE_SERIAL] = device_serial 193 194 if vnc_port: 195 device[constants.VNC_PORT] = vnc_port 196 197 if webrtc_port: 198 device[constants.WEBRTC_PORT] = webrtc_port 199 self.AddData(key=key, value=device) 200 201 def AddDeviceBootFailure(self, instance_name, ip_address, adb_port, 202 vnc_port, error, device_serial=None, 203 webrtc_port=None): 204 """Add a record of device boot failure. 205 206 Args: 207 instance_name: A string. 208 ip_address: A string. 209 adb_port: An integer. 210 vnc_port: An integer. Can be None if the device doesn't support it. 211 error: A string, the error message. 212 device_serial: String of device serial. 213 webrtc_port: An integer. 214 """ 215 self.AddDevice(instance_name, ip_address, adb_port, vnc_port, 216 webrtc_port, device_serial, "devices_failing_boot") 217 self.AddError(error) 218 219 def UpdateFailure(self, error, error_type=None): 220 """Update the falure information of report. 221 222 Args: 223 error: String, the error message. 224 error_type: String, the error type. 225 """ 226 self.AddError(error) 227 self.SetStatus(Status.FAIL) 228 if error_type: 229 self.SetErrorType(error_type) 230 231 def Dump(self, report_file): 232 """Dump report content to a file. 233 234 Args: 235 report_file: A path to a file where result will be dumped to. 236 If None, will only output result as logs. 237 """ 238 result = dict( 239 command=self.command, 240 status=self.status, 241 errors=self.errors, 242 error_type=self.error_type, 243 data=self.data) 244 logger.info("Report: %s", json.dumps(result, indent=2, sort_keys=True)) 245 if not report_file: 246 return 247 try: 248 with open(report_file, "w") as f: 249 json.dump(result, f, indent=2, sort_keys=True) 250 logger.info("Report file generated at %s", 251 os.path.abspath(report_file)) 252 except OSError as e: 253 logger.error("Failed to dump report to file: %s", str(e)) 254