fuel-devops/devops/helpers/exec_result.py

359 lines
9.7 KiB
Python

# Copyright 2016 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import unicode_literals
from json import loads
from threading import RLock
from yaml import safe_load
from devops.error import DevopsError
from devops.error import DevopsNotImplementedError
from devops.helpers.proc_enums import ExitCodes
from devops import logger
deprecated_aliases = {
'stdout_str',
'stdout_len',
'stderr_str',
'stderr_len',
'stdout_json',
'stdout_yaml'
}
class ExecResult(object):
__slots__ = [
'__cmd', '__stdout', '__stderr', '__exit_code',
'__stdout_str', '__stderr_str', '__stdout_brief', '__stderr_brief',
'__stdout_json', '__stdout_yaml',
'__lock'
]
def __init__(self, cmd, stdout=None, stderr=None,
exit_code=ExitCodes.EX_INVALID):
"""Command execution result read from fifo
:type cmd: str
:type stdout: list
:type stderr: list
:type exit_code: ExitCodes
"""
self.__lock = RLock()
self.__cmd = cmd
self.__stdout = stdout if stdout is not None else []
self.__stderr = stderr if stderr is not None else []
self.__exit_code = None
self.exit_code = exit_code
# By default is none:
self.__stdout_str = None
self.__stderr_str = None
self.__stdout_brief = None
self.__stderr_brief = None
self.__stdout_json = None
self.__stdout_yaml = None
@property
def lock(self):
"""Lock object for thread-safe operation
:rtype: RLock
"""
return self.__lock
@staticmethod
def _get_str_from_list(src):
"""Join data in list to the string, with python 2&3 compatibility.
:type src: list
:rtype: str
"""
return b''.join(src).strip().decode(encoding='utf-8')
@classmethod
def _get_brief(cls, data):
"""Get brief output: 7 lines maximum (3 first + ... + 3 last)
:type data: list
:rtype: str
"""
if len(data) <= 7:
return cls._get_str_from_list(data)
else:
return cls._get_str_from_list(
data[:3] + [b'...\n'] + data[-3:]
)
@property
def cmd(self):
"""Executed command
:rtype: str
"""
return self.__cmd
@property
def stdout(self):
"""Stdout output as list of binaries
:rtype: list
"""
return self.__stdout
@stdout.setter
def stdout(self, new_val):
"""Stdout output as list of binaries
:type new_val: list
:raises: TypeError
"""
if not isinstance(new_val, (list, type(None))):
raise TypeError('stdout should be list only!')
with self.lock:
self.__stdout_str = None
self.__stdout_brief = None
self.__stdout_json = None
self.__stdout_yaml = None
self.__stdout = new_val
@property
def stderr(self):
"""Stderr output as list of binaries
:rtype: list
"""
return self.__stderr
@stderr.setter
def stderr(self, new_val):
"""Stderr output as list of binaries
:type new_val: list
:raises: TypeError
"""
if not isinstance(new_val, (list, None)):
raise TypeError('stderr should be list only!')
with self.lock:
self.__stderr_str = None
self.__stderr_brief = None
self.__stderr = new_val
@property
def stdout_len(self):
return len(self.stdout)
@property
def stderr_len(self):
return len(self.stderr)
@property
def stdout_str(self):
"""Stdout output as string
:rtype: str
"""
with self.lock:
if self.__stdout_str is None:
self.__stdout_str = self._get_str_from_list(self.stdout)
return self.__stdout_str
@property
def stderr_str(self):
"""Stderr output as string
:rtype: str
"""
with self.lock:
if self.__stderr_str is None:
self.__stderr_str = self._get_str_from_list(self.stderr)
return self.__stderr_str
@property
def stdout_brief(self):
"""Brief stdout output (mostly for exceptions)
:rtype: str
"""
with self.lock:
if self.__stdout_brief is None:
self.__stdout_brief = self._get_brief(self.stdout)
return self.__stdout_brief
@property
def stderr_brief(self):
"""Brief stderr output (mostly for exceptions)
:rtype: str
"""
with self.lock:
if self.__stderr_brief is None:
self.__stderr_brief = self._get_brief(self.stderr)
return self.__stderr_brief
@property
def exit_code(self):
"""Return(exit) code of command
:rtype: int
"""
return self.__exit_code
@exit_code.setter
def exit_code(self, new_val):
"""Return(exit) code of command
:type new_val: int
"""
if not isinstance(new_val, (int, ExitCodes)):
raise TypeError('Exit code is strictly int')
with self.lock:
if isinstance(new_val, int) and \
new_val in ExitCodes.__members__.values():
new_val = ExitCodes(new_val)
self.__exit_code = new_val
def __deserialize(self, fmt):
"""Deserialize stdout as data format
:type fmt: str
:rtype: object
:raises: DevopsError
"""
try:
if fmt == 'json':
return loads(self.stdout_str, encoding='utf-8')
elif fmt == 'yaml':
return safe_load(self.stdout_str)
except BaseException:
tmpl = (
"'{cmd}' stdout is not valid {fmt}:\n"
'{{stdout!r}}\n'.format(
cmd=self.cmd,
fmt=fmt))
logger.exception(tmpl.format(stdout=self.stdout_str))
raise DevopsError(tmpl.format(stdout=self.stdout_brief))
msg = '{fmt} deserialize target is not implemented'.format(fmt=fmt)
logger.error(msg)
raise DevopsNotImplementedError(msg)
@property
def stdout_json(self):
"""JSON from stdout
:rtype: object
"""
with self.lock:
if self.__stdout_json is None:
# noinspection PyTypeChecker
self.__stdout_json = self.__deserialize(fmt='json')
return self.__stdout_json
@property
def stdout_yaml(self):
"""YAML from stdout
:rtype: object
"""
with self.lock:
if self.__stdout_yaml is None:
# noinspection PyTypeChecker
self.__stdout_yaml = self.__deserialize(fmt='yaml')
return self.__stdout_yaml
@staticmethod
def __dir__():
return [
'cmd', 'stdout', 'stderr', 'exit_code',
'stdout_str', 'stderr_str', 'stdout_brief', 'stderr_brief',
'stdout_json', 'stdout_yaml',
'lock'
]
def __getitem__(self, item):
if item in dir(self):
return getattr(self, item)
raise IndexError(
'"{item}" not found in {dir}'.format(
item=item, dir=dir(self)
)
)
def __setitem__(self, key, value):
rw = ['stdout', 'stderr', 'exit_code']
if key in rw:
setattr(self, key, value)
return
if key in deprecated_aliases:
logger.warning(
'{key} is read-only and calculated automatically'.format(
key=key
)
)
return
if key in dir(self):
raise DevopsError(
'{key} is read-only!'.format(key=key)
)
raise IndexError(
'{key} not found in {dir}'.format(
key=key, dir=rw
)
)
def __repr__(self):
return (
'{cls}(cmd={cmd}, stdout={stdout}, stderr={stderr}, '
'exit_code={exit_code!s})'.format(
cls=self.__class__.__name__,
cmd=self.cmd,
stdout=self.stdout,
stderr=self.stderr,
exit_code=self.exit_code
))
def __str__(self):
return (
"{cls}(\n\tcmd={cmd},"
"\n\t stdout=\n'{stdout_brief}',"
"\n\tstderr=\n'{stderr_brief}', "
'\n\texit_code={exit_code!s}\n)'.format(
cls=self.__class__.__name__,
cmd=self.cmd,
stdout_brief=self.stdout_brief,
stderr_brief=self.stderr_brief,
exit_code=self.exit_code
)
)
def __eq__(self, other):
return all(
(
getattr(self, val) == getattr(other, val)
for val in ['cmd', 'stdout', 'stderr', 'exit_code']
)
)
def __hash__(self):
return hash(
(
self.__class__, self.cmd, self.stdout_str, self.stderr_str,
self.exit_code
))