xref: /openbmc/qemu/python/qemu/utils/__init__.py (revision 7698afc42b5af9e55f12ab2236618e38e5a1c23f)
1"""
2QEMU development and testing utilities
3
4This package provides a small handful of utilities for performing
5various tasks not directly related to the launching of a VM.
6"""
7
8# Copyright (C) 2021 Red Hat Inc.
9#
10# Authors:
11#  John Snow <jsnow@redhat.com>
12#  Cleber Rosa <crosa@redhat.com>
13#
14# This work is licensed under the terms of the GNU GPL, version 2.  See
15# the COPYING file in the top-level directory.
16#
17
18import os
19import re
20import shutil
21from subprocess import CalledProcessError
22import textwrap
23from typing import Optional
24
25# pylint: disable=import-error
26from .accel import (
27    hvf_available,
28    kvm_available,
29    list_accel,
30    tcg_available,
31)
32
33
34__all__ = (
35    'VerboseProcessError',
36    'add_visual_margin',
37    'get_info_usernet_hostfwd_port',
38    'hvf_available',
39    'kvm_available',
40    'list_accel',
41    'tcg_available',
42)
43
44
45def get_info_usernet_hostfwd_port(info_usernet_output: str) -> Optional[int]:
46    """
47    Returns the port given to the hostfwd parameter via info usernet
48
49    :param info_usernet_output: output generated by hmp command "info usernet"
50    :return: the port number allocated by the hostfwd option
51    """
52    for line in info_usernet_output.split('\r\n'):
53        regex = r'TCP.HOST_FORWARD.*127\.0\.0\.1\s+(\d+)\s+10\.'
54        match = re.search(regex, line)
55        if match is not None:
56            return int(match[1])
57    return None
58
59
60# pylint: disable=too-many-arguments
61def add_visual_margin(
62        content: str = '',
63        width: Optional[int] = None,
64        name: Optional[str] = None,
65        padding: int = 1,
66        upper_left: str = '┏',
67        lower_left: str = '┗',
68        horizontal: str = '━',
69        vertical: str = '┃',
70) -> str:
71    """
72    Decorate and wrap some text with a visual decoration around it.
73
74    This function assumes that the text decoration characters are single
75    characters that display using a single monospace column.
76
77    ┏━ Example ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
78    ┃ This is what this function looks like with text content that's
79    ┃ wrapped to 66 characters. The right-hand margin is left open to
80    ┃ accommodate the occasional unicode character that might make
81    ┃ predicting the total "visual" width of a line difficult. This
82    ┃ provides a visual distinction that's good-enough, though.
83    ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
84
85    :param content: The text to wrap and decorate.
86    :param width:
87        The number of columns to use, including for the decoration
88        itself. The default (None) uses the available width of the
89        current terminal, or a fallback of 72 lines. A negative number
90        subtracts a fixed-width from the default size. The default obeys
91        the COLUMNS environment variable, if set.
92    :param name: A label to apply to the upper-left of the box.
93    :param padding: How many columns of padding to apply inside.
94    :param upper_left: Upper-left single-width text decoration character.
95    :param lower_left: Lower-left single-width text decoration character.
96    :param horizontal: Horizontal single-width text decoration character.
97    :param vertical: Vertical single-width text decoration character.
98    """
99    if width is None or width < 0:
100        avail = shutil.get_terminal_size(fallback=(72, 24))[0]
101        if width is None:
102            _width = avail
103        else:
104            _width = avail + width
105    else:
106        _width = width
107
108    prefix = vertical + (' ' * padding)
109
110    def _bar(name: Optional[str], top: bool = True) -> str:
111        ret = upper_left if top else lower_left
112        if name is not None:
113            ret += f"{horizontal} {name} "
114
115        filler_len = _width - len(ret)
116        ret += f"{horizontal * filler_len}"
117        return ret
118
119    def _wrap(line: str) -> str:
120        return os.linesep.join(
121            textwrap.wrap(
122                line, width=_width - padding, initial_indent=prefix,
123                subsequent_indent=prefix, replace_whitespace=False,
124                drop_whitespace=True, break_on_hyphens=False)
125        )
126
127    return os.linesep.join((
128        _bar(name, top=True),
129        os.linesep.join(_wrap(line) for line in content.splitlines()),
130        _bar(None, top=False),
131    ))
132
133
134class VerboseProcessError(CalledProcessError):
135    """
136    The same as CalledProcessError, but more verbose.
137
138    This is useful for debugging failed calls during test executions.
139    The return code, signal (if any), and terminal output will be displayed
140    on unhandled exceptions.
141    """
142    def summary(self) -> str:
143        """Return the normal CalledProcessError str() output."""
144        return super().__str__()
145
146    def __str__(self) -> str:
147        lmargin = '  '
148        width = -len(lmargin)
149        sections = []
150
151        # Does self.stdout contain both stdout and stderr?
152        has_combined_output = self.stderr is None
153
154        name = 'output' if has_combined_output else 'stdout'
155        if self.stdout:
156            sections.append(add_visual_margin(self.stdout, width, name))
157        else:
158            sections.append(f"{name}: N/A")
159
160        if self.stderr:
161            sections.append(add_visual_margin(self.stderr, width, 'stderr'))
162        elif not has_combined_output:
163            sections.append("stderr: N/A")
164
165        return os.linesep.join((
166            self.summary(),
167            textwrap.indent(os.linesep.join(sections), prefix=lmargin),
168        ))
169