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 "status": "FAIL" 36 } 37 38 - A successful "create" command: 39 { 40 "command": "create", 41 "data": { 42 "devices": [ 43 { 44 "instance_name": "instance_1", 45 "ip": "104.197.62.36" 46 }, 47 { 48 "instance_name": "instance_2", 49 "ip": "104.197.62.37" 50 } 51 ] 52 }, 53 "errors": [], 54 "status": "SUCCESS" 55 } 56""" 57 58import json 59import logging 60import os 61 62from acloud.internal import constants 63 64 65logger = logging.getLogger(__name__) 66 67 68class Status(object): 69 """Status of acloud command.""" 70 71 SUCCESS = "SUCCESS" 72 FAIL = "FAIL" 73 BOOT_FAIL = "BOOT_FAIL" 74 UNKNOWN = "UNKNOWN" 75 76 SEVERITY_ORDER = {UNKNOWN: 0, SUCCESS: 1, FAIL: 2, BOOT_FAIL: 3} 77 78 @classmethod 79 def IsMoreSevere(cls, candidate, reference): 80 """Compare the severity of two statuses. 81 82 Args: 83 candidate: One of the statuses. 84 reference: One of the statuses. 85 86 Returns: 87 True if candidate is more severe than reference, 88 False otherwise. 89 90 Raises: 91 ValueError: if candidate or reference is not a known state. 92 """ 93 if (candidate not in cls.SEVERITY_ORDER or 94 reference not in cls.SEVERITY_ORDER): 95 raise ValueError( 96 "%s or %s is not recognized." % (candidate, reference)) 97 return cls.SEVERITY_ORDER[candidate] > cls.SEVERITY_ORDER[reference] 98 99 100class Report(object): 101 """A class that stores and generates report.""" 102 103 def __init__(self, command): 104 """Initialize. 105 106 Args: 107 command: A string, name of the command. 108 """ 109 self.command = command 110 self.status = Status.UNKNOWN 111 self.errors = [] 112 self.data = {} 113 114 def AddData(self, key, value): 115 """Add a key-val to the report. 116 117 Args: 118 key: A key of basic type. 119 value: A value of any json compatible type. 120 """ 121 self.data.setdefault(key, []).append(value) 122 123 def UpdateData(self, dict_data): 124 """Update a dict data to the report. 125 126 Args: 127 dict_data: A dict of report data. 128 """ 129 self.data.update(dict_data) 130 131 def AddError(self, error): 132 """Add error message. 133 134 Args: 135 error: A string. 136 """ 137 self.errors.append(error) 138 139 def AddErrors(self, errors): 140 """Add a list of error messages. 141 142 Args: 143 errors: A list of string. 144 """ 145 self.errors.extend(errors) 146 147 def SetStatus(self, status): 148 """Set status. 149 150 Args: 151 status: One of the status in Status. 152 """ 153 if Status.IsMoreSevere(status, self.status): 154 self.status = status 155 else: 156 logger.debug( 157 "report: Current status is %s, " 158 "requested to update to a status with lower severity %s, ignored.", 159 self.status, status) 160 161 def AddDevice(self, instance_name, ip_address, adb_port, vnc_port, 162 key="devices"): 163 """Add a record of a device. 164 165 Args: 166 instance_name: A string. 167 ip_address: A string. 168 adb_port: An integer. 169 vnc_port: An integer. 170 key: A string, the data entry where the record is added. 171 """ 172 device = {constants.INSTANCE_NAME: instance_name} 173 if adb_port: 174 device[constants.ADB_PORT] = adb_port 175 device[constants.IP] = "%s:%d" % (ip_address, adb_port) 176 else: 177 device[constants.IP] = ip_address 178 179 if vnc_port: 180 device[constants.VNC_PORT] = vnc_port 181 self.AddData(key=key, value=device) 182 183 def AddDeviceBootFailure(self, instance_name, ip_address, adb_port, 184 vnc_port, error): 185 """Add a record of device boot failure. 186 187 Args: 188 instance_name: A string. 189 ip_address: A string. 190 adb_port: An integer. 191 vnc_port: An integer. Can be None if the device doesn't support it. 192 error: A string, the error message. 193 """ 194 self.AddDevice(instance_name, ip_address, adb_port, vnc_port, 195 "devices_failing_boot") 196 self.AddError(error) 197 198 def Dump(self, report_file): 199 """Dump report content to a file. 200 201 Args: 202 report_file: A path to a file where result will be dumped to. 203 If None, will only output result as logs. 204 """ 205 result = dict( 206 command=self.command, 207 status=self.status, 208 errors=self.errors, 209 data=self.data) 210 logger.info("Report: %s", json.dumps(result, indent=2, sort_keys=True)) 211 if not report_file: 212 return 213 try: 214 with open(report_file, "w") as f: 215 json.dump(result, f, indent=2, sort_keys=True) 216 logger.info("Report file generated at %s", 217 os.path.abspath(report_file)) 218 except OSError as e: 219 logger.error("Failed to dump report to file: %s", str(e)) 220