blob: 3dc6245382f7e8a5ff8f287170b09e7e9c8b1ede [file] [log] [blame]
Dennis Dmitrieve56c8b92017-06-16 01:53:16 +03001# 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
15from __future__ import unicode_literals
16
17import json
18import threading
19
20import yaml
21
22from reclass_tools.helpers import proc_enums
23from reclass_tools import logger
24
25
26deprecated_aliases = {
27 'stdout_str',
28 'stderr_str',
29 'stdout_json',
30 'stdout_yaml'
31}
32
33
34class 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 ))