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 AddError(self, error): 124 """Add error message. 125 126 Args: 127 error: A string. 128 """ 129 self.errors.append(error) 130 131 def AddErrors(self, errors): 132 """Add a list of error messages. 133 134 Args: 135 errors: A list of string. 136 """ 137 self.errors.extend(errors) 138 139 def SetStatus(self, status): 140 """Set status. 141 142 Args: 143 status: One of the status in Status. 144 """ 145 if Status.IsMoreSevere(status, self.status): 146 self.status = status 147 else: 148 logger.debug( 149 "report: Current status is %s, " 150 "requested to update to a status with lower severity %s, ignored.", 151 self.status, status) 152 153 def AddDevice(self, instance_name, ip_address, adb_port, vnc_port, 154 key="devices"): 155 """Add a record of a device. 156 157 Args: 158 instance_name: A string. 159 ip_address: A string. 160 adb_port: An integer. 161 vnc_port: An integer. 162 key: A string, the data entry where the record is added. 163 """ 164 device = {constants.INSTANCE_NAME: instance_name} 165 if adb_port: 166 device[constants.ADB_PORT] = adb_port 167 device[constants.IP] = "%s:%d" % (ip_address, adb_port) 168 else: 169 device[constants.IP] = ip_address 170 171 if vnc_port: 172 device[constants.VNC_PORT] = vnc_port 173 self.AddData(key=key, value=device) 174 175 def AddDeviceBootFailure(self, instance_name, ip_address, adb_port, 176 vnc_port, error): 177 """Add a record of device boot failure. 178 179 Args: 180 instance_name: A string. 181 ip_address: A string. 182 adb_port: An integer. 183 vnc_port: An integer. Can be None if the device doesn't support it. 184 error: A string, the error message. 185 """ 186 self.AddDevice(instance_name, ip_address, adb_port, vnc_port, 187 "devices_failing_boot") 188 self.AddError(error) 189 190 def Dump(self, report_file): 191 """Dump report content to a file. 192 193 Args: 194 report_file: A path to a file where result will be dumped to. 195 If None, will only output result as logs. 196 """ 197 result = dict( 198 command=self.command, 199 status=self.status, 200 errors=self.errors, 201 data=self.data) 202 logger.info("Report: %s", json.dumps(result, indent=2, sort_keys=True)) 203 if not report_file: 204 return 205 try: 206 with open(report_file, "w") as f: 207 json.dump(result, f, indent=2, sort_keys=True) 208 logger.info("Report file generated at %s", 209 os.path.abspath(report_file)) 210 except OSError as e: 211 logger.error("Failed to dump report to file: %s", str(e)) 212