-
Notifications
You must be signed in to change notification settings - Fork 213
Expand file tree
/
Copy pathbase.py
More file actions
237 lines (204 loc) · 7.84 KB
/
base.py
File metadata and controls
237 lines (204 loc) · 7.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# 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 collections.abc import Iterable, Mapping, Sequence
import json
import logging
import os
import shlex
import subprocess
from typing import Any, Literal, cast, overload
from tempest.lib.cli import output_parser
from tempest.lib import exceptions
import testtools
ADMIN_CLOUD = os.environ.get('OS_ADMIN_CLOUD', 'devstack-admin')
LOG = logging.getLogger(__name__)
def execute(cmd: str, *, fail_ok: bool = False) -> str:
"""Executes specified command for the given action."""
LOG.debug('Executing: %s', cmd)
cmdlist = shlex.split(cmd)
stdout = subprocess.PIPE
stderr = subprocess.PIPE
env = {
k: v for k, v in os.environ.copy().items() if not k.startswith('OS_')
}
proc = subprocess.Popen(cmdlist, stdout=stdout, stderr=stderr, env=env)
result_out_b, result_err = proc.communicate()
result_out = result_out_b.decode('utf-8')
LOG.debug('stdout: %s', result_out)
LOG.debug('stderr: %s', result_err)
if not fail_ok and proc.returncode != 0:
raise exceptions.CommandFailed(
proc.returncode,
cmd,
result_out,
result_err,
)
return result_out
class TestCase(testtools.TestCase):
@overload
@classmethod
def openstack(
cls,
cmd: str,
*,
cloud: str | None = ADMIN_CLOUD,
fail_ok: bool = False,
parse_output: Literal[False] = False,
) -> str: ...
@overload
@classmethod
def openstack(
cls,
cmd: str,
*,
cloud: str | None = ADMIN_CLOUD,
fail_ok: bool = False,
parse_output: Literal[True] = ...,
) -> Any: ...
@classmethod
def openstack(
cls,
cmd: str,
*,
cloud: str | None = ADMIN_CLOUD,
fail_ok: bool = False,
parse_output: bool = False,
) -> str | Any:
"""Executes openstackclient command for the given action
:param cmd: A string representation of the command to execute.
:param cloud: The cloud to execute against. This can be a string, empty
string, or None. A string results in '--os-auth-type $cloud', an
empty string results in the '--os-auth-type' option being
omitted, and None results in '--os-auth-type none' for legacy
reasons.
:param fail_ok: If failure is permitted. If False (default), a command
failure will result in `~tempest.lib.exceptions.CommandFailed`
being raised.
:param parse_output: If true, pass the '-f json' parameter and decode
the output.
:returns: The output from the command.
:raises: `~tempest.lib.exceptions.CommandFailed` if the command failed
and ``fail_ok`` was ``False``.
"""
auth_args = []
if cloud is None:
# Execute command with no auth
auth_args.append('--os-auth-type none')
elif cloud != '':
# Execute command with an explicit cloud specified
auth_args.append(f'--os-cloud {cloud}')
format_args = []
if parse_output:
format_args.append('-f json')
output = execute(
' '.join(['openstack', *auth_args, cmd, *format_args]),
fail_ok=fail_ok,
)
if parse_output:
try:
return json.loads(output)
except json.JSONDecodeError:
print(f'failed to decode: {output}')
raise
else:
return output
@classmethod
def is_service_enabled(
cls, service: str, version: str | None = None
) -> bool:
"""Ask client cloud if service is available
:param service: The service name or type. This should be either an
exact match to what is in the catalog or a known official value or
alias from service-types-authority
:param version: Optional version. This should be a major version, e.g.
'2.0'
:returns: True if the service is enabled and optionally provides the
specified API version, else False
"""
ret = cls.openstack(
f'versions show --service {service} -f value -c Version'
).splitlines()
if version:
return version in ret
return bool(ret)
@classmethod
def is_extension_enabled(
cls, alias: str, *, service: str = 'network'
) -> bool:
"""Ask client cloud if extension is enabled"""
extensions = cls.openstack(
f'extension list --{service}',
parse_output=True,
)
return alias in [x['Alias'] for x in extensions]
@classmethod
def get_openstack_configuration_value(cls, configuration: str) -> str:
opts = cls.get_opts([configuration])
return cls.openstack('configuration show ' + opts)
@classmethod
def get_opts(cls, fields: list[str], output_format: str = 'value') -> str:
return ' -f {} {}'.format(
output_format, ' '.join(['-c ' + it for it in fields])
)
@classmethod
def assertOutput(cls, expected: str, actual: str) -> None:
if expected != actual:
raise Exception(expected + ' != ' + actual)
@classmethod
def assertInOutput(cls, expected: str, actual: str) -> None:
if expected not in actual:
raise Exception(expected + ' not in ' + actual)
@classmethod
def assertsOutputNotNone(cls, observed: Any) -> None:
if observed is None:
raise Exception('No output observed')
def assert_table_structure(
self, items: Iterable[Mapping[str, Any]], field_names: Sequence[str]
) -> None:
"""Verify that all items have keys listed in field_names."""
for item in items:
for field in field_names:
self.assertIn(field, item)
def assert_show_fields(
self,
show_output: Iterable[Mapping[str, Any]],
field_names: Sequence[str],
) -> None:
"""Verify that all items have keys listed in field_names."""
# field_names = ['name', 'description']
# show_output = [{'name': 'fc2b98d8faed4126b9e371eda045ade2'},
# {'description': 'description-821397086'}]
# this next line creates a flattened list of all 'keys' (like 'name',
# and 'description' out of the output
all_headers = [item for sublist in show_output for item in sublist]
for field_name in field_names:
self.assertIn(field_name, all_headers)
def parse_show_as_object(self, raw_output):
"""Return a dict with values parsed from cli output."""
items = self.parse_show(raw_output)
o = {}
for item in items:
o.update(item)
return o
def parse_show(self, raw_output: str) -> list[dict[str, Any]]:
"""Return list of dicts with item values parsed from cli output."""
items = []
table_ = output_parser.table(raw_output)
for row in table_['values']:
item = {}
item[str(row[0])] = row[1]
items.append(item)
return items
def parse_listing(self, raw_output: str) -> list[dict[str, Any]]:
"""Return list of dicts with basic item parsed from cli output."""
# need to add hints to tempest
return cast(list[dict[str, Any]], output_parser.listing(raw_output))