tft-cli 0.0.24__tar.gz → 0.0.26__tar.gz
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-0.0.24 → tft_cli-0.0.26}/PKG-INFO +1 -1
- {tft_cli-0.0.24 → tft_cli-0.0.26}/pyproject.toml +10 -3
- {tft_cli-0.0.24 → tft_cli-0.0.26}/src/tft/cli/commands.py +119 -35
- {tft_cli-0.0.24 → tft_cli-0.0.26}/src/tft/cli/config.py +2 -2
- {tft_cli-0.0.24 → tft_cli-0.0.26}/src/tft/cli/utils.py +24 -0
- tft_cli-0.0.24/src/tft/cli/commands.py.backup +0 -2211
- {tft_cli-0.0.24 → tft_cli-0.0.26}/LICENSE +0 -0
- {tft_cli-0.0.24 → tft_cli-0.0.26}/LICENSE_SPDX +0 -0
- {tft_cli-0.0.24 → tft_cli-0.0.26}/src/tft/cli/__init__.py +0 -0
- {tft_cli-0.0.24 → tft_cli-0.0.26}/src/tft/cli/tool.py +0 -0
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
dynamic = ["version"]
|
|
3
|
+
name = "tft-cli"
|
|
4
|
+
|
|
1
5
|
[tool.poetry]
|
|
2
6
|
name = "tft-cli"
|
|
3
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.26"
|
|
4
8
|
description = "Testing Farm CLI tool"
|
|
5
9
|
authors = ["Miroslav Vadkerti <mvadkert@redhat.com>"]
|
|
6
10
|
license = "Apache-2.0"
|
|
@@ -43,6 +47,9 @@ skip-string-normalization = true
|
|
|
43
47
|
profile = "black"
|
|
44
48
|
multi_line_output = 3
|
|
45
49
|
|
|
50
|
+
[tool.poetry-dynamic-versioning]
|
|
51
|
+
enable = false
|
|
52
|
+
|
|
46
53
|
[build-system]
|
|
47
|
-
requires = ["poetry-core>=1.0.0"]
|
|
48
|
-
build-backend = "
|
|
54
|
+
requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"]
|
|
55
|
+
build-backend = "poetry_dynamic_versioning.backend"
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
# SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
4
|
import base64
|
|
5
|
+
import codecs
|
|
6
|
+
import importlib.metadata
|
|
5
7
|
import ipaddress
|
|
6
8
|
import json
|
|
7
9
|
import os
|
|
@@ -16,7 +18,6 @@ import xml.etree.ElementTree as ET
|
|
|
16
18
|
from enum import Enum
|
|
17
19
|
from typing import Any, Dict, List, Optional
|
|
18
20
|
|
|
19
|
-
import pkg_resources
|
|
20
21
|
import requests
|
|
21
22
|
import typer
|
|
22
23
|
from click.core import ParameterSource # pyre-ignore[21]
|
|
@@ -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,
|
|
@@ -39,7 +41,7 @@ from tft.cli.utils import (
|
|
|
39
41
|
uuid_valid,
|
|
40
42
|
)
|
|
41
43
|
|
|
42
|
-
cli_version: str =
|
|
44
|
+
cli_version: str = importlib.metadata.version("tft-cli")
|
|
43
45
|
|
|
44
46
|
TestingFarmRequestV1: Dict[str, Any] = {'test': {}, 'environments': None}
|
|
45
47
|
Environment: Dict[str, Any] = {'arch': None, 'os': None, 'pool': None, 'artifacts': None, 'variables': {}}
|
|
@@ -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",
|
|
@@ -254,6 +271,13 @@ OPTION_RESERVE: bool = typer.Option(
|
|
|
254
271
|
help="Reserve machine after testing, similarly to the `reserve` command.",
|
|
255
272
|
rich_help_panel=REQUEST_PANEL_RESERVE,
|
|
256
273
|
)
|
|
274
|
+
OPTION_TMT_CONTEXT: Optional[List[str]] = typer.Option(
|
|
275
|
+
None,
|
|
276
|
+
"-c",
|
|
277
|
+
"--context",
|
|
278
|
+
metavar="key=value|@file",
|
|
279
|
+
help="Context variables to pass to `tmt`. The @ prefix marks a yaml file to load.",
|
|
280
|
+
)
|
|
257
281
|
|
|
258
282
|
|
|
259
283
|
def _option_autoconnect(panel: str) -> bool:
|
|
@@ -326,12 +350,12 @@ def _sanity_reserve() -> None:
|
|
|
326
350
|
exit_error("No SSH identities found in the SSH agent. Please run `ssh-add`.")
|
|
327
351
|
|
|
328
352
|
|
|
329
|
-
def _handle_reservation(session, request_id: str, autoconnect: bool = False) -> None:
|
|
353
|
+
def _handle_reservation(session, api_url: str, request_id: str, autoconnect: bool = False) -> None:
|
|
330
354
|
"""
|
|
331
355
|
Handle the reservation for :py:func:``request`` and :py:func:``restart`` commands.
|
|
332
356
|
"""
|
|
333
357
|
# Get artifacts url
|
|
334
|
-
request_url = urllib.parse.urljoin(
|
|
358
|
+
request_url = urllib.parse.urljoin(api_url, f"/v0.1/requests/{request_id}")
|
|
335
359
|
response = session.get(request_url)
|
|
336
360
|
artifacts_url = response.json()['run']['artifacts']
|
|
337
361
|
|
|
@@ -386,7 +410,7 @@ def _handle_reservation(session, request_id: str, autoconnect: bool = False) ->
|
|
|
386
410
|
console.print(f"🌎 ssh root@{guests[0]}")
|
|
387
411
|
|
|
388
412
|
if autoconnect:
|
|
389
|
-
os.system(f"
|
|
413
|
+
os.system(f"{SSH_RESERVATION_OPTIONS} root@{guests[0]}") # noqa: E501
|
|
390
414
|
|
|
391
415
|
|
|
392
416
|
def _localhost_ingress_rule(session: requests.Session) -> str:
|
|
@@ -397,7 +421,7 @@ def _localhost_ingress_rule(session: requests.Session) -> str:
|
|
|
397
421
|
|
|
398
422
|
if get_ip.ok:
|
|
399
423
|
ip = get_ip.text.strip()
|
|
400
|
-
return f"-1:{ip}:-1"
|
|
424
|
+
return f"-1:{ip}:-1" # noqa: E231
|
|
401
425
|
|
|
402
426
|
else:
|
|
403
427
|
exit_error(f"Got {get_ip.status_code} while checking {settings.PUBLIC_IP_CHECKER_URL}")
|
|
@@ -666,21 +690,25 @@ def _print_summary_table(summary: dict, format: Optional[WatchFormat], show_deta
|
|
|
666
690
|
|
|
667
691
|
|
|
668
692
|
def watch(
|
|
669
|
-
|
|
693
|
+
context: typer.Context,
|
|
694
|
+
api_url: str = ARGUMENT_API_URL,
|
|
670
695
|
id: str = typer.Option(..., help="Request ID to watch"),
|
|
671
696
|
no_wait: bool = typer.Option(False, help="Skip waiting for request completion."),
|
|
672
697
|
format: Optional[WatchFormat] = typer.Option(WatchFormat.text, help="Output format"),
|
|
673
698
|
autoconnect: bool = typer.Option(True, hidden=True),
|
|
674
699
|
reserve: bool = typer.Option(False, hidden=True),
|
|
675
700
|
):
|
|
701
|
+
"""Watch request for completion."""
|
|
702
|
+
|
|
703
|
+
# Accept these arguments only via environment variables
|
|
704
|
+
check_unexpected_arguments(context, "api_url")
|
|
705
|
+
|
|
676
706
|
def _console_print(*args, **kwargs):
|
|
677
707
|
"""A helper function that will skip printing to console if output format is json"""
|
|
678
708
|
if format == WatchFormat.json:
|
|
679
709
|
return
|
|
680
710
|
console.print(*args, **kwargs)
|
|
681
711
|
|
|
682
|
-
"""Watch request for completion."""
|
|
683
|
-
|
|
684
712
|
if not uuid_valid(id):
|
|
685
713
|
exit_error("invalid request id")
|
|
686
714
|
|
|
@@ -737,10 +765,10 @@ def watch(
|
|
|
737
765
|
if state == current_state:
|
|
738
766
|
# check for reservation status and finish early if reserved
|
|
739
767
|
if reserve and _is_reserved(session, request):
|
|
740
|
-
_handle_reservation(session, request["id"], autoconnect)
|
|
768
|
+
_handle_reservation(session, api_url, request["id"], autoconnect)
|
|
741
769
|
return
|
|
742
770
|
|
|
743
|
-
time.sleep(
|
|
771
|
+
time.sleep(settings.WATCH_TICK)
|
|
744
772
|
continue
|
|
745
773
|
|
|
746
774
|
current_state = state
|
|
@@ -804,6 +832,7 @@ def version():
|
|
|
804
832
|
|
|
805
833
|
|
|
806
834
|
def request(
|
|
835
|
+
context: typer.Context,
|
|
807
836
|
api_url: str = ARGUMENT_API_URL,
|
|
808
837
|
api_token: str = ARGUMENT_API_TOKEN,
|
|
809
838
|
timeout: int = typer.Option(
|
|
@@ -839,13 +868,7 @@ def request(
|
|
|
839
868
|
hardware: List[str] = OPTION_HARDWARE,
|
|
840
869
|
kickstart: Optional[List[str]] = OPTION_KICKSTART,
|
|
841
870
|
pool: Optional[str] = OPTION_POOL,
|
|
842
|
-
cli_tmt_context: Optional[List[str]] =
|
|
843
|
-
None,
|
|
844
|
-
"-c",
|
|
845
|
-
"--context",
|
|
846
|
-
metavar="key=value|@file",
|
|
847
|
-
help="Context variables to pass to `tmt`. The @ prefix marks a yaml file to load.",
|
|
848
|
-
),
|
|
871
|
+
cli_tmt_context: Optional[List[str]] = OPTION_TMT_CONTEXT,
|
|
849
872
|
variables: Optional[List[str]] = OPTION_VARIABLES,
|
|
850
873
|
secrets: Optional[List[str]] = OPTION_SECRETS,
|
|
851
874
|
tmt_environment: Optional[List[str]] = typer.Option(
|
|
@@ -903,6 +926,10 @@ def request(
|
|
|
903
926
|
"""
|
|
904
927
|
Request testing from Testing Farm.
|
|
905
928
|
"""
|
|
929
|
+
|
|
930
|
+
# Accept these arguments only via environment variables
|
|
931
|
+
check_unexpected_arguments(context, "api_url", "api_token")
|
|
932
|
+
|
|
906
933
|
# Split comma separated arches
|
|
907
934
|
arches = normalize_multistring_option(arches)
|
|
908
935
|
|
|
@@ -1038,6 +1065,8 @@ def request(
|
|
|
1038
1065
|
environment["hardware"] = hw_constraints(hardware)
|
|
1039
1066
|
|
|
1040
1067
|
if kickstart:
|
|
1068
|
+
# Typer escapes newlines in options, we need to unescape them
|
|
1069
|
+
kickstart = [codecs.decode(value, 'unicode_escape') for value in kickstart] # pyre-ignore[6]
|
|
1041
1070
|
environment["kickstart"] = options_to_dict("environment kickstart", kickstart)
|
|
1042
1071
|
|
|
1043
1072
|
if redhat_brew_build:
|
|
@@ -1204,7 +1233,7 @@ def request(
|
|
|
1204
1233
|
request_id = response.json()['id']
|
|
1205
1234
|
|
|
1206
1235
|
# Watch the request and handle reservation
|
|
1207
|
-
watch(api_url, request_id, no_wait, reserve=reserve, autoconnect=autoconnect, format=WatchFormat.text)
|
|
1236
|
+
watch(context, api_url, request_id, no_wait, reserve=reserve, autoconnect=autoconnect, format=WatchFormat.text)
|
|
1208
1237
|
|
|
1209
1238
|
|
|
1210
1239
|
def restart(
|
|
@@ -1226,6 +1255,8 @@ def restart(
|
|
|
1226
1255
|
None,
|
|
1227
1256
|
help="Force pool to provision.",
|
|
1228
1257
|
),
|
|
1258
|
+
cli_tmt_context: Optional[List[str]] = OPTION_TMT_CONTEXT,
|
|
1259
|
+
variables: Optional[List[str]] = OPTION_VARIABLES,
|
|
1229
1260
|
git_url: Optional[str] = typer.Option(None, help="Force URL of the GIT repository to test."),
|
|
1230
1261
|
git_ref: Optional[str] = typer.Option(None, help="Force GIT ref or branch to test."),
|
|
1231
1262
|
git_merge_sha: Optional[str] = typer.Option(None, help="Force GIT ref or branch into which --ref will be merged."),
|
|
@@ -1256,6 +1287,9 @@ def restart(
|
|
|
1256
1287
|
Just pass a request ID or an URL with a request ID to restart it.
|
|
1257
1288
|
"""
|
|
1258
1289
|
|
|
1290
|
+
# Accept these arguments only via environment variables
|
|
1291
|
+
check_unexpected_arguments(context, "api_url", "api_token", "internal_api_url")
|
|
1292
|
+
|
|
1259
1293
|
# UUID pattern
|
|
1260
1294
|
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}')
|
|
1261
1295
|
|
|
@@ -1301,9 +1335,9 @@ def restart(
|
|
|
1301
1335
|
# Transform to a request
|
|
1302
1336
|
request['environments'] = request['environments_requested']
|
|
1303
1337
|
|
|
1304
|
-
# Remove all keys except test and
|
|
1338
|
+
# Remove all keys except test, environments and settings
|
|
1305
1339
|
for key in list(request):
|
|
1306
|
-
if key not in ['test', 'environments']:
|
|
1340
|
+
if key not in ['test', 'environments', 'settings']:
|
|
1307
1341
|
del request[key]
|
|
1308
1342
|
|
|
1309
1343
|
test = request['test']
|
|
@@ -1375,6 +1409,14 @@ def restart(
|
|
|
1375
1409
|
for environment in request["environments"]:
|
|
1376
1410
|
environment["tmt"]["extra_args"]["finish"] = tmt_finish
|
|
1377
1411
|
|
|
1412
|
+
if cli_tmt_context:
|
|
1413
|
+
for environment in request["environments"]:
|
|
1414
|
+
environment["tmt"]["context"] = options_to_dict("tmt context", cli_tmt_context)
|
|
1415
|
+
|
|
1416
|
+
if variables:
|
|
1417
|
+
for environment in request["environments"]:
|
|
1418
|
+
environment["variables"] = options_to_dict("environment variables", variables)
|
|
1419
|
+
|
|
1378
1420
|
test_type = "fmf" if "fmf" in request["test"] else "sti"
|
|
1379
1421
|
|
|
1380
1422
|
if tmt_plan_name:
|
|
@@ -1473,11 +1515,18 @@ def restart(
|
|
|
1473
1515
|
|
|
1474
1516
|
# watch
|
|
1475
1517
|
watch(
|
|
1476
|
-
|
|
1518
|
+
context,
|
|
1519
|
+
str(api_url),
|
|
1520
|
+
response.json()['id'],
|
|
1521
|
+
no_wait,
|
|
1522
|
+
reserve=reserve,
|
|
1523
|
+
autoconnect=autoconnect,
|
|
1524
|
+
format=WatchFormat.text,
|
|
1477
1525
|
)
|
|
1478
1526
|
|
|
1479
1527
|
|
|
1480
1528
|
def run(
|
|
1529
|
+
context: typer.Context,
|
|
1481
1530
|
arch: str = typer.Option("x86_64", "--arch", help="Hardware platform of the target machine."),
|
|
1482
1531
|
compose: Optional[str] = typer.Option(
|
|
1483
1532
|
None,
|
|
@@ -1489,6 +1538,10 @@ def run(
|
|
|
1489
1538
|
secrets: Optional[List[str]] = OPTION_SECRETS,
|
|
1490
1539
|
dry_run: bool = OPTION_DRY_RUN,
|
|
1491
1540
|
verbose: bool = typer.Option(False, help="Be verbose."),
|
|
1541
|
+
# NOTE: we cannot use ARGUMENT_API_* because it would collide with command,
|
|
1542
|
+
# so use rather OPTION variants for this command
|
|
1543
|
+
api_url: str = OPTION_API_URL,
|
|
1544
|
+
api_token: str = OPTION_API_TOKEN,
|
|
1492
1545
|
command: List[str] = typer.Argument(..., help="Command to run. Use `--` to separate COMMAND from CLI options."),
|
|
1493
1546
|
):
|
|
1494
1547
|
"""
|
|
@@ -1496,7 +1549,7 @@ def run(
|
|
|
1496
1549
|
"""
|
|
1497
1550
|
|
|
1498
1551
|
# check for token
|
|
1499
|
-
if not
|
|
1552
|
+
if not api_token:
|
|
1500
1553
|
exit_error("No API token found, export `TESTING_FARM_API_TOKEN` environment variable.")
|
|
1501
1554
|
|
|
1502
1555
|
# create request
|
|
@@ -1530,7 +1583,7 @@ def run(
|
|
|
1530
1583
|
request["environments"] = [environment]
|
|
1531
1584
|
|
|
1532
1585
|
# submit request to Testing Farm
|
|
1533
|
-
post_url = urllib.parse.urljoin(
|
|
1586
|
+
post_url = urllib.parse.urljoin(api_url, "v0.1/requests")
|
|
1534
1587
|
|
|
1535
1588
|
# Setting up retries
|
|
1536
1589
|
session = requests.Session()
|
|
@@ -1544,7 +1597,7 @@ def run(
|
|
|
1544
1597
|
raise typer.Exit()
|
|
1545
1598
|
|
|
1546
1599
|
# handle errors
|
|
1547
|
-
response = session.post(post_url, json=request, headers=_get_headers(
|
|
1600
|
+
response = session.post(post_url, json=request, headers=_get_headers(api_token))
|
|
1548
1601
|
if response.status_code == 401:
|
|
1549
1602
|
exit_error(f"API token is invalid. See {settings.ONBOARDING_DOCS} for more information.")
|
|
1550
1603
|
|
|
@@ -1556,7 +1609,7 @@ def run(
|
|
|
1556
1609
|
exit_error(f"Unexpected error. Please file an issue to {settings.ISSUE_TRACKER}.")
|
|
1557
1610
|
|
|
1558
1611
|
id = response.json()['id']
|
|
1559
|
-
get_url = urllib.parse.urljoin(
|
|
1612
|
+
get_url = urllib.parse.urljoin(api_url, f"/v0.1/requests/{id}")
|
|
1560
1613
|
|
|
1561
1614
|
if verbose:
|
|
1562
1615
|
console.print(f"🔎 api [blue]{get_url}[/blue]")
|
|
@@ -1588,7 +1641,7 @@ def run(
|
|
|
1588
1641
|
state = request["state"]
|
|
1589
1642
|
|
|
1590
1643
|
if state == current_state:
|
|
1591
|
-
time.sleep(
|
|
1644
|
+
time.sleep(settings.WATCH_TICK)
|
|
1592
1645
|
continue
|
|
1593
1646
|
|
|
1594
1647
|
current_state = state
|
|
@@ -1600,7 +1653,7 @@ def run(
|
|
|
1600
1653
|
progress.stop()
|
|
1601
1654
|
exit_error("Request canceled.")
|
|
1602
1655
|
|
|
1603
|
-
time.sleep(
|
|
1656
|
+
time.sleep(settings.WATCH_TICK)
|
|
1604
1657
|
|
|
1605
1658
|
# workaround TFT-1690
|
|
1606
1659
|
install_http_retries(session, status_forcelist_extend=[404], timeout=60, retry_backoff_factor=0.1)
|
|
@@ -1640,6 +1693,9 @@ def run(
|
|
|
1640
1693
|
|
|
1641
1694
|
|
|
1642
1695
|
def reserve(
|
|
1696
|
+
context: typer.Context,
|
|
1697
|
+
api_url: str = ARGUMENT_API_URL,
|
|
1698
|
+
api_token: str = ARGUMENT_API_TOKEN,
|
|
1643
1699
|
ssh_public_keys: List[str] = _option_ssh_public_keys(RESERVE_PANEL_GENERAL),
|
|
1644
1700
|
reservation_duration: int = _option_reservation_duration(RESERVE_PANEL_GENERAL),
|
|
1645
1701
|
arch: str = typer.Option(
|
|
@@ -1659,6 +1715,9 @@ def reserve(
|
|
|
1659
1715
|
repository: List[str] = OPTION_REPOSITORY,
|
|
1660
1716
|
repository_file: List[str] = OPTION_REPOSITORY_FILE,
|
|
1661
1717
|
redhat_brew_build: List[str] = OPTION_REDHAT_BREW_BUILD,
|
|
1718
|
+
tmt_discover: Optional[List[str]] = _generate_tmt_extra_args("discover"),
|
|
1719
|
+
tmt_prepare: Optional[List[str]] = _generate_tmt_extra_args("prepare"),
|
|
1720
|
+
tmt_finish: Optional[List[str]] = _generate_tmt_extra_args("finish"),
|
|
1662
1721
|
dry_run: bool = OPTION_DRY_RUN,
|
|
1663
1722
|
post_install_script: Optional[str] = OPTION_POST_INSTALL_SCRIPT,
|
|
1664
1723
|
print_only_request_id: bool = typer.Option(
|
|
@@ -1688,6 +1747,9 @@ def reserve(
|
|
|
1688
1747
|
|
|
1689
1748
|
_sanity_reserve()
|
|
1690
1749
|
|
|
1750
|
+
# Accept these arguments only via environment variables
|
|
1751
|
+
check_unexpected_arguments(context, "api_url", "api_token")
|
|
1752
|
+
|
|
1691
1753
|
# check for token
|
|
1692
1754
|
if not settings.API_TOKEN:
|
|
1693
1755
|
exit_error("No API token found, export `TESTING_FARM_API_TOKEN` environment variable.")
|
|
@@ -1734,6 +1796,8 @@ def reserve(
|
|
|
1734
1796
|
environment["settings"]["provisioning"]["tags"] = options_to_dict("tags", tags)
|
|
1735
1797
|
|
|
1736
1798
|
if kickstart:
|
|
1799
|
+
# Typer escapes newlines in options, we need to unescape them
|
|
1800
|
+
kickstart = [codecs.decode(value, 'unicode_escape') for value in kickstart] # pyre-ignore[6]
|
|
1737
1801
|
environment["kickstart"] = options_to_dict("environment kickstart", kickstart)
|
|
1738
1802
|
|
|
1739
1803
|
if redhat_brew_build:
|
|
@@ -1754,6 +1818,18 @@ def reserve(
|
|
|
1754
1818
|
if post_install_script:
|
|
1755
1819
|
environment["settings"]["provisioning"]["post_install_script"] = post_install_script
|
|
1756
1820
|
|
|
1821
|
+
if tmt_discover or tmt_prepare or tmt_finish:
|
|
1822
|
+
environment["tmt"] = {"extra_args": {}}
|
|
1823
|
+
|
|
1824
|
+
if tmt_discover:
|
|
1825
|
+
environment["tmt"]["extra_args"]["discover"] = tmt_discover
|
|
1826
|
+
|
|
1827
|
+
if tmt_prepare:
|
|
1828
|
+
environment["tmt"]["extra_args"]["prepare"] = tmt_prepare
|
|
1829
|
+
|
|
1830
|
+
if tmt_finish:
|
|
1831
|
+
environment["tmt"]["extra_args"]["finish"] = tmt_finish
|
|
1832
|
+
|
|
1757
1833
|
# Setting up retries
|
|
1758
1834
|
session = requests.Session()
|
|
1759
1835
|
install_http_retries(session)
|
|
@@ -1803,7 +1879,7 @@ def reserve(
|
|
|
1803
1879
|
console.print(f"⏳ Maximum reservation time is {DEFAULT_PIPELINE_TIMEOUT} minutes")
|
|
1804
1880
|
|
|
1805
1881
|
# submit request to Testing Farm
|
|
1806
|
-
post_url = urllib.parse.urljoin(
|
|
1882
|
+
post_url = urllib.parse.urljoin(api_url, "v0.1/requests")
|
|
1807
1883
|
|
|
1808
1884
|
# dry run
|
|
1809
1885
|
if dry_run:
|
|
@@ -1815,7 +1891,7 @@ def reserve(
|
|
|
1815
1891
|
raise typer.Exit()
|
|
1816
1892
|
|
|
1817
1893
|
# handle errors
|
|
1818
|
-
response = session.post(post_url, json=request, headers=_get_headers(
|
|
1894
|
+
response = session.post(post_url, json=request, headers=_get_headers(api_token))
|
|
1819
1895
|
if response.status_code == 401:
|
|
1820
1896
|
exit_error(f"API token is invalid. See {settings.ONBOARDING_DOCS} for more information.")
|
|
1821
1897
|
|
|
@@ -1830,7 +1906,7 @@ def reserve(
|
|
|
1830
1906
|
exit_error(f"Unexpected error. Please file an issue to {settings.ISSUE_TRACKER}.")
|
|
1831
1907
|
|
|
1832
1908
|
id = response.json()['id']
|
|
1833
|
-
get_url = urllib.parse.urljoin(
|
|
1909
|
+
get_url = urllib.parse.urljoin(api_url, f"/v0.1/requests/{id}")
|
|
1834
1910
|
|
|
1835
1911
|
if not print_only_request_id:
|
|
1836
1912
|
console.print(f"🔎 [blue]{get_url}[/blue]")
|
|
@@ -1868,7 +1944,7 @@ def reserve(
|
|
|
1868
1944
|
state = request["state"]
|
|
1869
1945
|
|
|
1870
1946
|
if state == current_state:
|
|
1871
|
-
time.sleep(
|
|
1947
|
+
time.sleep(settings.WATCH_TICK)
|
|
1872
1948
|
continue
|
|
1873
1949
|
|
|
1874
1950
|
current_state = state
|
|
@@ -1883,7 +1959,7 @@ def reserve(
|
|
|
1883
1959
|
if not print_only_request_id and task_id is not None:
|
|
1884
1960
|
progress.update(task_id, description=f"Reservation job is [yellow]{current_state}[/yellow]")
|
|
1885
1961
|
|
|
1886
|
-
time.sleep(
|
|
1962
|
+
time.sleep(settings.WATCH_TICK)
|
|
1887
1963
|
|
|
1888
1964
|
while current_state != "ready":
|
|
1889
1965
|
if not print_only_request_id and task_id:
|
|
@@ -1950,12 +2026,12 @@ def reserve(
|
|
|
1950
2026
|
current_state = "ready"
|
|
1951
2027
|
guest = search.group(1)
|
|
1952
2028
|
|
|
1953
|
-
time.sleep(
|
|
2029
|
+
time.sleep(settings.WATCH_TICK)
|
|
1954
2030
|
|
|
1955
2031
|
console.print(f"🌎 ssh root@{guest}")
|
|
1956
2032
|
|
|
1957
2033
|
if autoconnect:
|
|
1958
|
-
os.system(f"
|
|
2034
|
+
os.system(f"{SSH_RESERVATION_OPTIONS} root@{guest}") # noqa: E501
|
|
1959
2035
|
|
|
1960
2036
|
|
|
1961
2037
|
def update():
|
|
@@ -1967,6 +2043,7 @@ def update():
|
|
|
1967
2043
|
|
|
1968
2044
|
|
|
1969
2045
|
def cancel(
|
|
2046
|
+
context: typer.Context,
|
|
1970
2047
|
request_id: str = typer.Argument(
|
|
1971
2048
|
..., help="Testing Farm request to cancel. Specified by a request ID or a string containing it."
|
|
1972
2049
|
),
|
|
@@ -1977,6 +2054,9 @@ def cancel(
|
|
|
1977
2054
|
Cancel a Testing Farm request.
|
|
1978
2055
|
"""
|
|
1979
2056
|
|
|
2057
|
+
# Accept these arguments only via environment variables
|
|
2058
|
+
check_unexpected_arguments(context, "api_url", "api_token")
|
|
2059
|
+
|
|
1980
2060
|
# UUID pattern
|
|
1981
2061
|
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}')
|
|
1982
2062
|
|
|
@@ -2023,6 +2103,7 @@ def cancel(
|
|
|
2023
2103
|
|
|
2024
2104
|
|
|
2025
2105
|
def encrypt(
|
|
2106
|
+
context: typer.Context,
|
|
2026
2107
|
message: str = typer.Argument(..., help="Message to be encrypted."),
|
|
2027
2108
|
api_url: str = ARGUMENT_API_URL,
|
|
2028
2109
|
api_token: str = ARGUMENT_API_TOKEN,
|
|
@@ -2040,6 +2121,9 @@ def encrypt(
|
|
|
2040
2121
|
Create secrets for use in in-repository configuration.
|
|
2041
2122
|
"""
|
|
2042
2123
|
|
|
2124
|
+
# Accept these arguments only via environment variables
|
|
2125
|
+
check_unexpected_arguments(context, "api_url", "api_token")
|
|
2126
|
+
|
|
2043
2127
|
# check for token
|
|
2044
2128
|
if not api_token:
|
|
2045
2129
|
exit_error("No API token found, export `TESTING_FARM_API_TOKEN` environment variable")
|
|
@@ -12,9 +12,9 @@ settings = LazySettings(
|
|
|
12
12
|
API_TOKEN=None,
|
|
13
13
|
ISSUE_TRACKER="https://gitlab.com/testing-farm/general/-/issues/new",
|
|
14
14
|
STATUS_PAGE="https://status.testing-farm.io",
|
|
15
|
-
ONBOARDING_DOCS="https://docs.testing-farm.io/
|
|
15
|
+
ONBOARDING_DOCS="https://docs.testing-farm.io/Testing%20Farm/0.1/onboarding.html",
|
|
16
16
|
CONTAINER_SIGN="/.testing-farm-container",
|
|
17
|
-
WATCH_TICK=
|
|
17
|
+
WATCH_TICK=30,
|
|
18
18
|
DEFAULT_API_TIMEOUT=10,
|
|
19
19
|
DEFAULT_API_RETRIES=7,
|
|
20
20
|
# default reservation duration in minutes
|
|
@@ -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
|
+
)
|