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 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(settings.API_URL, f"/v0.1/requests/{request_id}")
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"ssh -oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null root@{guests[0]}") # noqa: E501
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(ssh_public_keys: List[str], rules: Dict[str, Any], duration: int, environment: Dict[str, Any]):
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
- api_url: str = typer.Option(settings.API_URL, help="Testing Farm API URL."),
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, rules=rules, duration=reservation_duration, environment=environment
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, rules=rules, duration=reservation_duration, environment=environment
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
- 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,
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 settings.API_TOKEN:
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(str(settings.API_URL), "v0.1/requests")
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(settings.API_TOKEN))
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(str(settings.API_URL), f"/v0.1/requests/{id}")
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
- environment["variables"] = {"TF_RESERVATION_DURATION": str(reservation_duration)}
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(str(settings.API_URL), "v0.1/requests")
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(settings.API_TOKEN))
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(str(settings.API_URL), f"/v0.1/requests/{id}")
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"ssh -oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null root@{guest}") # noqa: E501
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
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tft-cli
3
- Version: 0.0.23
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,,
@@ -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,,