tft-cli 0.0.24__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 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",
@@ -326,12 +343,12 @@ def _sanity_reserve() -> None:
326
343
  exit_error("No SSH identities found in the SSH agent. Please run `ssh-add`.")
327
344
 
328
345
 
329
- 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:
330
347
  """
331
348
  Handle the reservation for :py:func:``request`` and :py:func:``restart`` commands.
332
349
  """
333
350
  # Get artifacts url
334
- request_url = urllib.parse.urljoin(settings.API_URL, f"/v0.1/requests/{request_id}")
351
+ request_url = urllib.parse.urljoin(api_url, f"/v0.1/requests/{request_id}")
335
352
  response = session.get(request_url)
336
353
  artifacts_url = response.json()['run']['artifacts']
337
354
 
@@ -386,7 +403,7 @@ def _handle_reservation(session, request_id: str, autoconnect: bool = False) ->
386
403
  console.print(f"🌎 ssh root@{guests[0]}")
387
404
 
388
405
  if autoconnect:
389
- os.system(f"ssh -oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null root@{guests[0]}") # noqa: E501
406
+ os.system(f"{SSH_RESERVATION_OPTIONS} root@{guests[0]}") # noqa: E501
390
407
 
391
408
 
392
409
  def _localhost_ingress_rule(session: requests.Session) -> str:
@@ -397,7 +414,7 @@ def _localhost_ingress_rule(session: requests.Session) -> str:
397
414
 
398
415
  if get_ip.ok:
399
416
  ip = get_ip.text.strip()
400
- return f"-1:{ip}:-1"
417
+ return f"-1:{ip}:-1" # noqa: E231
401
418
 
402
419
  else:
403
420
  exit_error(f"Got {get_ip.status_code} while checking {settings.PUBLIC_IP_CHECKER_URL}")
