Dennis Dmitriev | e56c8b9 | 2017-06-16 01:53:16 +0300 | [diff] [blame] | 1 | # Copyright 2016 Mirantis, Inc. |
| 2 | # |
| 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may |
| 4 | # not use this file except in compliance with the License. You may obtain |
| 5 | # a copy of the License at |
| 6 | # |
| 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | # |
| 9 | # Unless required by applicable law or agreed to in writing, software |
| 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| 12 | # License for the specific language governing permissions and limitations |
| 13 | # under the License. |
| 14 | |
| 15 | from __future__ import unicode_literals |
| 16 | |
| 17 | import json |
| 18 | import threading |
| 19 | |
| 20 | import yaml |
| 21 | |
| 22 | from reclass_tools.helpers import proc_enums |
| 23 | from reclass_tools import logger |
| 24 | |
| 25 | |
| 26 | deprecated_aliases = { |
| 27 | 'stdout_str', |
| 28 | 'stderr_str', |
| 29 | 'stdout_json', |
| 30 | 'stdout_yaml' |
| 31 | } |
| 32 | |
| 33 | |
| 34 | class ExecResult(object): |
| 35 | __slots__ = [ |
| 36 | '__cmd', '__stdout', '__stderr', '__exit_code', |
| 37 | '__stdout_str', '__stderr_str', '__stdout_brief', '__stderr_brief', |
| 38 | '__stdout_json', '__stdout_yaml', |
| 39 | '__lock' |
| 40 | ] |
| 41 | |
| 42 | def __init__(self, cmd, stdout=None, stderr=None, |
| 43 | exit_code=proc_enums.ExitCodes.EX_INVALID): |
| 44 | """Command execution result read from fifo |
| 45 | |
| 46 | :type cmd: str |
| 47 | :type stdout: list |
| 48 | :type stderr: list |
| 49 | :type exit_code: ExitCodes |
| 50 | """ |
| 51 | self.__lock = threading.RLock() |
| 52 | |
| 53 | self.__cmd = cmd |
| 54 | self.__stdout = stdout if stdout is not None else [] |
| 55 | self.__stderr = stderr if stderr is not None else [] |
| 56 | |
| 57 | self.__exit_code = None |
| 58 | self.exit_code = exit_code |
| 59 | |
| 60 | # By default is none: |
| 61 | self.__stdout_str = None |
| 62 | self.__stderr_str = None |
| 63 | self.__stdout_brief = None |
| 64 | self.__stderr_brief = None |
| 65 | |
| 66 | self.__stdout_json = None |
| 67 | self.__stdout_yaml = None |
| 68 | |
| 69 | @property |
| 70 | def lock(self): |
| 71 | """Lock object for thread-safe operation |
| 72 | |
| 73 | :rtype: RLock |
| 74 | """ |
| 75 | return self.__lock |
| 76 | |
| 77 | @staticmethod |
| 78 | def _get_bytearray_from_array(src): |
| 79 | """Get bytearray from array of bytes blocks |
| 80 | |
| 81 | :type src: list(bytes) |
| 82 | :rtype: bytearray |
| 83 | """ |
| 84 | return bytearray(b''.join(src)) |
| 85 | |
| 86 | @staticmethod |
| 87 | def _get_str_from_bin(src): |
| 88 | """Join data in list to the string, with python 2&3 compatibility. |
| 89 | |
| 90 | :type src: bytearray |
| 91 | :rtype: str |
| 92 | """ |
| 93 | return src.strip().decode( |
| 94 | encoding='utf-8', |
| 95 | errors='backslashreplace' |
| 96 | ) |
| 97 | |
| 98 | @classmethod |
| 99 | def _get_brief(cls, data): |
| 100 | """Get brief output: 7 lines maximum (3 first + ... + 3 last) |
| 101 | |
| 102 | :type data: list(bytes) |
| 103 | :rtype: str |
| 104 | """ |
| 105 | src = data if len(data) <= 7 else data[:3] + [b'...\n'] + data[-3:] |
| 106 | return cls._get_str_from_bin( |
| 107 | cls._get_bytearray_from_array(src) |
| 108 | ) |
| 109 | |
| 110 | @property |
| 111 | def cmd(self): |
| 112 | """Executed command |
| 113 | |
| 114 | :rtype: str |
| 115 | """ |
| 116 | return self.__cmd |
| 117 | |
| 118 | @property |
| 119 | def stdout(self): |
| 120 | """Stdout output as list of binaries |
| 121 | |
| 122 | :rtype: list(bytes) |
| 123 | """ |
| 124 | return self.__stdout |
| 125 | |
| 126 | @stdout.setter |
| 127 | def stdout(self, new_val): |
| 128 | """Stdout output as list of binaries |
| 129 | |
| 130 | :type new_val: list(bytes) |
| 131 | :raises: TypeError |
| 132 | """ |
| 133 | if not isinstance(new_val, (list, type(None))): |
| 134 | raise TypeError('stdout should be list only!') |
| 135 | with self.lock: |
| 136 | self.__stdout_str = None |
| 137 | self.__stdout_brief = None |
| 138 | self.__stdout_json = None |
| 139 | self.__stdout_yaml = None |
| 140 | self.__stdout = new_val |
| 141 | |
| 142 | @property |
| 143 | def stderr(self): |
| 144 | """Stderr output as list of binaries |
| 145 | |
| 146 | :rtype: list(bytes) |
| 147 | """ |
| 148 | return self.__stderr |
| 149 | |
| 150 | @stderr.setter |
| 151 | def stderr(self, new_val): |
| 152 | """Stderr output as list of binaries |
| 153 | |
| 154 | :type new_val: list(bytes) |
| 155 | :raises: TypeError |
| 156 | """ |
| 157 | if not isinstance(new_val, (list, None)): |
| 158 | raise TypeError('stderr should be list only!') |
| 159 | with self.lock: |
| 160 | self.__stderr_str = None |
| 161 | self.__stderr_brief = None |
| 162 | self.__stderr = new_val |
| 163 | |
| 164 | @property |
| 165 | def stdout_bin(self): |
| 166 | """Stdout in binary format |
| 167 | |
| 168 | Sometimes logging is used to log binary objects too (example: Session), |
| 169 | and for debug purposes we can use this as data source. |
| 170 | :rtype: bytearray |
| 171 | """ |
| 172 | with self.lock: |
| 173 | return self._get_bytearray_from_array(self.stdout) |
| 174 | |
| 175 | @property |
| 176 | def stderr_bin(self): |
| 177 | """Stderr in binary format |
| 178 | |
| 179 | :rtype: bytearray |
| 180 | """ |
| 181 | with self.lock: |
| 182 | return self._get_bytearray_from_array(self.stderr) |
| 183 | |
| 184 | @property |
| 185 | def stdout_str(self): |
| 186 | """Stdout output as string |
| 187 | |
| 188 | :rtype: str |
| 189 | """ |
| 190 | with self.lock: |
| 191 | if self.__stdout_str is None: |
| 192 | self.__stdout_str = self._get_str_from_bin(self.stdout_bin) |
| 193 | return self.__stdout_str |
| 194 | |
| 195 | @property |
| 196 | def stderr_str(self): |
| 197 | """Stderr output as string |
| 198 | |
| 199 | :rtype: str |
| 200 | """ |
| 201 | with self.lock: |
| 202 | if self.__stderr_str is None: |
| 203 | self.__stderr_str = self._get_str_from_bin(self.stderr_bin) |
| 204 | return self.__stderr_str |
| 205 | |
| 206 | @property |
| 207 | def stdout_brief(self): |
| 208 | """Brief stdout output (mostly for exceptions) |
| 209 | |
| 210 | :rtype: str |
| 211 | """ |
| 212 | with self.lock: |
| 213 | if self.__stdout_brief is None: |
| 214 | self.__stdout_brief = self._get_brief(self.stdout) |
| 215 | return self.__stdout_brief |
| 216 | |
| 217 | @property |
| 218 | def stderr_brief(self): |
| 219 | """Brief stderr output (mostly for exceptions) |
| 220 | |
| 221 | :rtype: str |
| 222 | """ |
| 223 | with self.lock: |
| 224 | if self.__stderr_brief is None: |
| 225 | self.__stderr_brief = self._get_brief(self.stderr) |
| 226 | return self.__stderr_brief |
| 227 | |
| 228 | @property |
| 229 | def exit_code(self): |
| 230 | """Return(exit) code of command |
| 231 | |
| 232 | :rtype: int |
| 233 | """ |
| 234 | return self.__exit_code |
| 235 | |
| 236 | @exit_code.setter |
| 237 | def exit_code(self, new_val): |
| 238 | """Return(exit) code of command |
| 239 | |
| 240 | :type new_val: int |
| 241 | """ |
| 242 | if not isinstance(new_val, (int, proc_enums.ExitCodes)): |
| 243 | raise TypeError('Exit code is strictly int') |
| 244 | with self.lock: |
| 245 | if isinstance(new_val, int) and \ |
| 246 | new_val in proc_enums.ExitCodes.__members__.values(): |
| 247 | new_val = proc_enums.ExitCodes(new_val) |
| 248 | self.__exit_code = new_val |
| 249 | |
| 250 | def __deserialize(self, fmt): |
| 251 | """Deserialize stdout as data format |
| 252 | |
| 253 | :type fmt: str |
| 254 | :rtype: object |
| 255 | :raises: DevopsError |
| 256 | """ |
| 257 | try: |
| 258 | if fmt == 'json': |
| 259 | return json.loads(self.stdout_str, encoding='utf-8') |
| 260 | elif fmt == 'yaml': |
| 261 | return yaml.safe_load(self.stdout_str) |
| 262 | except BaseException: |
| 263 | tmpl = ( |
| 264 | " stdout is not valid {fmt}:\n" |
| 265 | '{{stdout!r}}\n'.format( |
| 266 | fmt=fmt)) |
| 267 | logger.exception(self.cmd + tmpl.format(stdout=self.stdout_str)) |
| 268 | raise TypeError( |
| 269 | self.cmd + tmpl.format(stdout=self.stdout_brief)) |
| 270 | msg = '{fmt} deserialize target is not implemented'.format(fmt=fmt) |
| 271 | logger.error(msg) |
| 272 | raise NotImplementedError(msg) |
| 273 | |
| 274 | @property |
| 275 | def stdout_json(self): |
| 276 | """JSON from stdout |
| 277 | |
| 278 | :rtype: object |
| 279 | """ |
| 280 | with self.lock: |
| 281 | if self.__stdout_json is None: |
| 282 | # noinspection PyTypeChecker |
| 283 | self.__stdout_json = self.__deserialize(fmt='json') |
| 284 | return self.__stdout_json |
| 285 | |
| 286 | @property |
| 287 | def stdout_yaml(self): |
| 288 | """YAML from stdout |
| 289 | |
| 290 | :rtype: Union(list, dict, None) |
| 291 | """ |
| 292 | with self.lock: |
| 293 | if self.__stdout_yaml is None: |
| 294 | # noinspection PyTypeChecker |
| 295 | self.__stdout_yaml = self.__deserialize(fmt='yaml') |
| 296 | return self.__stdout_yaml |
| 297 | |
| 298 | def __dir__(self): |
| 299 | return [ |
| 300 | 'cmd', 'stdout', 'stderr', 'exit_code', |
| 301 | 'stdout_bin', 'stderr_bin', |
| 302 | 'stdout_str', 'stderr_str', 'stdout_brief', 'stderr_brief', |
| 303 | 'stdout_json', 'stdout_yaml', |
| 304 | 'lock' |
| 305 | ] |
| 306 | |
| 307 | def __getitem__(self, item): |
| 308 | if item in dir(self): |
| 309 | return getattr(self, item) |
| 310 | raise IndexError( |
| 311 | '"{item}" not found in {dir}'.format( |
| 312 | item=item, dir=dir(self) |
| 313 | ) |
| 314 | ) |
| 315 | |
| 316 | def __setitem__(self, key, value): |
| 317 | rw = ['stdout', 'stderr', 'exit_code'] |
| 318 | if key in rw: |
| 319 | setattr(self, key, value) |
| 320 | return |
| 321 | if key in deprecated_aliases: |
| 322 | logger.warning( |
| 323 | '{key} is read-only and calculated automatically'.format( |
| 324 | key=key |
| 325 | ) |
| 326 | ) |
| 327 | return |
| 328 | if key in dir(self): |
| 329 | raise RuntimeError( |
| 330 | '{key} is read-only!'.format(key=key) |
| 331 | ) |
| 332 | raise IndexError( |
| 333 | '{key} not found in {dir}'.format( |
| 334 | key=key, dir=rw |
| 335 | ) |
| 336 | ) |
| 337 | |
| 338 | def __repr__(self): |
| 339 | return ( |
| 340 | '{cls}(cmd={cmd!r}, stdout={stdout}, stderr={stderr}, ' |
| 341 | 'exit_code={exit_code!s})'.format( |
| 342 | cls=self.__class__.__name__, |
| 343 | cmd=self.cmd, |
| 344 | stdout=self.stdout, |
| 345 | stderr=self.stderr, |
| 346 | exit_code=self.exit_code |
| 347 | )) |
| 348 | |
| 349 | def __str__(self): |
| 350 | return ( |
| 351 | "{cls}(\n\tcmd={cmd!r}," |
| 352 | "\n\t stdout=\n'{stdout_brief}'," |
| 353 | "\n\tstderr=\n'{stderr_brief}', " |
| 354 | '\n\texit_code={exit_code!s}\n)'.format( |
| 355 | cls=self.__class__.__name__, |
| 356 | cmd=self.cmd, |
| 357 | stdout_brief=self.stdout_brief, |
| 358 | stderr_brief=self.stderr_brief, |
| 359 | exit_code=self.exit_code |
| 360 | ) |
| 361 | ) |
| 362 | |
| 363 | def __eq__(self, other): |
| 364 | return all( |
| 365 | ( |
| 366 | getattr(self, val) == getattr(other, val) |
| 367 | for val in ['cmd', 'stdout', 'stderr', 'exit_code'] |
| 368 | ) |
| 369 | ) |
| 370 | |
| 371 | def __ne__(self, other): |
| 372 | return not self.__eq__(other) |
| 373 | |
| 374 | def __hash__(self): |
| 375 | return hash( |
| 376 | ( |
| 377 | self.__class__, self.cmd, self.stdout_str, self.stderr_str, |
| 378 | self.exit_code |
| 379 | )) |