• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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