tft-cli 0.0.25__py3-none-any.whl → 0.0.28__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 CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  import base64
5
5
  import codecs
6
+ import importlib.metadata
6
7
  import ipaddress
7
8
  import json
8
9
  import os
@@ -17,22 +18,24 @@ import xml.etree.ElementTree as ET
17
18
  from enum import Enum
18
19
  from typing import Any, Dict, List, Optional
19
20
 
20
- import pkg_resources
21
21
  import requests
22
22
  import typer
23
- from click.core import ParameterSource # pyre-ignore[21]
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,
@@ -41,7 +44,7 @@ from tft.cli.utils import (
41
44
  uuid_valid,
42
45
  )
43
46
 
44
- cli_version: str = pkg_resources.get_distribution("tft-cli").version
47
+ cli_version: str = importlib.metadata.version("tft-cli")
45
48
 
46
49
  TestingFarmRequestV1: Dict[str, Any] = {'test': {}, 'environments': None}
47
50
  Environment: Dict[str, Any] = {'arch': None, 'os': None, 'pool': None, 'artifacts': None, 'variables': {}}
@@ -66,6 +69,7 @@ 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
 
68
71
  DEFAULT_PIPELINE_TIMEOUT = 60 * 12
72
+ DEFAULT_AGE = "7d"
69
73
 
70
74
  # SSH command options for reservation connections