@@ -666,21 +683,25 @@ def _print_summary_table(summary: dict, format: Optional[WatchFormat], show_deta
666
683
 
667
684
 
668
685
  def watch(
669
- api_url: str = typer.Option(settings.API_URL, help="Testing Farm API URL."),
686
+ context: typer.Context,
687
+ api_url: str = ARGUMENT_API_URL,
670
688
  id: str = typer.Option(..., help="Request ID to watch"),
671
689
  no_wait: bool = typer.Option(False, help="Skip waiting for request completion."),
672
690
  format: Optional[WatchFormat] = typer.Option(WatchFormat.text, help="Output format"),
673
691
  autoconnect: bool = typer.Option(True, hidden=True),
674
692
  reserve: bool = typer.Option(False, hidden=True),
675
693
  ):
694
+ """Watch request for completion."""
695
+
696
+ # Accept these arguments only via environment variables
697
+ check_unexpected_arguments(context, "api_url")
698
+
676
699
  def _console_print(*args, **kwargs):
677
700
  """A helper function that will skip printing to console if output format is json"""
678
701
  if format == WatchFormat.json:
679
702
  return
680
703
  console.print(*args, **kwargs)
681
704
 
682
- """Watch request for completion."""
683
-
684
705
  if not uuid_valid(id):
685
706
  exit_error("invalid request id")
686
707
 
@@ -737,7 +758,7 @@ def watch(
737
758
  if state == current_state:
738
759
  # check for reservation status and finish early if reserved
739
760
  if reserve and _is_reserved(session, request):
740
- _handle_reservation(session, request["id"], autoconnect)
761
+ _handle_reservation(session, api_url, request["id"], autoconnect)
741
762
  return
742
763
 
743
764
  time.sleep(1)
@@ -804,6 +825,7 @@ def version():
804
825
 
805
826
 
806
827
  def request(
828
+ context: typer.Context,
807
829
  api_url: str = ARGUMENT_API_URL,
808
830
  api_token: str = ARGUMENT_API_TOKEN,
809
831
  timeout: int = typer.Option(
@@ -903,6 +925,10 @@ def request(
903
925
  """
904
926
  Request testing from Testing Farm.
905
927
  """
928
+
929
+ # Accept these arguments only via environment variables
930
+ check_unexpected_arguments(context, "api_url", "api_token")
931
+
906
932
  # Split comma separated arches
907
933
  arches = normalize_multistring_option(arches)
908
934
 
@@ -1038,6 +1064,8 @@ def request(
1038
1064
  environment["hardware"] = hw_constraints(hardware)
1039
1065
 
1040
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]
1041
1069
  environment["kickstart"] = options_to_dict("environment kickstart", kickstart)
1042
1070
 
1043
1071
  if redhat_brew_build:
@@ -1204,7 +1232,7 @@ def request(
1204
1232
  request_id = response.json()['id']
1205
1233
 
1206
1234
  # Watch the request and handle reservation
1207
- 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)
1208
1236
 
1209
1237
 
1210
1238
  def restart(
@@ -1256,6 +1284,9 @@ def restart(
1256
1284
  Just pass a request ID or an URL with a request ID to restart it.
1257
1285
  """
1258
1286
 
1287
+ # Accept these arguments only via environment variables
1288
+ check_unexpected_arguments(context, "api_url", "api_token", "internal_api_url")
1289
+
1259
1290
  # UUID pattern
1260
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}')
1261
1292
 
@@ -1473,11 +1504,18 @@ def restart(
1473
1504
 
1474
1505
  # watch
1475
1506
  watch(
1476
- str(api_url), response.json()['id'], no_wait, reserve=reserve, autoconnect=autoconnect, format=WatchFormat.text
1507
+ context,
1508
+ str(api_url),
1509
+ response.json()['id'],
1510
+ no_wait,
1511
+ reserve=reserve,
1512
+ autoconnect=autoconnect,
1513
+ format=WatchFormat.text,
1477
1514
  )
1478
1515
 
1479
1516
 
1480
1517
  def run(
1518
+ context: typer.Context,
1481
1519
  arch: str = typer.Option("x86_64", "--arch", help="Hardware platform of the target machine."),
1482
1520
  compose: Optional[str] = typer.Option(
1483
1521
  None,
@@ -1489,6 +1527,10 @@ def run(
1489
1527
  secrets: Optional[List[str]] = OPTION_SECRETS,
1490
1528
  dry_run: bool = OPTION_DRY_RUN,
1491
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,
1492
1534
  command: List[str] = typer.Argument(..., help="Command to run. Use `--` to separate COMMAND from CLI options."),
1493
1535
  ):
1494
1536
  """
@@ -1496,7 +1538,7 @@ def run(
1496
1538
  """
1497
1539
 
1498
1540
  # check for token
1499
- if not settings.API_TOKEN:
1541
+ if not api_token:
1500
1542
  exit_error("No API token found, export `TESTING_FARM_API_TOKEN` environment variable.")
1501
1543
 
1502
1544
  # create request
@@ -1530,7 +1572,7 @@ def run(
1530
1572
  request["environments"] = [environment]
1531
1573
 
1532
1574
  # submit request to Testing Farm
1533
- post_url = urllib.parse.urljoin(str(settings.API_URL), "v0.1/requests")
1575
+ post_url = urllib.parse.urljoin(api_url, "v0.1/requests")
1534
1576
 
1535
1577
  # Setting up retries
1536
1578
  session = requests.Session()
@@ -1544,7 +1586,7 @@ def run(
1544
1586
  raise typer.Exit()
1545
1587
 
1546
1588
  # handle errors
1547
- response = session.post(post_url, json=request, headers=_get_headers(settings.API_TOKEN))
1589
+ response = session.post(post_url, json=request, headers=_get_headers(api_token))
1548
1590
  if response.status_code == 401:
1549
1591
  exit_error(f"API token is invalid. See {settings.ONBOARDING_DOCS} for more information.")
1550
1592
 
@@ -1556,7 +1598,7 @@ def run(
1556
1598
  exit_error(f"Unexpected error. Please file an issue to {settings.ISSUE_TRACKER}.")
1557
1599
 
1558
1600
  id = response.json()['id']
1559
- get_url = urllib.parse.urljoin(str(settings.API_URL), f"/v0.1/requests/{id}")
1601
+ get_url = urllib.parse.urljoin(api_url, f"/v0.1/requests/{id}")
1560
1602
 
1561
1603
  if verbose:
1562
1604
  console.print(f"🔎 api [blue]{get_url}[/blue]")
@@ -1640,6 +1682,9 @@ def run(
1640
1682
 
1641
1683
 
1642
1684
  def reserve(
1685
+ context: typer.Context,
1686
+ api_url: str = ARGUMENT_API_URL,
1687
+ api_token: str = ARGUMENT_API_TOKEN,
1643
1688
  ssh_public_keys: List[str] = _option_ssh_public_keys(RESERVE_PANEL_GENERAL),
1644
1689
  reservation_duration: int = _option_reservation_duration(RESERVE_PANEL_GENERAL),
1645
1690
  arch: str = typer.Option(
@@ -1659,6 +1704,9 @@ def reserve(
1659
1704
  repository: List[str] = OPTION_REPOSITORY,
1660
1705
  repository_file: List[str] = OPTION_REPOSITORY_FILE,
1661
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"),
1662
1710
  dry_run: bool = OPTION_DRY_RUN,
1663
1711
  post_install_script: Optional[str] = OPTION_POST_INSTALL_SCRIPT,
1664
1712
  print_only_request_id: bool = typer.Option(
@@ -1688,6 +1736,9 @@ def reserve(
1688
1736
 
1689
1737
  _sanity_reserve()
1690
1738
 
1739
+ # Accept these arguments only via environment variables
1740
+ check_unexpected_arguments(context, "api_url", "api_token")
1741
+
1691
1742
  # check for token
1692
1743
  if not settings.API_TOKEN:
1693
1744
  exit_error("No API token found, export `TESTING_FARM_API_TOKEN` environment variable.")
@@ -1734,6 +1785,8 @@ def reserve(
1734
1785
  environment["settings"]["provisioning"]["tags"] = options_to_dict("tags", tags)
1735
1786
 
1736
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]
1737
1790
  environment["kickstart"] = options_to_dict("environment kickstart", kickstart)
1738
1791
 
1739
1792
  if redhat_brew_build:
@@ -1754,6 +1807,18 @@ def reserve(
1754
1807
  if post_install_script:
1755
1808
  environment["settings"]["provisioning"]["post_install_script"] = post_install_script
1756
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
+
1757
1822
  # Setting up retries
1758
1823
  session = requests.Session()
1759
1824
  install_http_retries(session)
@@ -1803,7 +1868,7 @@ def reserve(
1803
1868
  console.print(f"⏳ Maximum reservation time is {DEFAULT_PIPELINE_TIMEOUT} minutes")
1804
1869
 
1805
1870
  # submit request to Testing Farm
1806
- post_url = urllib.parse.urljoin(str(settings.API_URL), "v0.1/requests")
1871
+ post_url = urllib.parse.urljoin(api_url, "v0.1/requests")
1807
1872
 
1808
1873
  # dry run
1809
1874
  if dry_run:
@@ -1815,7 +1880,7 @@ def reserve(
1815
1880
  raise typer.Exit()
1816
1881
 
1817
1882
  # handle errors
1818
- response = session.post(post_url, json=request, headers=_get_headers(settings.API_TOKEN))
1883
+ response = session.post(post_url, json=request, headers=_get_headers(api_token))
1819
1884
  if response.status_code == 401:
1820
1885
  exit_error(f"API token is invalid. See {settings.ONBOARDING_DOCS} for more information.")
1821
1886
 
@@ -1830,7 +1895,7 @@ def reserve(
1830
1895
  exit_error(f"Unexpected error. Please file an issue to {settings.ISSUE_TRACKER}.")
1831
1896
 
1832
1897
  id = response.json()['id']
1833
- get_url = urllib.parse.urljoin(str(settings.API_URL), f"/v0.1/requests/{id}")
1898
+ get_url = urllib.parse.urljoin(api_url, f"/v0.1/requests/{id}")
1834
1899
 
1835
1900
  if not print_only_request_id:
1836
1901
  console.print(f"🔎 [blue]{get_url}[/blue]")
@@ -1955,7 +2020,7 @@ def reserve(
1955
2020
  console.print(f"🌎 ssh root@{guest}")
1956
2021
 
1957
2022
  if autoconnect:
1958
- os.system(f"ssh -oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null root@{guest}") # noqa: E501
2023
+ os.system(f"{SSH_RESERVATION_OPTIONS} root@{guest}") # noqa: E501
1959
2024
 
1960
2025
 
1961
2026
  def update():
@@ -1967,6 +2032,7 @@ def update():
1967
2032
 
1968
2033
 
1969
2034
  def cancel(
2035
+ context: typer.Context,
1970
2036
  request_id: str = typer.Argument(
1971
2037
  ..., help="Testing Farm request to cancel. Specified by a request ID or a string containing it."
1972
2038
  ),
@@ -1977,6 +2043,9 @@ def cancel(
1977
2043
  Cancel a Testing Farm request.
1978
2044
  """
1979
2045
 
2046
+ # Accept these arguments only via environment variables
2047
+ check_unexpected_arguments(context, "api_url", "api_token")
2048
+
1980
2049
  # UUID pattern
1981
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}')
1982
2051
 
@@ -2023,6 +2092,7 @@ def cancel(
2023
2092
 
2024
2093
 
2025
2094
  def encrypt(
2095
+ context: typer.Context,
2026
2096
  message: str = typer.Argument(..., help="Message to be encrypted."),
2027
2097
  api_url: str = ARGUMENT_API_URL,
2028
2098
  api_token: str = ARGUMENT_API_TOKEN,
@@ -2040,6 +2110,9 @@ def encrypt(
2040
2110
  Create secrets for use in in-repository configuration.
2041
2111
  """
2042
2112
 
2113
+ # Accept these arguments only via environment variables
2114
+ check_unexpected_arguments(context, "api_url", "api_token")
2115
+
2043
2116
  # check for token
2044
2117
  if not api_token:
2045
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
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tft-cli
3
- Version: 0.0.24
3
+ Version: 0.0.25
4
4
  Summary: Testing Farm CLI tool
5
5
  License: Apache-2.0
6
6
  Author: Miroslav Vadkerti
@@ -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,,