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 62logger = logging.getLogger(__name__) 63 64 65class Status(object): 66 """Status of acloud command.""" 67 68 SUCCESS = "SUCCESS" 69 FAIL = "FAIL" 70 BOOT_FAIL = "BOOT_FAIL" 71 UNKNOWN = "UNKNOWN" 72 73 SEVERITY_ORDER = {UNKNOWN: 0, SUCCESS: 1, FAIL: 2, BOOT_FAIL: 3} 74 75 @classmethod 76 def IsMoreSevere(cls, candidate, reference): 77 """Compare the severity of two statuses. 78 79 Args: 80 candidate: One of the statuses. 81 reference: One of the statuses. 82 83 Returns: 84 True if candidate is more severe than reference, 85 False otherwise. 86 87 Raises: 88 ValueError: if candidate or reference is not a known state. 89 """ 90 if (candidate not in cls.SEVERITY_ORDER or 91 reference not in cls.SEVERITY_ORDER): 92 raise ValueError( 93 "%s or %s is not recognized." % (candidate, reference)) 94 return cls.SEVERITY_ORDER[candidate] > cls.SEVERITY_ORDER[reference] 95 96 97class Report(object): 98 """A class that stores and generates report.""" 99 100 def __init__(self, command): 101 """Initialize. 102 103 Args: 104 command: A string, name of the command. 105 """ 106 self.command = command 107 self.status = Status.UNKNOWN 108 self.errors = [] 109 self.data = {} 110 111 def AddData(self, key, value): 112 """Add a key-val to the report. 113 114 Args: 115 key: A key of basic type. 116 value: A value of any json compatible type. 117 """ 118 self.data.setdefault(key, []).append(value) 119 120 def AddError(self, error): 121 """Add error message. 122 123 Args: 124 error: A string. 125 """ 126 self.errors.append(error) 127 128 def AddErrors(self, errors): 129 """Add a list of error messages. 130 131 Args: 132 errors: A list of string. 133 """ 134 self.errors.extend(errors) 135 136 def SetStatus(self, status): 137 """Set status. 138 139 Args: 140 status: One of the status in Status. 141 """ 142 if Status.IsMoreSevere(status, self.status): 143 self.status = status 144 else: 145 logger.debug( 146 "report: Current status is %s, " 147 "requested to update to a status with lower severity %s, ignored.", 148 self.status, status) 149 150 def Dump(self, report_file): 151 """Dump report content to a file. 152 153 Args: 154 report_file: A path to a file where result will be dumped to. 155 If None, will only output result as logs. 156 """ 157 result = dict( 158 command=self.command, 159 status=self.status, 160 errors=self.errors, 161 data=self.data) 162 logger.info("Report: %s", json.dumps(result, indent=2, sort_keys=True)) 163 if not report_file: 164 return 165 try: 166 with open(report_file, "w") as f: 167 json.dump(result, f, indent=2, sort_keys=True) 168 logger.info("Report file generated at %s", 169 os.path.abspath(report_file)) 170 except OSError as e: 171 logger.error("Failed to dump report to file: %s", str(e)) 172