tft-cli 0.0.23__py3-none-any.whl → 0.0.25__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/commands.py +153 -24
- tft/cli/utils.py +24 -0
- {tft_cli-0.0.23.dist-info → tft_cli-0.0.25.dist-info}/METADATA +1 -1
- tft_cli-0.0.25.dist-info/RECORD +10 -0
- tft_cli-0.0.23.dist-info/RECORD +0 -10
- {tft_cli-0.0.23.dist-info → tft_cli-0.0.25.dist-info}/LICENSE +0 -0
- {tft_cli-0.0.23.dist-info → tft_cli-0.0.25.dist-info}/WHEEL +0 -0
- {tft_cli-0.0.23.dist-info → tft_cli-0.0.25.dist-info}/entry_points.txt +0 -0
tft/cli/commands.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
4
|
import base64
|
|
5
|
+
import codecs
|
|
5
6
|
import ipaddress
|
|
6
7
|
import json
|
|
7
8
|
import os
|
|
@@ -27,6 +28,7 @@ from rich.table import Table
|
|
|
27
28
|
from tft.cli.config import settings
|
|
28
29
|
from tft.cli.utils import (
|
|
29
30
|
artifacts,
|
|
31
|
+
check_unexpected_arguments,
|
|
30
32
|
cmd_output_or_exit,
|
|
31
33
|
console,
|
|
32
34
|
console_stderr,
|
|
@@ -65,6 +67,11 @@ RESERVE_TMT_DISCOVER_EXTRA_ARGS = f"--insert --how fmf --url {RESERVE_URL} --ref
|
|
|
65
67
|
|
|
66
68
|
DEFAULT_PIPELINE_TIMEOUT = 60 * 12
|
|
67
69
|
|
|
70
|
+
# SSH command options for reservation connections
|
|
71
|
+
SSH_RESERVATION_OPTIONS = (
|
|
72
|
+
"ssh -oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null -oServerAliveInterval=60 -oServerAliveCountMax=3"
|
|
73
|
+
)
|
|
74
|
+
|
|
68
75
|
# Won't be validating CIDR and 65535 max port range with regex here, not worth it
|
|
69
76
|
SECURITY_GROUP_RULE_FORMAT = re.compile(r"(tcp|ip|icmp|udp|-1|[0-255]):(.*):(\d{1,5}-\d{1,5}|\d{1,5}|-1)")
|
|
70
77
|
|
|
@@ -82,6 +89,9 @@ class PipelineType(str, Enum):
|
|
|
82
89
|
ARGUMENT_API_URL: str = typer.Argument(
|
|
83
90
|
settings.API_URL, envvar="TESTING_FARM_API_URL", metavar='', rich_help_panel='Environment variables'
|
|
84
91
|
)
|
|
92
|
+
OPTION_API_URL: str = typer.Option(
|
|
93
|
+
settings.API_URL, envvar="TESTING_FARM_API_URL", metavar='', rich_help_panel='Environment variables'
|
|
94
|
+
)
|
|
85
95
|
ARGUMENT_API_TOKEN: str = typer.Argument(
|
|
86
96
|
settings.API_TOKEN,
|
|
87
97
|
envvar="TESTING_FARM_API_TOKEN",
|
|
@@ -89,6 +99,13 @@ ARGUMENT_API_TOKEN: str = typer.Argument(
|
|
|
89
99
|
metavar='',
|
|
90
100
|
rich_help_panel='Environment variables',
|
|
91
101
|
)
|
|
102
|
+
OPTION_API_TOKEN: str = typer.Option(
|
|
103
|
+
settings.API_TOKEN,
|
|
104
|
+
envvar="TESTING_FARM_API_TOKEN",
|
|
105
|
+
show_default=False,
|
|
106
|
+
metavar='',
|
|
107
|
+
rich_help_panel='Environment variables',
|
|
108
|
+
)
|
|
92
109
|
OPTION_TMT_PLAN_NAME: Optional[str] = typer.Option(
|
|
93
110
|
None,
|
|
94
111
|
"--plan",
|
|
@@ -278,6 +295,14 @@ def _option_reservation_duration(panel: str) -> int:
|
|
|
278
295
|
)
|
|
279
296
|
|
|
280
297
|
|
|
298
|
+
def _option_debug_reservation(panel: Optional[str] = None) -> bool:
|
|
299
|
+
return typer.Option(
|
|
300
|
+
False,
|
|
301
|
+
help="Enable debug messages in the reservation code. Useful for testing changes to reservation code.",
|
|
302
|
+
rich_help_panel=panel,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
281
306
|
def _generate_tmt_extra_args(step: str) -> Optional[List[str]]:
|
|
282
307
|
return typer.Option(
|
|
283
308
|
None,
|
|
@@ -318,12 +343,12 @@ def _sanity_reserve() -> None:
|
|
|
318
343
|
exit_error("No SSH identities found in the SSH agent. Please run `ssh-add`.")
|
|
319
344
|
|
|
320
345
|
|
|
321
|
-
def _handle_reservation(session, request_id: str, autoconnect: bool = False) -> None:
|
|
346
|
+
def _handle_reservation(session, api_url: str, request_id: str, autoconnect: bool = False) -> None:
|
|
322
347
|
"""
|
|
323
348
|
Handle the reservation for :py:func:``request`` and :py:func:``restart`` commands.
|
|
324
349
|
"""
|
|
325
350
|
# Get artifacts url
|
|
326
|
-
request_url = urllib.parse.urljoin(
|
|
351
|
+
request_url = urllib.parse.urljoin(api_url, f"/v0.1/requests/{request_id}")
|
|
327
352
|
response = session.get(request_url)
|
|
328
353
|
artifacts_url = response.json()['run']['artifacts']
|
|
329
354
|
|
|
@@ -378,7 +403,7 @@ def _handle_reservation(session, request_id: str, autoconnect: bool = False) ->
|
|
|
378
403
|
console.print(f"🌎 ssh root@{guests[0]}")
|
|
379
404
|
|
|
380
405
|
if autoconnect:
|
|
381
|
-
os.system(f"
|
|
406
|
+
os.system(f"{SSH_RESERVATION_OPTIONS} root@{guests[0]}") # noqa: E501
|
|
382
407
|
|
|
383
408
|
|
|
384
409
|
def _localhost_ingress_rule(session: requests.Session) -> str:
|
|
@@ -389,13 +414,19 @@ def _localhost_ingress_rule(session: requests.Session) -> str:
|
|
|
389
414
|
|
|
390
415
|
if get_ip.ok:
|
|
391
416
|
ip = get_ip.text.strip()
|
|
392
|
-
return f"-1:{ip}:-1"
|
|
417
|
+
return f"-1:{ip}:-1" # noqa: E231
|
|
393
418
|
|
|
394
419
|
else:
|
|
395
420
|
exit_error(f"Got {get_ip.status_code} while checking {settings.PUBLIC_IP_CHECKER_URL}")
|
|
396
421
|
|
|
397
422
|
|
|
398
|
-
def _add_reservation(
|
|
423
|
+
def _add_reservation(
|
|
424
|
+
ssh_public_keys: List[str],
|
|
425
|
+
rules: Dict[str, Any],
|
|
426
|
+
duration: int,
|
|
427
|
+
environment: Dict[str, Any],
|
|
428
|
+
debug_reservation: bool,
|
|
429
|
+
):
|
|
399
430
|
"""
|
|
400
431
|
Add discovery of the reservation test to the given environment.
|
|
401
432
|
"""
|
|
@@ -423,6 +454,9 @@ def _add_reservation(ssh_public_keys: List[str], rules: Dict[str, Any], duration
|
|
|
423
454
|
|
|
424
455
|
environment["variables"].update({"TF_RESERVATION_DURATION": str(duration)})
|
|
425
456
|
|
|
457
|
+
if debug_reservation:
|
|
458
|
+
environment["variables"].update({"TF_RESERVATION_DEBUG": "1"})
|
|
459
|
+
|
|
426
460
|
if "tmt" not in environment or environment["tmt"] is None:
|
|
427
461
|
environment["tmt"] = {"extra_args": {}}
|
|
428
462
|
|
|
@@ -649,21 +683,25 @@ def _print_summary_table(summary: dict, format: Optional[WatchFormat], show_deta
|
|
|
649
683
|
|
|
650
684
|
|
|
651
685
|
def watch(
|
|
652
|
-
|
|
686
|
+
context: typer.Context,
|
|
687
|
+
api_url: str = ARGUMENT_API_URL,
|
|
653
688
|
id: str = typer.Option(..., help="Request ID to watch"),
|
|
654
689
|
no_wait: bool = typer.Option(False, help="Skip waiting for request completion."),
|
|
655
690
|
format: Optional[WatchFormat] = typer.Option(WatchFormat.text, help="Output format"),
|
|
656
691
|
autoconnect: bool = typer.Option(True, hidden=True),
|
|
657
692
|
reserve: bool = typer.Option(False, hidden=True),
|
|
658
693
|
):
|
|
694
|
+
"""Watch request for completion."""
|
|
695
|
+
|
|
696
|
+
# Accept these arguments only via environment variables
|
|
697
|
+
check_unexpected_arguments(context, "api_url")
|
|
698
|
+
|
|
659
699
|
def _console_print(*args, **kwargs):
|
|
660
700
|
"""A helper function that will skip printing to console if output format is json"""
|
|
661
701
|
if format == WatchFormat.json:
|
|
662
702
|
return
|
|
663
703
|
console.print(*args, **kwargs)
|
|
664
704
|
|
|
665
|
-
"""Watch request for completion."""
|
|
666
|
-
|
|
667
705
|
if not uuid_valid(id):
|
|
668
706
|
exit_error("invalid request id")
|
|
669
707
|
|
|
@@ -720,7 +758,7 @@ def watch(
|
|
|
720
758
|
if state == current_state:
|
|
721
759
|
# check for reservation status and finish early if reserved
|
|
722
760
|
if reserve and _is_reserved(session, request):
|
|
723
|
-
_handle_reservation(session, request["id"], autoconnect)
|
|
761
|
+
_handle_reservation(session, api_url, request["id"], autoconnect)
|
|
724
762
|
return
|
|
725
763
|
|
|
726
764
|
time.sleep(1)
|
|
@@ -770,6 +808,10 @@ def watch(
|
|
|
770
808
|
_print_summary_table(request_summary, format)
|
|
771
809
|
raise typer.Exit(code=2)
|
|
772
810
|
|
|
811
|
+
elif state in ["canceled", "cancel-requested"]:
|
|
812
|
+
_console_print("⚠️ pipeline cancelled", style="yellow")
|
|
813
|
+
raise typer.Exit(code=3)
|
|
814
|
+
|
|
773
815
|
if no_wait:
|
|
774
816
|
_print_summary_table(request_summary, format, show_details=False)
|
|
775
817
|
raise typer.Exit()
|
|
@@ -783,6 +825,7 @@ def version():
|
|
|
783
825
|
|
|
784
826
|
|
|
785
827
|
def request(
|
|
828
|
+
context: typer.Context,
|
|
786
829
|
api_url: str = ARGUMENT_API_URL,
|
|
787
830
|
api_token: str = ARGUMENT_API_TOKEN,
|
|
788
831
|
timeout: int = typer.Option(
|
|
@@ -877,10 +920,15 @@ def request(
|
|
|
877
920
|
ssh_public_keys: List[str] = _option_ssh_public_keys(REQUEST_PANEL_RESERVE),
|
|
878
921
|
autoconnect: bool = _option_autoconnect(REQUEST_PANEL_RESERVE),
|
|
879
922
|
reservation_duration: int = _option_reservation_duration(REQUEST_PANEL_RESERVE),
|
|
923
|
+
debug_reservation: bool = _option_debug_reservation(REQUEST_PANEL_RESERVE),
|
|
880
924
|
):
|
|
881
925
|
"""
|
|
882
926
|
Request testing from Testing Farm.
|
|
883
927
|
"""
|
|
928
|
+
|
|
929
|
+
# Accept these arguments only via environment variables
|
|
930
|
+
check_unexpected_arguments(context, "api_url", "api_token")
|
|
931
|
+
|
|
884
932
|
# Split comma separated arches
|
|
885
933
|
arches = normalize_multistring_option(arches)
|
|
886
934
|
|
|
@@ -1016,6 +1064,8 @@ def request(
|
|
|
1016
1064
|
environment["hardware"] = hw_constraints(hardware)
|
|
1017
1065
|
|
|
1018
1066
|
if kickstart:
|
|
1067
|
+
# Typer escapes newlines in options, we need to unescape them
|
|
1068
|
+
kickstart = [codecs.decode(value, 'unicode_escape') for value in kickstart] # pyre-ignore[6]
|
|
1019
1069
|
environment["kickstart"] = options_to_dict("environment kickstart", kickstart)
|
|
1020
1070
|
|
|
1021
1071
|
if redhat_brew_build:
|
|
@@ -1066,7 +1116,11 @@ def request(
|
|
|
1066
1116
|
|
|
1067
1117
|
for environment in environments:
|
|
1068
1118
|
_add_reservation(
|
|
1069
|
-
ssh_public_keys=ssh_public_keys,
|
|
1119
|
+
ssh_public_keys=ssh_public_keys,
|
|
1120
|
+
rules=rules,
|
|
1121
|
+
duration=reservation_duration,
|
|
1122
|
+
environment=environment,
|
|
1123
|
+
debug_reservation=debug_reservation,
|
|
1070
1124
|
)
|
|
1071
1125
|
|
|
1072
1126
|
machine_pre = "Machine" if len(environments) == 1 else str(len(environments)) + " machines"
|
|
@@ -1116,7 +1170,7 @@ def request(
|
|
|
1116
1170
|
request["environments"] = environments
|
|
1117
1171
|
request["settings"] = {}
|
|
1118
1172
|
|
|
1119
|
-
if reserve or pipeline_type or parallel_limit:
|
|
1173
|
+
if reserve or pipeline_type or parallel_limit or timeout != DEFAULT_PIPELINE_TIMEOUT:
|
|
1120
1174
|
request["settings"]["pipeline"] = {}
|
|
1121
1175
|
|
|
1122
1176
|
# in case the reservation duration is more than the pipeline timeout, adjust also the pipeline timeout
|
|
@@ -1128,6 +1182,11 @@ def request(
|
|
|
1128
1182
|
request["settings"]["pipeline"] = {"timeout": timeout}
|
|
1129
1183
|
console.print(f"⏳ Maximum reservation time is {timeout} minutes")
|
|
1130
1184
|
|
|
1185
|
+
# forced pipeline timeout
|
|
1186
|
+
elif timeout != DEFAULT_PIPELINE_TIMEOUT:
|
|
1187
|
+
console.print(f"⏳ Pipeline timeout forced to {timeout} minutes")
|
|
1188
|
+
request["settings"]["pipeline"] = {"timeout": timeout}
|
|
1189
|
+
|
|
1131
1190
|
if pipeline_type:
|
|
1132
1191
|
request["settings"]["pipeline"]["type"] = pipeline_type.value
|
|
1133
1192
|
|
|
@@ -1173,7 +1232,7 @@ def request(
|
|
|
1173
1232
|
request_id = response.json()['id']
|
|
1174
1233
|
|
|
1175
1234
|
# Watch the request and handle reservation
|
|
1176
|
-
watch(api_url, request_id, no_wait, reserve=reserve, autoconnect=autoconnect, format=WatchFormat.text)
|
|
1235
|
+
watch(context, api_url, request_id, no_wait, reserve=reserve, autoconnect=autoconnect, format=WatchFormat.text)
|
|
1177
1236
|
|
|
1178
1237
|
|
|
1179
1238
|
def restart(
|
|
@@ -1217,6 +1276,7 @@ def restart(
|
|
|
1217
1276
|
ssh_public_keys: List[str] = _option_ssh_public_keys(REQUEST_PANEL_RESERVE),
|
|
1218
1277
|
autoconnect: bool = _option_autoconnect(REQUEST_PANEL_RESERVE),
|
|
1219
1278
|
reservation_duration: int = _option_reservation_duration(REQUEST_PANEL_RESERVE),
|
|
1279
|
+
debug_reservation: bool = _option_debug_reservation(REQUEST_PANEL_RESERVE),
|
|
1220
1280
|
):
|
|
1221
1281
|
"""
|
|
1222
1282
|
Restart a Testing Farm request.
|
|
@@ -1224,6 +1284,9 @@ def restart(
|
|
|
1224
1284
|
Just pass a request ID or an URL with a request ID to restart it.
|
|
1225
1285
|
"""
|
|
1226
1286
|
|
|
1287
|
+
# Accept these arguments only via environment variables
|
|
1288
|
+
check_unexpected_arguments(context, "api_url", "api_token", "internal_api_url")
|
|
1289
|
+
|
|
1227
1290
|
# UUID pattern
|
|
1228
1291
|
uuid_pattern = re.compile('[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}')
|
|
1229
1292
|
|
|
@@ -1401,7 +1464,11 @@ def restart(
|
|
|
1401
1464
|
|
|
1402
1465
|
for environment in request["environments"]:
|
|
1403
1466
|
_add_reservation(
|
|
1404
|
-
ssh_public_keys=ssh_public_keys,
|
|
1467
|
+
ssh_public_keys=ssh_public_keys,
|
|
1468
|
+
rules=rules,
|
|
1469
|
+
duration=reservation_duration,
|
|
1470
|
+
environment=environment,
|
|
1471
|
+
debug_reservation=debug_reservation,
|
|
1405
1472
|
)
|
|
1406
1473
|
|
|
1407
1474
|
machine_pre = (
|
|
@@ -1437,11 +1504,18 @@ def restart(
|
|
|
1437
1504
|
|
|
1438
1505
|
# watch
|
|
1439
1506
|
watch(
|
|
1440
|
-
|
|
1507
|
+
context,
|
|
1508
|
+
str(api_url),
|
|
1509
|
+
response.json()['id'],
|
|
1510
|
+
no_wait,
|
|
1511
|
+
reserve=reserve,
|
|
1512
|
+
autoconnect=autoconnect,
|
|
1513
|
+
format=WatchFormat.text,
|
|
1441
1514
|
)
|
|
1442
1515
|
|
|
1443
1516
|
|
|
1444
1517
|
def run(
|
|
1518
|
+
context: typer.Context,
|
|
1445
1519
|
arch: str = typer.Option("x86_64", "--arch", help="Hardware platform of the target machine."),
|
|
1446
1520
|
compose: Optional[str] = typer.Option(
|
|
1447
1521
|
None,
|
|
@@ -1453,6 +1527,10 @@ def run(
|
|
|
1453
1527
|
secrets: Optional[List[str]] = OPTION_SECRETS,
|
|
1454
1528
|
dry_run: bool = OPTION_DRY_RUN,
|
|
1455
1529
|
verbose: bool = typer.Option(False, help="Be verbose."),
|
|
1530
|
+
# NOTE: we cannot use ARGUMENT_API_* because it would collide with command,
|
|
1531
|
+
# so use rather OPTION variants for this command
|
|
1532
|
+
api_url: str = OPTION_API_URL,
|
|
1533
|
+
api_token: str = OPTION_API_TOKEN,
|
|
1456
1534
|
command: List[str] = typer.Argument(..., help="Command to run. Use `--` to separate COMMAND from CLI options."),
|
|
1457
1535
|
):
|
|
1458
1536
|
"""
|
|
@@ -1460,7 +1538,7 @@ def run(
|
|
|
1460
1538
|
"""
|
|
1461
1539
|
|
|
1462
1540
|
# check for token
|
|
1463
|
-
if not
|
|
1541
|
+
if not api_token:
|
|
1464
1542
|
exit_error("No API token found, export `TESTING_FARM_API_TOKEN` environment variable.")
|
|
1465
1543
|
|
|
1466
1544
|
# create request
|
|
@@ -1494,7 +1572,7 @@ def run(
|
|
|
1494
1572
|
request["environments"] = [environment]
|
|
1495
1573
|
|
|
1496
1574
|
# submit request to Testing Farm
|
|
1497
|
-
post_url = urllib.parse.urljoin(
|
|
1575
|
+
post_url = urllib.parse.urljoin(api_url, "v0.1/requests")
|
|
1498
1576
|
|
|
1499
1577
|
# Setting up retries
|
|
1500
1578
|
session = requests.Session()
|
|
@@ -1508,7 +1586,7 @@ def run(
|
|
|
1508
1586
|
raise typer.Exit()
|
|
1509
1587
|
|
|
1510
1588
|
# handle errors
|
|
1511
|
-
response = session.post(post_url, json=request, headers=_get_headers(
|
|
1589
|
+
response = session.post(post_url, json=request, headers=_get_headers(api_token))
|
|
1512
1590
|
if response.status_code == 401:
|
|
1513
1591
|
exit_error(f"API token is invalid. See {settings.ONBOARDING_DOCS} for more information.")
|
|
1514
1592
|
|
|
@@ -1520,7 +1598,7 @@ def run(
|
|
|
1520
1598
|
exit_error(f"Unexpected error. Please file an issue to {settings.ISSUE_TRACKER}.")
|
|
1521
1599
|
|
|
1522
1600
|
id = response.json()['id']
|
|
1523
|
-
get_url = urllib.parse.urljoin(
|
|
1601
|
+
get_url = urllib.parse.urljoin(api_url, f"/v0.1/requests/{id}")
|
|
1524
1602
|
|
|
1525
1603
|
if verbose:
|
|
1526
1604
|
console.print(f"🔎 api [blue]{get_url}[/blue]")
|
|
@@ -1560,6 +1638,10 @@ def run(
|
|
|
1560
1638
|
if state in ["complete", "error"]:
|
|
1561
1639
|
break
|
|
1562
1640
|
|
|
1641
|
+
if state in ["canceled", "cancel-requested"]:
|
|
1642
|
+
progress.stop()
|
|
1643
|
+
exit_error("Request canceled.")
|
|
1644
|
+
|
|
1563
1645
|
time.sleep(1)
|
|
1564
1646
|
|
|
1565
1647
|
# workaround TFT-1690
|
|
@@ -1600,6 +1682,9 @@ def run(
|
|
|
1600
1682
|
|
|
1601
1683
|
|
|
1602
1684
|
def reserve(
|
|
1685
|
+
context: typer.Context,
|
|
1686
|
+
api_url: str = ARGUMENT_API_URL,
|
|
1687
|
+
api_token: str = ARGUMENT_API_TOKEN,
|
|
1603
1688
|
ssh_public_keys: List[str] = _option_ssh_public_keys(RESERVE_PANEL_GENERAL),
|
|
1604
1689
|
reservation_duration: int = _option_reservation_duration(RESERVE_PANEL_GENERAL),
|
|
1605
1690
|
arch: str = typer.Option(
|
|
@@ -1619,6 +1704,9 @@ def reserve(
|
|
|
1619
1704
|
repository: List[str] = OPTION_REPOSITORY,
|
|
1620
1705
|
repository_file: List[str] = OPTION_REPOSITORY_FILE,
|
|
1621
1706
|
redhat_brew_build: List[str] = OPTION_REDHAT_BREW_BUILD,
|
|
1707
|
+
tmt_discover: Optional[List[str]] = _generate_tmt_extra_args("discover"),
|
|
1708
|
+
tmt_prepare: Optional[List[str]] = _generate_tmt_extra_args("prepare"),
|
|
1709
|
+
tmt_finish: Optional[List[str]] = _generate_tmt_extra_args("finish"),
|
|
1622
1710
|
dry_run: bool = OPTION_DRY_RUN,
|
|
1623
1711
|
post_install_script: Optional[str] = OPTION_POST_INSTALL_SCRIPT,
|
|
1624
1712
|
print_only_request_id: bool = typer.Option(
|
|
@@ -1636,6 +1724,7 @@ def reserve(
|
|
|
1636
1724
|
git_ref: Optional[str] = typer.Option(
|
|
1637
1725
|
None, help="Force GIT ref or branch. Useful for testing changes to reservation plan."
|
|
1638
1726
|
),
|
|
1727
|
+
debug_reservation: bool = _option_debug_reservation(),
|
|
1639
1728
|
):
|
|
1640
1729
|
"""
|
|
1641
1730
|
Reserve a system in Testing Farm.
|
|
@@ -1647,6 +1736,9 @@ def reserve(
|
|
|
1647
1736
|
|
|
1648
1737
|
_sanity_reserve()
|
|
1649
1738
|
|
|
1739
|
+
# Accept these arguments only via environment variables
|
|
1740
|
+
check_unexpected_arguments(context, "api_url", "api_token")
|
|
1741
|
+
|
|
1650
1742
|
# check for token
|
|
1651
1743
|
if not settings.API_TOKEN:
|
|
1652
1744
|
exit_error("No API token found, export `TESTING_FARM_API_TOKEN` environment variable.")
|
|
@@ -1693,6 +1785,8 @@ def reserve(
|
|
|
1693
1785
|
environment["settings"]["provisioning"]["tags"] = options_to_dict("tags", tags)
|
|
1694
1786
|
|
|
1695
1787
|
if kickstart:
|
|
1788
|
+
# Typer escapes newlines in options, we need to unescape them
|
|
1789
|
+
kickstart = [codecs.decode(value, 'unicode_escape') for value in kickstart] # pyre-ignore[6]
|
|
1696
1790
|
environment["kickstart"] = options_to_dict("environment kickstart", kickstart)
|
|
1697
1791
|
|
|
1698
1792
|
if redhat_brew_build:
|
|
@@ -1713,6 +1807,18 @@ def reserve(
|
|
|
1713
1807
|
if post_install_script:
|
|
1714
1808
|
environment["settings"]["provisioning"]["post_install_script"] = post_install_script
|
|
1715
1809
|
|
|
1810
|
+
if tmt_discover or tmt_prepare or tmt_finish:
|
|
1811
|
+
environment["tmt"] = {"extra_args": {}}
|
|
1812
|
+
|
|
1813
|
+
if tmt_discover:
|
|
1814
|
+
environment["tmt"]["extra_args"]["discover"] = tmt_discover
|
|
1815
|
+
|
|
1816
|
+
if tmt_prepare:
|
|
1817
|
+
environment["tmt"]["extra_args"]["prepare"] = tmt_prepare
|
|
1818
|
+
|
|
1819
|
+
if tmt_finish:
|
|
1820
|
+
environment["tmt"]["extra_args"]["finish"] = tmt_finish
|
|
1821
|
+
|
|
1716
1822
|
# Setting up retries
|
|
1717
1823
|
session = requests.Session()
|
|
1718
1824
|
install_http_retries(session)
|
|
@@ -1726,7 +1832,14 @@ def reserve(
|
|
|
1726
1832
|
environment["settings"]["provisioning"].update(rules)
|
|
1727
1833
|
|
|
1728
1834
|
console.print(f"🕗 Reserved for [blue]{str(reservation_duration)}[/blue] minutes")
|
|
1729
|
-
|
|
1835
|
+
|
|
1836
|
+
if "variables" not in environment or environment["variables"] is None:
|
|
1837
|
+
environment["variables"] = {}
|
|
1838
|
+
|
|
1839
|
+
environment["variables"]["TF_RESERVATION_DURATION"] = str(reservation_duration)
|
|
1840
|
+
|
|
1841
|
+
if debug_reservation:
|
|
1842
|
+
environment["variables"]["TF_RESERVATION_DEBUG"] = "1"
|
|
1730
1843
|
|
|
1731
1844
|
authorized_keys = read_glob_paths(ssh_public_keys).encode("utf-8")
|
|
1732
1845
|
if not authorized_keys:
|
|
@@ -1755,7 +1868,7 @@ def reserve(
|
|
|
1755
1868
|
console.print(f"⏳ Maximum reservation time is {DEFAULT_PIPELINE_TIMEOUT} minutes")
|
|
1756
1869
|
|
|
1757
1870
|
# submit request to Testing Farm
|
|
1758
|
-
post_url = urllib.parse.urljoin(
|
|
1871
|
+
post_url = urllib.parse.urljoin(api_url, "v0.1/requests")
|
|
1759
1872
|
|
|
1760
1873
|
# dry run
|
|
1761
1874
|
if dry_run:
|
|
@@ -1767,7 +1880,7 @@ def reserve(
|
|
|
1767
1880
|
raise typer.Exit()
|
|
1768
1881
|
|
|
1769
1882
|
# handle errors
|
|
1770
|
-
response = session.post(post_url, json=request, headers=_get_headers(
|
|
1883
|
+
response = session.post(post_url, json=request, headers=_get_headers(api_token))
|
|
1771
1884
|
if response.status_code == 401:
|
|
1772
1885
|
exit_error(f"API token is invalid. See {settings.ONBOARDING_DOCS} for more information.")
|
|
1773
1886
|
|
|
@@ -1782,7 +1895,7 @@ def reserve(
|
|
|
1782
1895
|
exit_error(f"Unexpected error. Please file an issue to {settings.ISSUE_TRACKER}.")
|
|
1783
1896
|
|
|
1784
1897
|
id = response.json()['id']
|
|
1785
|
-
get_url = urllib.parse.urljoin(
|
|
1898
|
+
get_url = urllib.parse.urljoin(api_url, f"/v0.1/requests/{id}")
|
|
1786
1899
|
|
|
1787
1900
|
if not print_only_request_id:
|
|
1788
1901
|
console.print(f"🔎 [blue]{get_url}[/blue]")
|
|
@@ -1826,7 +1939,11 @@ def reserve(
|
|
|
1826
1939
|
current_state = state
|
|
1827
1940
|
|
|
1828
1941
|
if state in ["complete", "error"]:
|
|
1829
|
-
exit_error("Reservation failed, check API request or contact Testing Farm")
|
|
1942
|
+
exit_error("Reservation failed, check the API request or contact Testing Farm.")
|
|
1943
|
+
|
|
1944
|
+
if state in ["canceled", "cancel-requested"]:
|
|
1945
|
+
progress.stop()
|
|
1946
|
+
exit_error("Reservation canceled.")
|
|
1830
1947
|
|
|
1831
1948
|
if not print_only_request_id and task_id is not None:
|
|
1832
1949
|
progress.update(task_id, description=f"Reservation job is [yellow]{current_state}[/yellow]")
|
|
@@ -1881,6 +1998,10 @@ def reserve(
|
|
|
1881
1998
|
)
|
|
1882
1999
|
)
|
|
1883
2000
|
|
|
2001
|
+
if '[testing-farm-request] Cancelling pipeline' in pipeline_log:
|
|
2002
|
+
progress.stop()
|
|
2003
|
+
exit_error('Pipeline was canceled.')
|
|
2004
|
+
|
|
1884
2005
|
if '[pre-artifact-installation]' in pipeline_log:
|
|
1885
2006
|
current_state = "preparing environment"
|
|
1886
2007
|
|
|
@@ -1899,7 +2020,7 @@ def reserve(
|
|
|
1899
2020
|
console.print(f"🌎 ssh root@{guest}")
|
|
1900
2021
|
|
|
1901
2022
|
if autoconnect:
|
|
1902
|
-
os.system(f"
|
|
2023
|
+
os.system(f"{SSH_RESERVATION_OPTIONS} root@{guest}") # noqa: E501
|
|
1903
2024
|
|
|
1904
2025
|
|
|
1905
2026
|
def update():
|
|
@@ -1911,6 +2032,7 @@ def update():
|
|
|
1911
2032
|
|
|
1912
2033
|
|
|
1913
2034
|
def cancel(
|
|
2035
|
+
context: typer.Context,
|
|
1914
2036
|
request_id: str = typer.Argument(
|
|
1915
2037
|
..., help="Testing Farm request to cancel. Specified by a request ID or a string containing it."
|
|
1916
2038
|
),
|
|
@@ -1921,6 +2043,9 @@ def cancel(
|
|
|
1921
2043
|
Cancel a Testing Farm request.
|
|
1922
2044
|
"""
|
|
1923
2045
|
|
|
2046
|
+
# Accept these arguments only via environment variables
|
|
2047
|
+
check_unexpected_arguments(context, "api_url", "api_token")
|
|
2048
|
+
|
|
1924
2049
|
# UUID pattern
|
|
1925
2050
|
uuid_pattern = re.compile('[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}')
|
|
1926
2051
|
|
|
@@ -1967,6 +2092,7 @@ def cancel(
|
|
|
1967
2092
|
|
|
1968
2093
|
|
|
1969
2094
|
def encrypt(
|
|
2095
|
+
context: typer.Context,
|
|
1970
2096
|
message: str = typer.Argument(..., help="Message to be encrypted."),
|
|
1971
2097
|
api_url: str = ARGUMENT_API_URL,
|
|
1972
2098
|
api_token: str = ARGUMENT_API_TOKEN,
|
|
@@ -1984,6 +2110,9 @@ def encrypt(
|
|
|
1984
2110
|
Create secrets for use in in-repository configuration.
|
|
1985
2111
|
"""
|
|
1986
2112
|
|
|
2113
|
+
# Accept these arguments only via environment variables
|
|
2114
|
+
check_unexpected_arguments(context, "api_url", "api_token")
|
|
2115
|
+
|
|
1987
2116
|
# check for token
|
|
1988
2117
|
if not api_token:
|
|
1989
2118
|
exit_error("No API token found, export `TESTING_FARM_API_TOKEN` environment variable")
|
tft/cli/utils.py
CHANGED
|
@@ -13,6 +13,7 @@ from typing import Any, Dict, List, NoReturn, Optional, Union
|
|
|
13
13
|
import requests
|
|
14
14
|
import requests.adapters
|
|
15
15
|
import typer
|
|
16
|
+
from click.core import ParameterSource # pyre-ignore[21]
|
|
16
17
|
from rich.console import Console
|
|
17
18
|
from ruamel.yaml import YAML
|
|
18
19
|
from urllib3 import Retry
|
|
@@ -89,6 +90,20 @@ def hw_constraints(hardware: List[str]) -> Dict[Any, Any]:
|
|
|
89
90
|
constraints[first_key].append(new_dict)
|
|
90
91
|
continue
|
|
91
92
|
|
|
93
|
+
# Special handling for CPU flags as they are also a list
|
|
94
|
+
if first_key == 'cpu' and len(path_splitted) == 2 and path_splitted[1] == 'flag':
|
|
95
|
+
second_key = 'flag'
|
|
96
|
+
|
|
97
|
+
if first_key not in constraints:
|
|
98
|
+
constraints[first_key] = {}
|
|
99
|
+
|
|
100
|
+
if second_key not in constraints[first_key]:
|
|
101
|
+
constraints[first_key][second_key] = []
|
|
102
|
+
|
|
103
|
+
constraints[first_key][second_key].append(value)
|
|
104
|
+
|
|
105
|
+
continue
|
|
106
|
+
|
|
92
107
|
# Walk the path, step by step, and initialize containers along the way. The last step is not
|
|
93
108
|
# a name of another nested container, but actually a name in the last container.
|
|
94
109
|
container: Any = constraints
|
|
@@ -244,3 +259,12 @@ def read_glob_paths(glob_paths: List[str]) -> str:
|
|
|
244
259
|
contents.append(file.read())
|
|
245
260
|
|
|
246
261
|
return ''.join(contents)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def check_unexpected_arguments(context: typer.Context, *args: str) -> Union[None, NoReturn]:
|
|
265
|
+
for argument in args:
|
|
266
|
+
if context.get_parameter_source(argument) == ParameterSource.COMMANDLINE: # pyre-ignore[16]
|
|
267
|
+
exit_error(
|
|
268
|
+
f"Unexpected argument '{context.params.get(argument)}'. "
|
|
269
|
+
"Please make sure you are passing the parameters correctly."
|
|
270
|
+
)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
tft/cli/__init__.py,sha256=uEJkNJbqC583PBtNI30kxWdeOr3Wj6zJzIYKf0AD72I,92
|
|
2
|
+
tft/cli/commands.py,sha256=5fcoNlJ_K1r6j99qoWKh9hJM2nJ3xY1Tp6vL4wcdQxk,82322
|
|
3
|
+
tft/cli/config.py,sha256=JiVLrgM4REWdljA9DjAuH4fNR1ekE7Crv2oM6vBPt9Q,1254
|
|
4
|
+
tft/cli/tool.py,sha256=nuz57u3yE4fKgdMgNtAFM7PCTcF6PSNFBQbCYDzxBOw,897
|
|
5
|
+
tft/cli/utils.py,sha256=t3ZSnviGxVbBQ4u4ltwQwRgN647BvuyL1QrSlQAAT1Q,9136
|
|
6
|
+
tft_cli-0.0.25.dist-info/LICENSE,sha256=YpVAQfXkIyzQAdm5LZkI6L5UWqLppa6O8_tgDSdoabQ,574
|
|
7
|
+
tft_cli-0.0.25.dist-info/METADATA,sha256=tIo9HRQmpNjq58ru63CRTun0owhYGucYQDFuWhB_fFE,789
|
|
8
|
+
tft_cli-0.0.25.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
|
|
9
|
+
tft_cli-0.0.25.dist-info/entry_points.txt,sha256=xzdebHkH5Bx-YRf-XPMsIoVpvgfUqqcRQGuo8DFkiao,49
|
|
10
|
+
tft_cli-0.0.25.dist-info/RECORD,,
|
tft_cli-0.0.23.dist-info/RECORD
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
tft/cli/__init__.py,sha256=uEJkNJbqC583PBtNI30kxWdeOr3Wj6zJzIYKf0AD72I,92
|
|
2
|
-
tft/cli/commands.py,sha256=7ngkjZDlabd44NiCzQunUUIbBLWbm61eTMhCyqV2gfQ,77778
|
|
3
|
-
tft/cli/config.py,sha256=JiVLrgM4REWdljA9DjAuH4fNR1ekE7Crv2oM6vBPt9Q,1254
|
|
4
|
-
tft/cli/tool.py,sha256=nuz57u3yE4fKgdMgNtAFM7PCTcF6PSNFBQbCYDzxBOw,897
|
|
5
|
-
tft/cli/utils.py,sha256=KAk52TRCqHvrIZEKTpJn7HWskeEU4yvNEVlfnkfgMW4,8191
|
|
6
|
-
tft_cli-0.0.23.dist-info/LICENSE,sha256=YpVAQfXkIyzQAdm5LZkI6L5UWqLppa6O8_tgDSdoabQ,574
|
|
7
|
-
tft_cli-0.0.23.dist-info/METADATA,sha256=_xV2L07TUW8s1Iww24Agl0U7BJ_hVBuwCgbq7nwKGug,789
|
|
8
|
-
tft_cli-0.0.23.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
|
|
9
|
-
tft_cli-0.0.23.dist-info/entry_points.txt,sha256=xzdebHkH5Bx-YRf-XPMsIoVpvgfUqqcRQGuo8DFkiao,49
|
|
10
|
-
tft_cli-0.0.23.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|