71
75
  SSH_RESERVATION_OPTIONS = (
@@ -85,6 +89,20 @@ class PipelineType(str, Enum):
85
89
  tmt_multihost = "tmt-multihost"
86
90
 
87
91
 
92
+ class PipelineState(str, Enum):
93
+ new = "new"
94
+ queued = "queued"
95
+ running = "running"
96
+ complete = "complete"
97
+ error = "error"
98
+ canceled = "canceled"
99
+
100
+
101
+ class Ranch(str, Enum):
102
+ public = "public"
103
+ redhat = "redhat"
104
+
105
+
88
106
  # Arguments and options that are shared among multiple commands
89
107
  ARGUMENT_API_URL: str = typer.Argument(
90
108
  settings.API_URL, envvar="TESTING_FARM_API_URL", metavar='', rich_help_panel='Environment variables'
@@ -99,6 +117,12 @@ ARGUMENT_API_TOKEN: str = typer.Argument(
99
117
  metavar='',
100
118
  rich_help_panel='Environment variables',
101
119
  )
120
+ ARGUMENT_INTERNAL_API_URL: str = typer.Argument(
121
+ settings.INTERNAL_API_URL,
122
+ envvar="TESTING_FARM_INTERNAL_API_URL",
123
+ metavar='',
124
+ rich_help_panel='Environment variables',
125
+ )
102
126
  OPTION_API_TOKEN: str = typer.Option(
103
127
  settings.API_TOKEN,
104
128
  envvar="TESTING_FARM_API_TOKEN",
@@ -106,6 +130,33 @@ OPTION_API_TOKEN: str = typer.Option(
106
130
  metavar='',
107
131
  rich_help_panel='Environment variables',
108
132
  )
133
+
134
+ # Restart command specific arguments for source operations
135
+ ARGUMENT_SOURCE_API_URL: str = typer.Argument(
136
+ None, envvar="TESTING_FARM_SOURCE_API_URL", metavar='', rich_help_panel='Environment variables'
137
+ )
138
+ ARGUMENT_INTERNAL_SOURCE_API_URL: str = typer.Argument(
139
+ None, envvar="TESTING_FARM_INTERNAL_SOURCE_API_URL", metavar='', rich_help_panel='Environment variables'
140
+ )
141
+ ARGUMENT_SOURCE_API_TOKEN: str = typer.Argument(
142
+ None,
143
+ envvar="TESTING_FARM_SOURCE_API_TOKEN",
144
+ show_default=False,
145
+ metavar='',
146
+ rich_help_panel='Environment variables',
147
+ )
148
+
149
+ # Restart command specific arguments for target operations
150
+ ARGUMENT_TARGET_API_URL: str = typer.Argument(
151
+ None, envvar="TESTING_FARM_TARGET_API_URL", metavar='', rich_help_panel='Environment variables'
152
+ )
153
+ ARGUMENT_TARGET_API_TOKEN: str = typer.Argument(
154
+ None,
155
+ envvar="TESTING_FARM_TARGET_API_TOKEN",
156
+ show_default=False,
157
+ metavar='',
158
+ rich_help_panel='Environment variables',
159
+ )
109
160
  OPTION_TMT_PLAN_NAME: Optional[str] = typer.Option(
110
161
  None,
111
162
  "--plan",
@@ -271,6 +322,13 @@ OPTION_RESERVE: bool = typer.Option(
271
322
  help="Reserve machine after testing, similarly to the `reserve` command.",
272
323
  rich_help_panel=REQUEST_PANEL_RESERVE,
273
324
  )
325
+ OPTION_TMT_CONTEXT: Optional[List[str]] = typer.Option(
326
+ None,
327
+ "-c",
328
+ "--context",
329
+ metavar="key=value|@file",
330
+ help="Context variables to pass to `tmt`. The @ prefix marks a yaml file to load.",
331
+ )
274
332
 
275
333
 
276
334
  def _option_autoconnect(panel: str) -> bool:
@@ -527,14 +585,6 @@ def _parse_security_group_rules(ingress_rules: List[str], egress_rules: List[str
527
585
  return security_group_rules
528
586
 
529
587
 
530
- def _get_headers(api_key: str) -> Dict[str, str]:
531
- """
532
- Return a dict with headers for a request to Testing Farm API.
533
- Used for authentication.
534
- """
535
- return {'Authorization': f'Bearer {api_key}'}
536
-
537
-
538
588
  def _parse_xunit(xunit: str):
539
589
  """
540
590
  A helper that parses xunit file into sets of passed_plans/failed_plans/errored_plans per arch.
@@ -761,7 +811,7 @@ def watch(
761
811
  _handle_reservation(session, api_url, request["id"], autoconnect)
762
812
  return
763
813
 
764
- time.sleep(1)
814
+ time.sleep(settings.WATCH_TICK)
765
815
  continue
766
816
 
767
817
  current_state = state
@@ -861,13 +911,7 @@ def request(
861
911
  hardware: List[str] = OPTION_HARDWARE,
862
912
  kickstart: Optional[List[str]] = OPTION_KICKSTART,
863
913
  pool: Optional[str] = OPTION_POOL,
864
- cli_tmt_context: Optional[List[str]] = typer.Option(
865
- None,
866
- "-c",
867
- "--context",
868
- metavar="key=value|@file",
869
- help="Context variables to pass to `tmt`. The @ prefix marks a yaml file to load.",
870
- ),
914
+ cli_tmt_context: Optional[List[str]] = OPTION_TMT_CONTEXT,
871
915
  variables: Optional[List[str]] = OPTION_VARIABLES,
872
916
  secrets: Optional[List[str]] = OPTION_SECRETS,
873
917
  tmt_environment: Optional[List[str]] = typer.Option(
@@ -1065,7 +1109,7 @@ def request(
1065
1109
 
1066
1110
  if kickstart:
1067
1111
  # Typer escapes newlines in options, we need to unescape them
1068
- kickstart = [codecs.decode(value, 'unicode_escape') for value in kickstart] # pyre-ignore[6]
1112
+ kickstart = [codecs.decode(value, 'unicode_escape') for value in kickstart]
1069
1113
  environment["kickstart"] = options_to_dict("environment kickstart", kickstart)
1070
1114
 
1071
1115
  if redhat_brew_build:
@@ -1112,7 +1156,10 @@ def request(
1112
1156
  if len(environments) > 1:
1113
1157
  exit_error("Reservations are currently supported for a single plan, cannot continue")
1114
1158
 
1115
- rules = _parse_security_group_rules([_localhost_ingress_rule(session)], [])
1159
+ # support cases where the user has multiple localhost addresses
1160
+ rules = _parse_security_group_rules(
1161
+ list({_localhost_ingress_rule(requests.Session()) for _ in range(0, settings.PUBLIC_IP_RESOLVE_TRIES)}), []
1162
+ )
1116
1163
 
1117
1164
  for environment in environments:
1118
1165
  _add_reservation(
@@ -1215,7 +1262,7 @@ def request(
1215
1262
  raise typer.Exit()
1216
1263
 
1217
1264
  # handle errors
1218
- response = session.post(post_url, json=request, headers=_get_headers(api_token))
1265
+ response = session.post(post_url, json=request, headers=authorization_headers(api_token))
1219
1266
  if response.status_code == 401:
1220
1267
  exit_error(f"API token is invalid. See {settings.ONBOARDING_DOCS} for more information.")
1221
1268
 
@@ -1239,13 +1286,13 @@ def restart(
1239
1286
  context: typer.Context,
1240
1287
  request_id: str = typer.Argument(..., help="Testing Farm request ID or a string containing it."),
1241
1288
  api_url: str = ARGUMENT_API_URL,
1242
- internal_api_url: str = typer.Argument(
1243
- settings.INTERNAL_API_URL,
1244
- envvar="TESTING_FARM_INTERNAL_API_URL",
1245
- metavar='',
1246
- rich_help_panel='Environment variables',
1247
- ),
1289
+ internal_api_url: str = ARGUMENT_INTERNAL_API_URL,
1248
1290
  api_token: str = ARGUMENT_API_TOKEN,
1291
+ source_api_url: Optional[str] = ARGUMENT_SOURCE_API_URL,
1292
+ internal_source_api_url: Optional[str] = ARGUMENT_INTERNAL_SOURCE_API_URL,
1293
+ source_api_token: Optional[str] = ARGUMENT_SOURCE_API_TOKEN,
1294
+ target_api_url: Optional[str] = ARGUMENT_TARGET_API_URL,
1295
+ target_api_token: Optional[str] = ARGUMENT_TARGET_API_TOKEN,
1249
1296
  compose: Optional[str] = typer.Option(
1250
1297
  None,
1251
1298
  help="Force compose used to provision test environment.", # noqa
@@ -1254,6 +1301,8 @@ def restart(
1254
1301
  None,
1255
1302
  help="Force pool to provision.",
1256
1303
  ),
1304
+ cli_tmt_context: Optional[List[str]] = OPTION_TMT_CONTEXT,
1305
+ variables: Optional[List[str]] = OPTION_VARIABLES,
1257
1306
  git_url: Optional[str] = typer.Option(None, help="Force URL of the GIT repository to test."),
1258
1307
  git_ref: Optional[str] = typer.Option(None, help="Force GIT ref or branch to test."),
1259
1308
  git_merge_sha: Optional[str] = typer.Option(None, help="Force GIT ref or branch into which --ref will be merged."),
@@ -1277,6 +1326,13 @@ def restart(
1277
1326
  autoconnect: bool = _option_autoconnect(REQUEST_PANEL_RESERVE),
1278
1327
  reservation_duration: int = _option_reservation_duration(REQUEST_PANEL_RESERVE),
1279
1328
  debug_reservation: bool = _option_debug_reservation(REQUEST_PANEL_RESERVE),
1329
+ edit: bool = typer.Option(
1330
+ False,
1331
+ help=(
1332
+ "Edit the request JSON in editor before submitting. "
1333
+ "Use the EDITOR environment variable to adjust the editor if needed."
1334
+ ),
1335
+ ),
1280
1336
  ):
1281
1337
  """
1282
1338
  Restart a Testing Farm request.
@@ -1285,30 +1341,39 @@ def restart(
1285
1341
  """
1286
1342
 
1287
1343
  # Accept these arguments only via environment variables
1288
- check_unexpected_arguments(context, "api_url", "api_token", "internal_api_url")
1289
-
1290
- # UUID pattern
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}')
1344
+ check_unexpected_arguments(
1345
+ context,
1346
+ "api_url",
1347
+ "api_token",
1348
+ "internal_api_url",
1349
+ "source_api_url",
1350
+ "internal_source_api_url",
1351
+ "source_api_token",
1352
+ "target_api_url",
1353
+ "target_api_token",
1354
+ )
1292
1355
 
1293
- # Find the UUID in the string
1294
- uuid_match = uuid_pattern.search(request_id)
1356
+ # Determine source configuration (fallback to general settings)
1357
+ effective_source_api_url = source_api_url or api_url
1358
+ effective_internal_source_api_url = internal_source_api_url or internal_api_url
1359
+ effective_source_api_token = source_api_token or api_token
1295
1360
 
1296
- if not uuid_match:
1297
- exit_error(f"Could not find a valid Testing Farm request id in '{request_id}'.")
1298
- return
1361
+ # Determine target configuration (fallback to general settings)
1362
+ effective_target_api_url = target_api_url or api_url
1363
+ effective_target_api_token = target_api_token or api_token
1299
1364
 
1300
- # Extract the UUID from the match object
1301
- _request_id = uuid_match.group()
1365
+ # Extract the UUID from the request_id string
1366
+ _request_id = extract_uuid(request_id)
1302
1367
 
1303
1368
  # Construct URL to the internal API
1304
- get_url = urllib.parse.urljoin(str(internal_api_url), f"v0.1/requests/{_request_id}")
1369
+ get_url = urllib.parse.urljoin(str(effective_internal_source_api_url), f"v0.1/requests/{_request_id}")
1305
1370
 
1306
1371
  # Setting up retries
1307
1372
  session = requests.Session()
1308
1373
  install_http_retries(session)
1309
1374
 
1310
1375
  # Get the request details
1311
- response = session.get(get_url, headers=_get_headers(api_token))
1376
+ response = session.get(get_url, headers=authorization_headers(effective_source_api_token))
1312
1377
 
1313
1378
  if response.status_code == 401:
1314
1379
  exit_error(f"API token is invalid. See {settings.ONBOARDING_DOCS} for more information.")
@@ -1316,10 +1381,11 @@ def restart(
1316
1381
  # The API token is valid, but it doesn't own the request
1317
1382
  if response.status_code == 403:
1318
1383
  console.print(
1319
- "⚠️ [yellow] You are not the owner of this request. Any secrets associated with the request will not be included on the restart.[/yellow]" # noqa: E501
1384
+ "⚠️ [yellow] You are not the owner of this request. Any secrets associated with the "
1385
+ "request will not be included on the restart.[/yellow]"
1320
1386
  )
1321
- # Construct URL to the internal API
1322
- get_url = urllib.parse.urljoin(str(api_url), f"v0.1/requests/{_request_id}")
1387
+ # Construct URL to the API
1388
+ get_url = urllib.parse.urljoin(str(effective_source_api_url), f"v0.1/requests/{_request_id}")
1323
1389
 
1324
1390
  # Get the request details
1325
1391
  response = session.get(get_url)
@@ -1332,9 +1398,9 @@ def restart(
1332
1398
  # Transform to a request
1333
1399
  request['environments'] = request['environments_requested']
1334
1400
 
1335
- # Remove all keys except test and environments
1401
+ # Remove all keys except test, environments and settings
1336
1402
  for key in list(request):
1337
- if key not in ['test', 'environments']:
1403
+ if key not in ['test', 'environments', 'settings']:
1338
1404
  del request[key]
1339
1405
 
1340
1406
  test = request['test']
@@ -1406,6 +1472,14 @@ def restart(
1406
1472
  for environment in request["environments"]:
1407
1473
  environment["tmt"]["extra_args"]["finish"] = tmt_finish
1408
1474
 
1475
+ if cli_tmt_context:
1476
+ for environment in request["environments"]:
1477
+ environment["tmt"]["context"] = options_to_dict("tmt context", cli_tmt_context)
1478
+
1479
+ if variables:
1480
+ for environment in request["environments"]:
1481
+ environment["variables"] = options_to_dict("environment variables", variables)
1482
+
1409
1483
  test_type = "fmf" if "fmf" in request["test"] else "sti"
1410
1484
 
1411
1485
  if tmt_plan_name:
@@ -1420,7 +1494,7 @@ def restart(
1420
1494
 
1421
1495
  if test_type == "fmf":
1422
1496
  # The method explained in https://github.com/fastapi/typer/discussions/668
1423
- if context.get_parameter_source("tmt_path") == ParameterSource.COMMANDLINE: # pyre-ignore[16]
1497
+ if context.get_parameter_source("tmt_path") == ParameterSource.COMMANDLINE:
1424
1498
  request["test"][test_type]["path"] = tmt_path
1425
1499
 
1426
1500
  # worker image
@@ -1460,7 +1534,10 @@ def restart(
1460
1534
  if len(request["environments"]) > 1:
1461
1535
  exit_error("Reservations are currently supported for a single plan, cannot continue")
1462
1536
 
1463
- rules = _parse_security_group_rules([_localhost_ingress_rule(session)], [])
1537
+ # support cases where the user has multiple localhost addresses
1538
+ rules = _parse_security_group_rules(
1539
+ list({_localhost_ingress_rule(requests.Session()) for _ in range(0, settings.PUBLIC_IP_RESOLVE_TRIES)}), []
1540
+ )
1464
1541
 
1465
1542
  for environment in request["environments"]:
1466
1543
  _add_reservation(
@@ -1478,6 +1555,18 @@ def restart(
1478
1555
  f"🕗 {machine_pre} will be reserved after testing for [blue]{str(reservation_duration)}[/blue] minutes"
1479
1556
  )
1480
1557
 
1558
+ # edit request if requested
1559
+ if edit:
1560
+ while True:
1561
+ try:
1562
+ request = json.loads(edit_with_editor(json.dumps(request, indent=2), "editing request"))
1563
+ break
1564
+ except (TypeError, ValueError) as error:
1565
+ console.print(f"⛔ Edited request is not a valid JSON, cannot continue: {error}", style="red")
1566
+ if typer.confirm("❓️ Edit again?"):
1567
+ continue
1568
+ raise typer.Exit(code=255)
1569
+
1481
1570
  # dry run
1482
1571
  if dry_run:
1483
1572
  console.print("🔍 Dry run, showing POST json only", style="bright_yellow")
@@ -1485,10 +1574,10 @@ def restart(
1485
1574
  raise typer.Exit()
1486
1575
 
1487
1576
  # submit request to Testing Farm
1488
- post_url = urllib.parse.urljoin(str(api_url), "v0.1/requests")
1577
+ post_url = urllib.parse.urljoin(str(effective_target_api_url), "v0.1/requests")
1489
1578
 
1490
1579
  # handle errors
1491
- response = session.post(post_url, json=request, headers=_get_headers(api_token))
1580
+ response = session.post(post_url, json=request, headers=authorization_headers(effective_target_api_token))
1492
1581
  if response.status_code == 401:
1493
1582
  exit_error(f"API token is invalid. See {settings.ONBOARDING_DOCS} for more information.")
1494
1583
 
@@ -1505,7 +1594,7 @@ def restart(
1505
1594
  # watch
1506
1595
  watch(
1507
1596
  context,
1508
- str(api_url),
1597
+ str(effective_target_api_url),
1509
1598
  response.json()['id'],
1510
1599
  no_wait,
1511
1600
  reserve=reserve,
@@ -1586,7 +1675,7 @@ def run(
1586
1675
  raise typer.Exit()
1587
1676
 
1588
1677
  # handle errors
1589
- response = session.post(post_url, json=request, headers=_get_headers(api_token))
1678
+ response = session.post(post_url, json=request, headers=authorization_headers(api_token))
1590
1679
  if response.status_code == 401:
1591
1680
  exit_error(f"API token is invalid. See {settings.ONBOARDING_DOCS} for more information.")
1592
1681
 
@@ -1630,7 +1719,7 @@ def run(
1630
1719
  state = request["state"]
1631
1720
 
1632
1721
  if state == current_state:
1633
- time.sleep(1)
1722
+ time.sleep(settings.WATCH_TICK)
1634
1723
  continue
1635
1724
 
1636
1725
  current_state = state
@@ -1642,7 +1731,7 @@ def run(
1642
1731
  progress.stop()
1643
1732
  exit_error("Request canceled.")
1644
1733
 
1645
- time.sleep(1)
1734
+ time.sleep(settings.WATCH_TICK)
1646
1735
 
1647
1736
  # workaround TFT-1690
1648
1737
  install_http_retries(session, status_forcelist_extend=[404], timeout=60, retry_backoff_factor=0.1)
@@ -1786,7 +1875,7 @@ def reserve(
1786
1875
 
1787
1876
  if kickstart:
1788
1877
  # Typer escapes newlines in options, we need to unescape them
1789
- kickstart = [codecs.decode(value, 'unicode_escape') for value in kickstart] # pyre-ignore[6]
1878
+ kickstart = [codecs.decode(value, 'unicode_escape') for value in kickstart]
1790
1879
  environment["kickstart"] = options_to_dict("environment kickstart", kickstart)
1791
1880
 
1792
1881
  if redhat_brew_build:
@@ -1826,7 +1915,10 @@ def reserve(
1826
1915
  if not skip_workstation_access or security_group_rule_ingress or security_group_rule_egress:
1827
1916
  ingress_rules = security_group_rule_ingress or []
1828
1917
  if not skip_workstation_access:
1829
- ingress_rules.append(_localhost_ingress_rule(session))
1918
+ # support cases where the user has multiple localhost addresses
1919
+ ingress_rules.extend(
1920
+ {_localhost_ingress_rule(requests.Session()) for _ in range(0, settings.PUBLIC_IP_RESOLVE_TRIES)}
1921
+ )
1830
1922
 
1831
1923
  rules = _parse_security_group_rules(ingress_rules, security_group_rule_egress or [])
1832
1924
  environment["settings"]["provisioning"].update(rules)
@@ -1880,7 +1972,7 @@ def reserve(
1880
1972
  raise typer.Exit()
1881
1973
 
1882
1974
  # handle errors
1883
- response = session.post(post_url, json=request, headers=_get_headers(api_token))
1975
+ response = session.post(post_url, json=request, headers=authorization_headers(api_token))
1884
1976
  if response.status_code == 401:
1885
1977
  exit_error(f"API token is invalid. See {settings.ONBOARDING_DOCS} for more information.")
1886
1978
 
@@ -1933,7 +2025,7 @@ def reserve(
1933
2025
  state = request["state"]
1934
2026
 
1935
2027
  if state == current_state:
1936
- time.sleep(1)
2028
+ time.sleep(settings.WATCH_TICK)
1937
2029
  continue
1938
2030
 
1939
2031
  current_state = state
@@ -1948,7 +2040,7 @@ def reserve(
1948
2040
  if not print_only_request_id and task_id is not None:
1949
2041
  progress.update(task_id, description=f"Reservation job is [yellow]{current_state}[/yellow]")
1950
2042
 
1951
- time.sleep(1)
2043
+ time.sleep(settings.WATCH_TICK)
1952
2044
 
1953
2045
  while current_state != "ready":
1954
2046
  if not print_only_request_id and task_id:
@@ -2015,7 +2107,7 @@ def reserve(
2015
2107
  current_state = "ready"
2016
2108
  guest = search.group(1)
2017
2109
 
2018
- time.sleep(1)
2110
+ time.sleep(settings.WATCH_TICK)
2019
2111
 
2020
2112
  console.print(f"🌎 ssh root@{guest}")
2021
2113
 
@@ -2046,23 +2138,13 @@ def cancel(
2046
2138
  # Accept these arguments only via environment variables
2047
2139
  check_unexpected_arguments(context, "api_url", "api_token")
2048
2140
 
2049
- # UUID pattern
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}')
2051
-
2052
- # Find the UUID in the string
2053
- uuid_match = uuid_pattern.search(request_id)
2054
-
2055
- if not uuid_match:
2056
- exit_error(f"Could not find a valid Testing Farm request id in '{request_id}'.")
2057
- return
2141
+ # Extract the UUID from the request_id string
2142
+ _request_id = extract_uuid(request_id)
2058
2143
 
2059
2144
  if not api_token:
2060
2145
  exit_error("No API token found in the environment, please export 'TESTING_FARM_API_TOKEN' variable.")
2061
2146
  return
2062
2147
 
2063
- # Extract the UUID from the match object
2064
- _request_id = uuid_match.group()
2065
-
2066
2148
  # Construct URL to the internal API
2067
2149
  request_url = urllib.parse.urljoin(str(api_url), f"v0.1/requests/{_request_id}")
2068
2150
 
@@ -2071,11 +2153,17 @@ def cancel(
2071
2153
  install_http_retries(session)
2072
2154
 
2073
2155
  # Get the request details
2074
- response = session.delete(request_url, headers=_get_headers(api_token))
2156
+ response = session.delete(request_url, headers=authorization_headers(api_token))
2075
2157
 
2076
2158
  if response.status_code == 401:
2077
2159
  exit_error(f"API token is invalid. See {settings.ONBOARDING_DOCS} for more information.")
2078
2160
 
2161
+ if response.status_code == 403:
2162
+ exit_error(
2163
+ "You cannot cancel foreign requests. You can only cancel your own requests "
2164
+ "or must have 'admin' permissions."
2165
+ )
2166
+
2079
2167
  if response.status_code == 404:
2080
2168
  exit_error("Request was not found. Verify the request ID is correct.")
2081
2169
 
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,11 +10,18 @@ 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
- ONBOARDING_DOCS="https://docs.testing-farm.io/general/0.1/onboarding.html",
22
+ ONBOARDING_DOCS="https://docs.testing-farm.io/Testing%20Farm/0.1/onboarding.html",
16
23
  CONTAINER_SIGN="/.testing-farm-container",
17
- WATCH_TICK=3,
24
+ WATCH_TICK=30,
18
25
  DEFAULT_API_TIMEOUT=10,
19
26
  DEFAULT_API_RETRIES=7,
20
27
  # default reservation duration in minutes
@@ -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)