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