tft-cli 0.0.26__py3-none-any.whl → 0.0.29__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tft/cli/command/__init__.py +2 -0
- tft/cli/command/listing.py +836 -0
- tft/cli/commands.py +203 -77
- tft/cli/config.py +10 -1
- tft/cli/tool.py +2 -0
- tft/cli/utils.py +149 -5
- {tft_cli-0.0.26.dist-info → tft_cli-0.0.29.dist-info}/METADATA +2 -1
- tft_cli-0.0.29.dist-info/RECORD +12 -0
- tft_cli-0.0.26.dist-info/RECORD +0 -10
- {tft_cli-0.0.26.dist-info → tft_cli-0.0.29.dist-info}/LICENSE +0 -0
- {tft_cli-0.0.26.dist-info → tft_cli-0.0.29.dist-info}/WHEEL +0 -0
- {tft_cli-0.0.26.dist-info → tft_cli-0.0.29.dist-info}/entry_points.txt +0 -0
tft/cli/commands.py
CHANGED
|
@@ -20,19 +20,22 @@ from typing import Any, Dict, List, Optional
|
|
|
20
20
|
|
|
21
21
|
import requests
|
|
22
22
|
import typer
|
|
23
|
-
from click.core import ParameterSource
|
|
23
|
+
from click.core import ParameterSource
|
|
24
24
|
from rich import print, print_json
|
|
25
25
|
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
26
|
-
from rich.table import Table
|
|
26
|
+
from rich.table import Table # type: ignore
|
|
27
27
|
|
|
28
28
|
from tft.cli.config import settings
|
|
29
29
|
from tft.cli.utils import (
|
|
30
30
|
artifacts,
|
|
31
|
+
authorization_headers,
|
|
31
32
|
check_unexpected_arguments,
|
|
32
33
|
cmd_output_or_exit,
|
|
33
34
|
console,
|
|
34
35
|
console_stderr,
|
|
36
|
+
edit_with_editor,
|
|
35
37
|
exit_error,
|
|
38
|
+
extract_uuid,
|
|
36
39
|
hw_constraints,
|
|
37
40
|
install_http_retries,
|
|
38
41
|
normalize_multistring_option,
|
|
@@ -65,8 +68,12 @@ RESERVE_URL = os.getenv("TESTING_FARM_RESERVE_URL", "https://gitlab.com/testing-
|
|
|
65
68
|
RESERVE_REF = os.getenv("TESTING_FARM_RESERVE_REF", "main")
|
|
66
69
|
RESERVE_TMT_DISCOVER_EXTRA_ARGS = f"--insert --how fmf --url {RESERVE_URL} --ref {RESERVE_REF} --test {RESERVE_TEST}"
|
|
67
70
|
|
|
71
|
+
# NOTE(mvadkert): note that reservation duration is different per ranch,
|
|
72
|
+
# ignore this fact for now here for reservations
|
|
68
73
|
DEFAULT_PIPELINE_TIMEOUT = 60 * 12
|
|
69
74
|
|
|
75
|
+
DEFAULT_AGE = "7d"
|
|
76
|
+
|
|
70
77
|
# SSH command options for reservation connections
|
|
71
78
|
SSH_RESERVATION_OPTIONS = (
|
|
72
79
|
"ssh -oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null -oServerAliveInterval=60 -oServerAliveCountMax=3"
|
|
@@ -85,6 +92,20 @@ class PipelineType(str, Enum):
|
|
|
85
92
|
tmt_multihost = "tmt-multihost"
|
|
86
93
|
|
|
87
94
|
|
|
95
|
+
class PipelineState(str, Enum):
|
|
96
|
+
new = "new"
|
|
97
|
+
queued = "queued"
|
|
98
|
+
running = "running"
|
|
99
|
+
complete = "complete"
|
|
100
|
+
error = "error"
|
|
101
|
+
canceled = "canceled"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class Ranch(str, Enum):
|
|
105
|
+
public = "public"
|
|
106
|
+
redhat = "redhat"
|
|
107
|
+
|
|
108
|
+
|
|
88
109
|
# Arguments and options that are shared among multiple commands
|
|
89
110
|
ARGUMENT_API_URL: str = typer.Argument(
|
|
90
111
|
settings.API_URL, envvar="TESTING_FARM_API_URL", metavar='', rich_help_panel='Environment variables'
|
|
@@ -99,6 +120,12 @@ ARGUMENT_API_TOKEN: str = typer.Argument(
|
|
|
99
120
|
metavar='',
|
|
100
121
|
rich_help_panel='Environment variables',
|
|
101
122
|
)
|
|
123
|
+
ARGUMENT_INTERNAL_API_URL: str = typer.Argument(
|
|
124
|
+
settings.INTERNAL_API_URL,
|
|
125
|
+
envvar="TESTING_FARM_INTERNAL_API_URL",
|
|
126
|
+
metavar='',
|
|
127
|
+
rich_help_panel='Environment variables',
|
|
128
|
+
)
|
|
102
129
|
OPTION_API_TOKEN: str = typer.Option(
|
|
103
130
|
settings.API_TOKEN,
|
|
104
131
|
envvar="TESTING_FARM_API_TOKEN",
|
|
@@ -106,6 +133,33 @@ OPTION_API_TOKEN: str = typer.Option(
|
|
|
106
133
|
metavar='',
|
|
107
134
|
rich_help_panel='Environment variables',
|
|
108
135
|
)
|
|
136
|
+
|
|
137
|
+
# Restart command specific arguments for source operations
|
|
138
|
+
ARGUMENT_SOURCE_API_URL: str = typer.Argument(
|
|
139
|
+
None, envvar="TESTING_FARM_SOURCE_API_URL", metavar='', rich_help_panel='Environment variables'
|
|
140
|
+
)
|
|
141
|
+
ARGUMENT_INTERNAL_SOURCE_API_URL: str = typer.Argument(
|
|
142
|
+
None, envvar="TESTING_FARM_INTERNAL_SOURCE_API_URL", metavar='', rich_help_panel='Environment variables'
|
|
143
|
+
)
|
|
144
|
+
ARGUMENT_SOURCE_API_TOKEN: str = typer.Argument(
|
|
145
|
+
None,
|
|
146
|
+
envvar="TESTING_FARM_SOURCE_API_TOKEN",
|
|
147
|
+
show_default=False,
|
|
148
|
+
metavar='',
|
|
149
|
+
rich_help_panel='Environment variables',
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Restart command specific arguments for target operations
|
|
153
|
+
ARGUMENT_TARGET_API_URL: str = typer.Argument(
|
|
154
|
+
None, envvar="TESTING_FARM_TARGET_API_URL", metavar='', rich_help_panel='Environment variables'
|
|
155
|
+
)
|
|
156
|
+
ARGUMENT_TARGET_API_TOKEN: str = typer.Argument(
|
|
157
|
+
None,
|
|
158
|
+
envvar="TESTING_FARM_TARGET_API_TOKEN",
|
|
159
|
+
show_default=False,
|
|
160
|
+
metavar='',
|
|
161
|
+
rich_help_panel='Environment variables',
|
|
162
|
+
)
|
|
109
163
|
OPTION_TMT_PLAN_NAME: Optional[str] = typer.Option(
|
|
110
164
|
None,
|
|
111
165
|
"--plan",
|
|
@@ -278,6 +332,17 @@ OPTION_TMT_CONTEXT: Optional[List[str]] = typer.Option(
|
|
|
278
332
|
metavar="key=value|@file",
|
|
279
333
|
help="Context variables to pass to `tmt`. The @ prefix marks a yaml file to load.",
|
|
280
334
|
)
|
|
335
|
+
OPTION_TMT_ENVIRONMENT: Optional[List[str]] = typer.Option(
|
|
336
|
+
None,
|
|
337
|
+
"-T",
|
|
338
|
+
"--tmt-environment",
|
|
339
|
+
metavar="key=value|@file",
|
|
340
|
+
help=(
|
|
341
|
+
"Environment variables to pass to the tmt process. "
|
|
342
|
+
"Used to configure tmt report plugins like reportportal or polarion. "
|
|
343
|
+
"The @ prefix marks a yaml file to load."
|
|
344
|
+
),
|
|
345
|
+
)
|
|
281
346
|
|
|
282
347
|
|
|
283
348
|
def _option_autoconnect(panel: str) -> bool:
|
|
@@ -427,6 +492,24 @@ def _localhost_ingress_rule(session: requests.Session) -> str:
|
|
|
427
492
|
exit_error(f"Got {get_ip.status_code} while checking {settings.PUBLIC_IP_CHECKER_URL}")
|
|
428
493
|
|
|
429
494
|
|
|
495
|
+
def _extend_test_name_for_reservation(tmt_test_name: Optional[str]) -> Optional[str]:
|
|
496
|
+
"""
|
|
497
|
+
Extend test name to include the reservation test when --reserve is used.
|
|
498
|
+
"""
|
|
499
|
+
if tmt_test_name:
|
|
500
|
+
return f"{tmt_test_name}|{RESERVE_TEST}"
|
|
501
|
+
return None
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _extend_test_filter_for_reservation(tmt_test_filter: Optional[str]) -> Optional[str]:
|
|
505
|
+
"""
|
|
506
|
+
Extend test filter to include the reservation test when --reserve is used.
|
|
507
|
+
"""
|
|
508
|
+
if tmt_test_filter:
|
|
509
|
+
return f"{tmt_test_filter} | name:{RESERVE_TEST}"
|
|
510
|
+
return None
|
|
511
|
+
|
|
512
|
+
|
|
430
513
|
def _add_reservation(
|
|
431
514
|
ssh_public_keys: List[str],
|
|
432
515
|
rules: Dict[str, Any],
|
|
@@ -534,14 +617,6 @@ def _parse_security_group_rules(ingress_rules: List[str], egress_rules: List[str
|
|
|
534
617
|
return security_group_rules
|
|
535
618
|
|
|
536
619
|
|
|
537
|
-
def _get_headers(api_key: str) -> Dict[str, str]:
|
|
538
|
-
"""
|
|
539
|
-
Return a dict with headers for a request to Testing Farm API.
|
|
540
|
-
Used for authentication.
|
|
541
|
-
"""
|
|
542
|
-
return {'Authorization': f'Bearer {api_key}'}
|
|
543
|
-
|
|
544
|
-
|
|
545
620
|
def _parse_xunit(xunit: str):
|
|
546
621
|
"""
|
|
547
622
|
A helper that parses xunit file into sets of passed_plans/failed_plans/errored_plans per arch.
|
|
@@ -835,9 +910,14 @@ def request(
|
|
|
835
910
|
context: typer.Context,
|
|
836
911
|
api_url: str = ARGUMENT_API_URL,
|
|
837
912
|
api_token: str = ARGUMENT_API_TOKEN,
|
|
838
|
-
timeout: int = typer.Option(
|
|
839
|
-
|
|
840
|
-
help=
|
|
913
|
+
timeout: Optional[int] = typer.Option(
|
|
914
|
+
None,
|
|
915
|
+
help=(
|
|
916
|
+
"Set the timeout for the request in minutes. "
|
|
917
|
+
"If the request takes longer than this, it will be terminated. "
|
|
918
|
+
"The timeout is counted from the time the request is switched to the running state."
|
|
919
|
+
"For default timeout see https://docs.testing-farm.io/Testing%20Farm/0.1/test-request.html."
|
|
920
|
+
),
|
|
841
921
|
),
|
|
842
922
|
test_type: str = typer.Option("fmf", help="Test type to use, if not set autodetected."),
|
|
843
923
|
tmt_plan_name: Optional[str] = OPTION_TMT_PLAN_NAME,
|
|
@@ -871,17 +951,7 @@ def request(
|
|
|
871
951
|
cli_tmt_context: Optional[List[str]] = OPTION_TMT_CONTEXT,
|
|
872
952
|
variables: Optional[List[str]] = OPTION_VARIABLES,
|
|
873
953
|
secrets: Optional[List[str]] = OPTION_SECRETS,
|
|
874
|
-
tmt_environment: Optional[List[str]] =
|
|
875
|
-
None,
|
|
876
|
-
"-T",
|
|
877
|
-
"--tmt-environment",
|
|
878
|
-
metavar="key=value|@file",
|
|
879
|
-
help=(
|
|
880
|
-
"Environment variables to pass to the tmt process. "
|
|
881
|
-
"Used to configure tmt report plugins like reportportal or polarion. "
|
|
882
|
-
"The @ prefix marks a yaml file to load."
|
|
883
|
-
),
|
|
884
|
-
),
|
|
954
|
+
tmt_environment: Optional[List[str]] = OPTION_TMT_ENVIRONMENT,
|
|
885
955
|
no_wait: bool = typer.Option(False, help="Skip waiting for request completion."),
|
|
886
956
|
worker_image: Optional[str] = OPTION_WORKER_IMAGE,
|
|
887
957
|
redhat_brew_build: List[str] = OPTION_REDHAT_BREW_BUILD,
|
|
@@ -1027,10 +1097,16 @@ def request(
|
|
|
1027
1097
|
test["plan_filter"] = tmt_plan_filter
|
|
1028
1098
|
|
|
1029
1099
|
if tmt_test_name:
|
|
1030
|
-
|
|
1100
|
+
if reserve:
|
|
1101
|
+
test["test_name"] = _extend_test_name_for_reservation(tmt_test_name)
|
|
1102
|
+
else:
|
|
1103
|
+
test["test_name"] = tmt_test_name
|
|
1031
1104
|
|
|
1032
1105
|
if tmt_test_filter:
|
|
1033
|
-
|
|
1106
|
+
if reserve:
|
|
1107
|
+
test["test_filter"] = _extend_test_filter_for_reservation(tmt_test_filter)
|
|
1108
|
+
else:
|
|
1109
|
+
test["test_filter"] = tmt_test_filter
|
|
1034
1110
|
|
|
1035
1111
|
if sti_playbooks:
|
|
1036
1112
|
test["playbooks"] = sti_playbooks
|
|
@@ -1066,7 +1142,7 @@ def request(
|
|
|
1066
1142
|
|
|
1067
1143
|
if kickstart:
|
|
1068
1144
|
# Typer escapes newlines in options, we need to unescape them
|
|
1069
|
-
kickstart = [codecs.decode(value, 'unicode_escape') for value in kickstart]
|
|
1145
|
+
kickstart = [codecs.decode(value, 'unicode_escape') for value in kickstart]
|
|
1070
1146
|
environment["kickstart"] = options_to_dict("environment kickstart", kickstart)
|
|
1071
1147
|
|
|
1072
1148
|
if redhat_brew_build:
|
|
@@ -1113,7 +1189,10 @@ def request(
|
|
|
1113
1189
|
if len(environments) > 1:
|
|
1114
1190
|
exit_error("Reservations are currently supported for a single plan, cannot continue")
|
|
1115
1191
|
|
|
1116
|
-
|
|
1192
|
+
# support cases where the user has multiple localhost addresses
|
|
1193
|
+
rules = _parse_security_group_rules(
|
|
1194
|
+
list({_localhost_ingress_rule(requests.Session()) for _ in range(0, settings.PUBLIC_IP_RESOLVE_TRIES)}), []
|
|
1195
|
+
)
|
|
1117
1196
|
|
|
1118
1197
|
for environment in environments:
|
|
1119
1198
|
_add_reservation(
|
|
@@ -1171,11 +1250,14 @@ def request(
|
|
|
1171
1250
|
request["environments"] = environments
|
|
1172
1251
|
request["settings"] = {}
|
|
1173
1252
|
|
|
1174
|
-
|
|
1253
|
+
forced_pipeline_timeout = context.get_parameter_source("timeout") == ParameterSource.COMMANDLINE
|
|
1254
|
+
|
|
1255
|
+
if reserve or pipeline_type or parallel_limit or forced_pipeline_timeout:
|
|
1175
1256
|
request["settings"]["pipeline"] = {}
|
|
1176
1257
|
|
|
1177
1258
|
# in case the reservation duration is more than the pipeline timeout, adjust also the pipeline timeout
|
|
1178
1259
|
if reserve:
|
|
1260
|
+
timeout = timeout or DEFAULT_PIPELINE_TIMEOUT
|
|
1179
1261
|
if reservation_duration > timeout:
|
|
1180
1262
|
request["settings"]["pipeline"] = {"timeout": reservation_duration}
|
|
1181
1263
|
console.print(f"⏳ Maximum reservation time is {reservation_duration} minutes")
|
|
@@ -1184,7 +1266,7 @@ def request(
|
|
|
1184
1266
|
console.print(f"⏳ Maximum reservation time is {timeout} minutes")
|
|
1185
1267
|
|
|
1186
1268
|
# forced pipeline timeout
|
|
1187
|
-
elif
|
|
1269
|
+
elif forced_pipeline_timeout:
|
|
1188
1270
|
console.print(f"⏳ Pipeline timeout forced to {timeout} minutes")
|
|
1189
1271
|
request["settings"]["pipeline"] = {"timeout": timeout}
|
|
1190
1272
|
|
|
@@ -1216,7 +1298,7 @@ def request(
|
|
|
1216
1298
|
raise typer.Exit()
|
|
1217
1299
|
|
|
1218
1300
|
# handle errors
|
|
1219
|
-
response = session.post(post_url, json=request, headers=
|
|
1301
|
+
response = session.post(post_url, json=request, headers=authorization_headers(api_token))
|
|
1220
1302
|
if response.status_code == 401:
|
|
1221
1303
|
exit_error(f"API token is invalid. See {settings.ONBOARDING_DOCS} for more information.")
|
|
1222
1304
|
|
|
@@ -1240,13 +1322,13 @@ def restart(
|
|
|
1240
1322
|
context: typer.Context,
|
|
1241
1323
|
request_id: str = typer.Argument(..., help="Testing Farm request ID or a string containing it."),
|
|
1242
1324
|
api_url: str = ARGUMENT_API_URL,
|
|
1243
|
-
internal_api_url: str =
|
|
1244
|
-
settings.INTERNAL_API_URL,
|
|
1245
|
-
envvar="TESTING_FARM_INTERNAL_API_URL",
|
|
1246
|
-
metavar='',
|
|
1247
|
-
rich_help_panel='Environment variables',
|
|
1248
|
-
),
|
|
1325
|
+
internal_api_url: str = ARGUMENT_INTERNAL_API_URL,
|
|
1249
1326
|
api_token: str = ARGUMENT_API_TOKEN,
|
|
1327
|
+
source_api_url: Optional[str] = ARGUMENT_SOURCE_API_URL,
|
|
1328
|
+
internal_source_api_url: Optional[str] = ARGUMENT_INTERNAL_SOURCE_API_URL,
|
|
1329
|
+
source_api_token: Optional[str] = ARGUMENT_SOURCE_API_TOKEN,
|
|
1330
|
+
target_api_url: Optional[str] = ARGUMENT_TARGET_API_URL,
|
|
1331
|
+
target_api_token: Optional[str] = ARGUMENT_TARGET_API_TOKEN,
|
|
1250
1332
|
compose: Optional[str] = typer.Option(
|
|
1251
1333
|
None,
|
|
1252
1334
|
help="Force compose used to provision test environment.", # noqa
|
|
@@ -1280,6 +1362,13 @@ def restart(
|
|
|
1280
1362
|
autoconnect: bool = _option_autoconnect(REQUEST_PANEL_RESERVE),
|
|
1281
1363
|
reservation_duration: int = _option_reservation_duration(REQUEST_PANEL_RESERVE),
|
|
1282
1364
|
debug_reservation: bool = _option_debug_reservation(REQUEST_PANEL_RESERVE),
|
|
1365
|
+
edit: bool = typer.Option(
|
|
1366
|
+
False,
|
|
1367
|
+
help=(
|
|
1368
|
+
"Edit the request JSON in editor before submitting. "
|
|
1369
|
+
"Use the EDITOR environment variable to adjust the editor if needed."
|
|
1370
|
+
),
|
|
1371
|
+
),
|
|
1283
1372
|
):
|
|
1284
1373
|
"""
|
|
1285
1374
|
Restart a Testing Farm request.
|
|
@@ -1288,30 +1377,39 @@ def restart(
|
|
|
1288
1377
|
"""
|
|
1289
1378
|
|
|
1290
1379
|
# Accept these arguments only via environment variables
|
|
1291
|
-
check_unexpected_arguments(
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1380
|
+
check_unexpected_arguments(
|
|
1381
|
+
context,
|
|
1382
|
+
"api_url",
|
|
1383
|
+
"api_token",
|
|
1384
|
+
"internal_api_url",
|
|
1385
|
+
"source_api_url",
|
|
1386
|
+
"internal_source_api_url",
|
|
1387
|
+
"source_api_token",
|
|
1388
|
+
"target_api_url",
|
|
1389
|
+
"target_api_token",
|
|
1390
|
+
)
|
|
1295
1391
|
|
|
1296
|
-
#
|
|
1297
|
-
|
|
1392
|
+
# Determine source configuration (fallback to general settings)
|
|
1393
|
+
effective_source_api_url = source_api_url or api_url
|
|
1394
|
+
effective_internal_source_api_url = internal_source_api_url or internal_api_url
|
|
1395
|
+
effective_source_api_token = source_api_token or api_token
|
|
1298
1396
|
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1397
|
+
# Determine target configuration (fallback to general settings)
|
|
1398
|
+
effective_target_api_url = target_api_url or api_url
|
|
1399
|
+
effective_target_api_token = target_api_token or api_token
|
|
1302
1400
|
|
|
1303
|
-
# Extract the UUID from the
|
|
1304
|
-
_request_id =
|
|
1401
|
+
# Extract the UUID from the request_id string
|
|
1402
|
+
_request_id = extract_uuid(request_id)
|
|
1305
1403
|
|
|
1306
1404
|
# Construct URL to the internal API
|
|
1307
|
-
get_url = urllib.parse.urljoin(str(
|
|
1405
|
+
get_url = urllib.parse.urljoin(str(effective_internal_source_api_url), f"v0.1/requests/{_request_id}")
|
|
1308
1406
|
|
|
1309
1407
|
# Setting up retries
|
|
1310
1408
|
session = requests.Session()
|
|
1311
1409
|
install_http_retries(session)
|
|
1312
1410
|
|
|
1313
1411
|
# Get the request details
|
|
1314
|
-
response = session.get(get_url, headers=
|
|
1412
|
+
response = session.get(get_url, headers=authorization_headers(effective_source_api_token))
|
|
1315
1413
|
|
|
1316
1414
|
if response.status_code == 401:
|
|
1317
1415
|
exit_error(f"API token is invalid. See {settings.ONBOARDING_DOCS} for more information.")
|
|
@@ -1319,10 +1417,11 @@ def restart(
|
|
|
1319
1417
|
# The API token is valid, but it doesn't own the request
|
|
1320
1418
|
if response.status_code == 403:
|
|
1321
1419
|
console.print(
|
|
1322
|
-
"⚠️ [yellow] You are not the owner of this request. Any secrets associated with the
|
|
1420
|
+
"⚠️ [yellow] You are not the owner of this request. Any secrets associated with the "
|
|
1421
|
+
"request will not be included on the restart.[/yellow]"
|
|
1323
1422
|
)
|
|
1324
|
-
# Construct URL to the
|
|
1325
|
-
get_url = urllib.parse.urljoin(str(
|
|
1423
|
+
# Construct URL to the API
|
|
1424
|
+
get_url = urllib.parse.urljoin(str(effective_source_api_url), f"v0.1/requests/{_request_id}")
|
|
1326
1425
|
|
|
1327
1426
|
# Get the request details
|
|
1328
1427
|
response = session.get(get_url)
|
|
@@ -1360,10 +1459,16 @@ def restart(
|
|
|
1360
1459
|
test["ref"] = git_ref
|
|
1361
1460
|
|
|
1362
1461
|
if tmt_test_name:
|
|
1363
|
-
|
|
1462
|
+
if reserve:
|
|
1463
|
+
test["test_name"] = _extend_test_name_for_reservation(tmt_test_name)
|
|
1464
|
+
else:
|
|
1465
|
+
test["test_name"] = tmt_test_name
|
|
1364
1466
|
|
|
1365
1467
|
if tmt_test_filter:
|
|
1366
|
-
|
|
1468
|
+
if reserve:
|
|
1469
|
+
test["test_filter"] = _extend_test_filter_for_reservation(tmt_test_filter)
|
|
1470
|
+
else:
|
|
1471
|
+
test["test_filter"] = tmt_test_filter
|
|
1367
1472
|
|
|
1368
1473
|
merge_sha_info = ""
|
|
1369
1474
|
if git_merge_sha:
|
|
@@ -1431,7 +1536,7 @@ def restart(
|
|
|
1431
1536
|
|
|
1432
1537
|
if test_type == "fmf":
|
|
1433
1538
|
# The method explained in https://github.com/fastapi/typer/discussions/668
|
|
1434
|
-
if context.get_parameter_source("tmt_path") == ParameterSource.COMMANDLINE:
|
|
1539
|
+
if context.get_parameter_source("tmt_path") == ParameterSource.COMMANDLINE:
|
|
1435
1540
|
request["test"][test_type]["path"] = tmt_path
|
|
1436
1541
|
|
|
1437
1542
|
# worker image
|
|
@@ -1471,7 +1576,10 @@ def restart(
|
|
|
1471
1576
|
if len(request["environments"]) > 1:
|
|
1472
1577
|
exit_error("Reservations are currently supported for a single plan, cannot continue")
|
|
1473
1578
|
|
|
1474
|
-
|
|
1579
|
+
# support cases where the user has multiple localhost addresses
|
|
1580
|
+
rules = _parse_security_group_rules(
|
|
1581
|
+
list({_localhost_ingress_rule(requests.Session()) for _ in range(0, settings.PUBLIC_IP_RESOLVE_TRIES)}), []
|
|
1582
|
+
)
|
|
1475
1583
|
|
|
1476
1584
|
for environment in request["environments"]:
|
|
1477
1585
|
_add_reservation(
|
|
@@ -1489,6 +1597,18 @@ def restart(
|
|
|
1489
1597
|
f"🕗 {machine_pre} will be reserved after testing for [blue]{str(reservation_duration)}[/blue] minutes"
|
|
1490
1598
|
)
|
|
1491
1599
|
|
|
1600
|
+
# edit request if requested
|
|
1601
|
+
if edit:
|
|
1602
|
+
while True:
|
|
1603
|
+
try:
|
|
1604
|
+
request = json.loads(edit_with_editor(json.dumps(request, indent=2), "editing request"))
|
|
1605
|
+
break
|
|
1606
|
+
except (TypeError, ValueError) as error:
|
|
1607
|
+
console.print(f"⛔ Edited request is not a valid JSON, cannot continue: {error}", style="red")
|
|
1608
|
+
if typer.confirm("❓️ Edit again?"):
|
|
1609
|
+
continue
|
|
1610
|
+
raise typer.Exit(code=255)
|
|
1611
|
+
|
|
1492
1612
|
# dry run
|
|
1493
1613
|
if dry_run:
|
|
1494
1614
|
console.print("🔍 Dry run, showing POST json only", style="bright_yellow")
|
|
@@ -1496,10 +1616,10 @@ def restart(
|
|
|
1496
1616
|
raise typer.Exit()
|
|
1497
1617
|
|
|
1498
1618
|
# submit request to Testing Farm
|
|
1499
|
-
post_url = urllib.parse.urljoin(str(
|
|
1619
|
+
post_url = urllib.parse.urljoin(str(effective_target_api_url), "v0.1/requests")
|
|
1500
1620
|
|
|
1501
1621
|
# handle errors
|
|
1502
|
-
response = session.post(post_url, json=request, headers=
|
|
1622
|
+
response = session.post(post_url, json=request, headers=authorization_headers(effective_target_api_token))
|
|
1503
1623
|
if response.status_code == 401:
|
|
1504
1624
|
exit_error(f"API token is invalid. See {settings.ONBOARDING_DOCS} for more information.")
|
|
1505
1625
|
|
|
@@ -1516,7 +1636,7 @@ def restart(
|
|
|
1516
1636
|
# watch
|
|
1517
1637
|
watch(
|
|
1518
1638
|
context,
|
|
1519
|
-
str(
|
|
1639
|
+
str(effective_target_api_url),
|
|
1520
1640
|
response.json()['id'],
|
|
1521
1641
|
no_wait,
|
|
1522
1642
|
reserve=reserve,
|
|
@@ -1597,7 +1717,7 @@ def run(
|
|
|
1597
1717
|
raise typer.Exit()
|
|
1598
1718
|
|
|
1599
1719
|
# handle errors
|
|
1600
|
-
response = session.post(post_url, json=request, headers=
|
|
1720
|
+
response = session.post(post_url, json=request, headers=authorization_headers(api_token))
|
|
1601
1721
|
if response.status_code == 401:
|
|
1602
1722
|
exit_error(f"API token is invalid. See {settings.ONBOARDING_DOCS} for more information.")
|
|
1603
1723
|
|
|
@@ -1715,6 +1835,7 @@ def reserve(
|
|
|
1715
1835
|
repository: List[str] = OPTION_REPOSITORY,
|
|
1716
1836
|
repository_file: List[str] = OPTION_REPOSITORY_FILE,
|
|
1717
1837
|
redhat_brew_build: List[str] = OPTION_REDHAT_BREW_BUILD,
|
|
1838
|
+
tmt_environment: Optional[List[str]] = OPTION_TMT_ENVIRONMENT,
|
|
1718
1839
|
tmt_discover: Optional[List[str]] = _generate_tmt_extra_args("discover"),
|
|
1719
1840
|
tmt_prepare: Optional[List[str]] = _generate_tmt_extra_args("prepare"),
|
|
1720
1841
|
tmt_finish: Optional[List[str]] = _generate_tmt_extra_args("finish"),
|
|
@@ -1797,7 +1918,7 @@ def reserve(
|
|
|
1797
1918
|
|
|
1798
1919
|
if kickstart:
|
|
1799
1920
|
# Typer escapes newlines in options, we need to unescape them
|
|
1800
|
-
kickstart = [codecs.decode(value, 'unicode_escape') for value in kickstart]
|
|
1921
|
+
kickstart = [codecs.decode(value, 'unicode_escape') for value in kickstart]
|
|
1801
1922
|
environment["kickstart"] = options_to_dict("environment kickstart", kickstart)
|
|
1802
1923
|
|
|
1803
1924
|
if redhat_brew_build:
|
|
@@ -1830,6 +1951,12 @@ def reserve(
|
|
|
1830
1951
|
if tmt_finish:
|
|
1831
1952
|
environment["tmt"]["extra_args"]["finish"] = tmt_finish
|
|
1832
1953
|
|
|
1954
|
+
if tmt_environment:
|
|
1955
|
+
if "tmt" not in environment:
|
|
1956
|
+
environment["tmt"] = {}
|
|
1957
|
+
|
|
1958
|
+
environment["tmt"].update({"environment": options_to_dict("tmt environment variables", tmt_environment)})
|
|
1959
|
+
|
|
1833
1960
|
# Setting up retries
|
|
1834
1961
|
session = requests.Session()
|
|
1835
1962
|
install_http_retries(session)
|
|
@@ -1837,7 +1964,10 @@ def reserve(
|
|
|
1837
1964
|
if not skip_workstation_access or security_group_rule_ingress or security_group_rule_egress:
|
|
1838
1965
|
ingress_rules = security_group_rule_ingress or []
|
|
1839
1966
|
if not skip_workstation_access:
|
|
1840
|
-
|
|
1967
|
+
# support cases where the user has multiple localhost addresses
|
|
1968
|
+
ingress_rules.extend(
|
|
1969
|
+
{_localhost_ingress_rule(requests.Session()) for _ in range(0, settings.PUBLIC_IP_RESOLVE_TRIES)}
|
|
1970
|
+
)
|
|
1841
1971
|
|
|
1842
1972
|
rules = _parse_security_group_rules(ingress_rules, security_group_rule_egress or [])
|
|
1843
1973
|
environment["settings"]["provisioning"].update(rules)
|
|
@@ -1891,7 +2021,7 @@ def reserve(
|
|
|
1891
2021
|
raise typer.Exit()
|
|
1892
2022
|
|
|
1893
2023
|
# handle errors
|
|
1894
|
-
response = session.post(post_url, json=request, headers=
|
|
2024
|
+
response = session.post(post_url, json=request, headers=authorization_headers(api_token))
|
|
1895
2025
|
if response.status_code == 401:
|
|
1896
2026
|
exit_error(f"API token is invalid. See {settings.ONBOARDING_DOCS} for more information.")
|
|
1897
2027
|
|
|
@@ -2057,23 +2187,13 @@ def cancel(
|
|
|
2057
2187
|
# Accept these arguments only via environment variables
|
|
2058
2188
|
check_unexpected_arguments(context, "api_url", "api_token")
|
|
2059
2189
|
|
|
2060
|
-
# UUID
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
# Find the UUID in the string
|
|
2064
|
-
uuid_match = uuid_pattern.search(request_id)
|
|
2065
|
-
|
|
2066
|
-
if not uuid_match:
|
|
2067
|
-
exit_error(f"Could not find a valid Testing Farm request id in '{request_id}'.")
|
|
2068
|
-
return
|
|
2190
|
+
# Extract the UUID from the request_id string
|
|
2191
|
+
_request_id = extract_uuid(request_id)
|
|
2069
2192
|
|
|
2070
2193
|
if not api_token:
|
|
2071
2194
|
exit_error("No API token found in the environment, please export 'TESTING_FARM_API_TOKEN' variable.")
|
|
2072
2195
|
return
|
|
2073
2196
|
|
|
2074
|
-
# Extract the UUID from the match object
|
|
2075
|
-
_request_id = uuid_match.group()
|
|
2076
|
-
|
|
2077
2197
|
# Construct URL to the internal API
|
|
2078
2198
|
request_url = urllib.parse.urljoin(str(api_url), f"v0.1/requests/{_request_id}")
|
|
2079
2199
|
|
|
@@ -2082,11 +2202,17 @@ def cancel(
|
|
|
2082
2202
|
install_http_retries(session)
|
|
2083
2203
|
|
|
2084
2204
|
# Get the request details
|
|
2085
|
-
response = session.delete(request_url, headers=
|
|
2205
|
+
response = session.delete(request_url, headers=authorization_headers(api_token))
|
|
2086
2206
|
|
|
2087
2207
|
if response.status_code == 401:
|
|
2088
2208
|
exit_error(f"API token is invalid. See {settings.ONBOARDING_DOCS} for more information.")
|
|
2089
2209
|
|
|
2210
|
+
if response.status_code == 403:
|
|
2211
|
+
exit_error(
|
|
2212
|
+
"You cannot cancel foreign requests. You can only cancel your own requests "
|
|
2213
|
+
"or must have 'admin' permissions."
|
|
2214
|
+
)
|
|
2215
|
+
|
|
2090
2216
|
if response.status_code == 404:
|
|
2091
2217
|
exit_error("Request was not found. Verify the request ID is correct.")
|
|
2092
2218
|
|
tft/cli/config.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Copyright Contributors to the Testing Farm project.
|
|
2
2
|
# SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
from dynaconf import LazySettings
|
|
4
|
+
from dynaconf import LazySettings # type: ignore
|
|
5
5
|
|
|
6
6
|
settings = LazySettings(
|
|
7
7
|
# all environment variables have `TESTING_FARM_` prefix
|
|
@@ -10,6 +10,13 @@ settings = LazySettings(
|
|
|
10
10
|
API_URL="https://api.dev.testing-farm.io/v0.1",
|
|
11
11
|
INTERNAL_API_URL="https://internal.api.dev.testing-farm.io/v0.1",
|
|
12
12
|
API_TOKEN=None,
|
|
13
|
+
# Restart command specific source API configuration (fallback to general settings)
|
|
14
|
+
SOURCE_API_URL=None,
|
|
15
|
+
INTERNAL_SOURCE_API_URL=None,
|
|
16
|
+
SOURCE_API_TOKEN=None,
|
|
17
|
+
# Restart command specific target API configuration (fallback to general settings)
|
|
18
|
+
TARGET_API_URL=None,
|
|
19
|
+
TARGET_API_TOKEN=None,
|
|
13
20
|
ISSUE_TRACKER="https://gitlab.com/testing-farm/general/-/issues/new",
|
|
14
21
|
STATUS_PAGE="https://status.testing-farm.io",
|
|
15
22
|
ONBOARDING_DOCS="https://docs.testing-farm.io/Testing%20Farm/0.1/onboarding.html",
|
|
@@ -27,4 +34,6 @@ settings = LazySettings(
|
|
|
27
34
|
TESTING_FARM_TESTS_GIT_URL="https://gitlab.com/testing-farm/tests",
|
|
28
35
|
TESTING_FARM_SANITY_PLAN="/testing-farm/sanity",
|
|
29
36
|
PUBLIC_IP_CHECKER_URL="https://ipv4.icanhazip.com",
|
|
37
|
+
# number or tries for resolving localhost public IP, useful if the user has multiple IPs
|
|
38
|
+
PUBLIC_IP_RESOLVE_TRIES=1,
|
|
30
39
|
)
|
tft/cli/tool.py
CHANGED
|
@@ -6,11 +6,13 @@ import os
|
|
|
6
6
|
import typer
|
|
7
7
|
|
|
8
8
|
import tft.cli.commands as commands
|
|
9
|
+
from tft.cli.command.listing import listing
|
|
9
10
|
from tft.cli.config import settings
|
|
10
11
|
|
|
11
12
|
app = typer.Typer()
|
|
12
13
|
|
|
13
14
|
app.command()(commands.cancel)
|
|
15
|
+
app.command(name="list")(listing)
|
|
14
16
|
app.command()(commands.request)
|
|
15
17
|
app.command()(commands.restart)
|
|
16
18
|
app.command()(commands.reserve)
|