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("%s or %s is not recognized." %
93                             (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(command=self.command,
158                      status=self.status,
159                      errors=self.errors,
160                      data=self.data)
161        logger.info("Report: %s", json.dumps(result, indent=2))
162        if not report_file:
163            return
164        try:
165            with open(report_file, "w") as f:
166                json.dump(result, f, indent=2)
167            logger.info("Report file generated at %s",
168                        os.path.abspath(report_file))
169        except OSError as e:
170            logger.error("Failed to dump report to file: %s", str(e))
171