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