• 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
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