devstack-cli 11.0.83__py3-none-any.whl → 11.0.85__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.
cli.py CHANGED
@@ -10,6 +10,7 @@ import json
10
10
  import logging
11
11
  import os
12
12
  import pathlib
13
+ import re
13
14
  import readline
14
15
  import shlex
15
16
  import shutil
@@ -1002,10 +1003,11 @@ class Cli:
1002
1003
  logger.info('Connecting to CDE')
1003
1004
  known_hosts = await self._get_known_hosts()
1004
1005
  if known_hosts is None:
1006
+ logger.error('Cannot connect to CDE. Host-key not found.')
1005
1007
  return
1006
1008
  self.known_hosts_file = await _create_temp_file(exit_stack=self.exit_stack, content=known_hosts)
1007
1009
  self.ssh_client = paramiko.SSHClient()
1008
- self.ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
1010
+ self.ssh_client.load_host_keys(self.known_hosts_file.name)
1009
1011
  try:
1010
1012
  self.ssh_client.connect(
1011
1013
  hostname=self.hostname,
@@ -1022,34 +1024,18 @@ class Cli:
1022
1024
  logger.info('Connected to CDE')
1023
1025
 
1024
1026
  async def _get_known_hosts(self: 'Cli') -> typing.Optional[str]:
1025
- if self.cde['value']['hostkey']:
1026
- if self.cde['value']['hostkey'].startswith(self.cde['value']['hostname']):
1027
- return self.cde['value']['hostkey']
1028
- return f"{self.cde['value']['hostname']} {self.cde['value']['hostkey']}"
1029
1027
  if not self.cde:
1030
1028
  logger.error('No CDE is selected. Cannot fetch host-key.')
1031
1029
  return None
1032
1030
  if not self.is_cde_running:
1033
1031
  logger.error('CDE is not running. Cannot fetch host-key.')
1034
1032
  return None
1035
- logger.debug('Scanning hostkeys of "%s"', self.hostname)
1036
- try:
1037
- stdout, stderr = await run_subprocess(
1038
- 'ssh-keyscan',
1039
- [
1040
- self.hostname,
1041
- ],
1042
- name='ssh-keyscan',
1043
- print_stdout=False,
1044
- print_stderr=False,
1045
- )
1046
- except SubprocessError as ex:
1047
- logger.error('%s Failed to fetch hostkeys. Is you CDE running?', ex) # noqa: TRY400
1048
- sys.exit(1)
1049
- known_hosts = stdout
1050
- with contextlib.suppress(FileNotFoundError):
1051
- known_hosts += pathlib.Path(os.path.expandvars('$HOME/.ssh/known_hosts')).read_bytes()
1052
- return known_hosts
1033
+ if not self.cde['value']['hostkey']:
1034
+ logger.error('CDE record does not contain a hostkey.')
1035
+ return None
1036
+ if self.cde['value']['hostkey'].startswith(self.cde['value']['hostname']):
1037
+ return self.cde['value']['hostkey']
1038
+ return f"{self.cde['value']['hostname']} {self.cde['value']['hostkey']}"
1053
1039
 
1054
1040
  async def _disconnect_cde(self: 'Cli') -> None:
1055
1041
  logger.info('Disconnecting from CDE')
@@ -1237,6 +1223,12 @@ class Cli:
1237
1223
  self.port_forwarding_task = None
1238
1224
 
1239
1225
  async def _bg_port_forwarding(self: 'Cli') -> None:
1226
+ remote_username = self.cde_type['value']['remote-username']
1227
+ if re.match(r'[^a-zA-Z0-9]', remote_username):
1228
+ raise Exception(f'Invalid remote username: "{remote_username}". Only alphanumeric characters are allowed.')
1229
+ hostname = self.hostname
1230
+ if re.match(r'[^a-zA-Z0-9]', hostname):
1231
+ raise Exception(f'Invalid hostname: "{hostname}". Only alphanumeric characters are allowed.')
1240
1232
  service_ports = self.cde_type['value'].get('service-ports')
1241
1233
  if service_ports is None:
1242
1234
  service_ports = [
@@ -1253,6 +1245,11 @@ class Cli:
1253
1245
  for port
1254
1246
  in service_ports
1255
1247
  ]
1248
+ for port in service_ports:
1249
+ if port[0] < 1 or port[0] > 65535:
1250
+ raise Exception(f'Invalid port: "{port[0]}". Only numbers between 1 and 65535 are allowed.')
1251
+ if port[1] < 1 or port[1] > 65535:
1252
+ raise Exception(f'Invalid port: "{port[1]}". Only numbers between 1 and 65535 are allowed.')
1256
1253
  while True:
1257
1254
  logger.info('Starting port forwarding of %s', ', '.join(str(port[0]) for port in service_ports))
1258
1255
  try:
@@ -1262,7 +1259,7 @@ class Cli:
1262
1259
  '-o', 'ConnectTimeout=10',
1263
1260
  '-o', f'UserKnownHostsFile={self.known_hosts_file.name}',
1264
1261
  '-NT',
1265
- f"{self.cde_type['value']['remote-username']}@{self.hostname}",
1262
+ f'{remote_username}@{hostname}',
1266
1263
  *itertools.chain.from_iterable([
1267
1264
  ('-L', f'{port[0]}:localhost:{port[1]}')
1268
1265
  for port
@@ -1442,13 +1439,22 @@ class Cli:
1442
1439
  return await self._process_remote_item_copy_file(file_info.filename)
1443
1440
 
1444
1441
  async def _process_remote_item_copy_dir(self: 'Cli', filename: str) -> str:
1442
+ remote_username = self.cde_type['value']['remote-username']
1443
+ if re.match(r'[^a-zA-Z0-9]', remote_username):
1444
+ raise Exception(f'Invalid remote username: "{remote_username}". Only alphanumeric characters are allowed.')
1445
+ hostname = self.hostname
1446
+ if re.match(r'[^a-zA-Z0-9]', hostname):
1447
+ raise Exception(f'Invalid hostname: "{hostname}". Only alphanumeric characters are allowed.')
1448
+ remote_source_directory = self.cde_type['value']['remote-source-directory']
1449
+ if re.match(r'[^a-zA-Z0-9/_.-]', remote_source_directory):
1450
+ raise Exception(f'Invalid remote source directory: "{remote_source_directory}". Only alphanumeric characters, slashes, underscores, dots and hyphens are allowed.')
1445
1451
  await run_subprocess(
1446
1452
  'rsync',
1447
1453
  [
1448
1454
  '-e', f'ssh -o ConnectTimeout=10 -o UserKnownHostsFile={self.known_hosts_file.name}',
1449
1455
  '--archive',
1450
1456
  '--checksum',
1451
- f"{self.cde_type['value']['remote-username']}@{self.hostname}:{self.cde_type['value']['remote-source-directory']}/{filename}/",
1457
+ f'{remote_username}@{hostname}:{remote_source_directory}/{filename}/',
1452
1458
  str(self.local_source_directory / filename),
1453
1459
  ],
1454
1460
  name='Copy remote directory',
@@ -1456,23 +1462,35 @@ class Cli:
1456
1462
  return f'Copied directory "{filename}"'
1457
1463
 
1458
1464
  async def _process_remote_item_copy_file(self: 'Cli', filename: str) -> str:
1465
+ remote_source_directory = self.cde_type['value']['remote-source-directory']
1466
+ if re.match(r'[^a-zA-Z0-9/_.-]', remote_source_directory):
1467
+ raise Exception(f'Invalid remote source directory: "{remote_source_directory}". Only alphanumeric characters, slashes, underscores, dots and hyphens are allowed.')
1459
1468
  await self.loop.run_in_executor(
1460
1469
  executor=None,
1461
1470
  func=functools.partial(
1462
1471
  self.sftp_client.get,
1463
- remotepath=f"{self.cde_type['value']['remote-source-directory']}/{filename}",
1472
+ remotepath=f'{remote_source_directory}/{filename}',
1464
1473
  localpath=str(self.local_source_directory / filename),
1465
1474
  ),
1466
1475
  )
1467
1476
  return f'Copied file "{filename}"'
1468
1477
 
1469
1478
  async def _process_remote_item_clone(self: 'Cli', filename: str) -> str:
1479
+ remote_username = self.cde_type['value']['remote-username']
1480
+ if re.match(r'[^a-zA-Z0-9]', remote_username):
1481
+ raise Exception(f'Invalid remote username: "{remote_username}". Only alphanumeric characters are allowed.')
1482
+ hostname = self.hostname
1483
+ if re.match(r'[^a-zA-Z0-9]', hostname):
1484
+ raise Exception(f'Invalid hostname: "{hostname}". Only alphanumeric characters are allowed.')
1485
+ remote_source_directory = self.cde_type['value']['remote-source-directory']
1486
+ if re.match(r'[^a-zA-Z0-9/_.-]', remote_source_directory):
1487
+ raise Exception(f'Invalid remote source directory: "{remote_source_directory}". Only alphanumeric characters, slashes, underscores, dots and hyphens are allowed.')
1470
1488
  await run_subprocess(
1471
1489
  'git',
1472
1490
  [
1473
1491
  'clone',
1474
1492
  '-q',
1475
- f"{self.cde_type['value']['remote-username']}@{self.hostname}:{self.cde_type['value']['remote-source-directory']}/{filename}",
1493
+ f'{remote_username}@{hostname}:{remote_source_directory}/{filename}',
1476
1494
  ],
1477
1495
  name='Git clone',
1478
1496
  cwd=self.local_source_directory,
@@ -1487,7 +1505,7 @@ class Cli:
1487
1505
  shlex.join([
1488
1506
  'git',
1489
1507
  '-C',
1490
- f"{self.cde_type['value']['remote-source-directory']}/{filename}",
1508
+ f'{remote_source_directory}/{filename}',
1491
1509
  'config',
1492
1510
  '--get',
1493
1511
  'remote.origin.url',
@@ -1512,10 +1530,19 @@ class Cli:
1512
1530
  return f'Cloned repository "{filename}"'
1513
1531
 
1514
1532
  async def _background_sync(self: 'Cli') -> None:
1533
+ remote_username = self.cde_type['value']['remote-username']
1534
+ if re.match(r'[^a-zA-Z0-9]', remote_username):
1535
+ raise Exception(f'Invalid remote username: "{remote_username}". Only alphanumeric characters are allowed.')
1536
+ hostname = self.hostname
1537
+ if re.match(r'[^a-zA-Z0-9]', hostname):
1538
+ raise Exception(f'Invalid hostname: "{hostname}". Only alphanumeric characters are allowed.')
1539
+ remote_source_directory = self.cde_type['value']['remote-source-directory']
1540
+ if re.match(r'[^a-zA-Z0-9/_.-]', remote_source_directory):
1541
+ raise Exception(f'Invalid remote source directory: "{remote_source_directory}". Only alphanumeric characters, slashes, underscores, dots and hyphens are allowed.')
1515
1542
  logger.debug('Starting background sync')
1516
1543
  self.local_source_directory.mkdir(parents=True, exist_ok=True)
1517
1544
  with contextlib.suppress(OSError):
1518
- self.sftp_client.mkdir(self.cde_type['value']['remote-source-directory'])
1545
+ self.sftp_client.mkdir(remote_source_directory)
1519
1546
  file_sync_exclusions = self.cde_type['value'].get('file-sync-exclusions')
1520
1547
  if file_sync_exclusions is None:
1521
1548
  file_sync_exclusions = [
@@ -1563,7 +1590,7 @@ class Cli:
1563
1590
  '--human-readable',
1564
1591
  '--verbose',
1565
1592
  f'{self.local_source_directory}/',
1566
- f"{self.cde_type['value']['remote-username']}@{self.hostname}:{self.cde_type['value']['remote-source-directory']}",
1593
+ f'{remote_username}@{hostname}:{remote_source_directory}',
1567
1594
  ],
1568
1595
  name='Background sync',
1569
1596
  print_to_debug_log=True,
@@ -1579,19 +1606,31 @@ class Cli:
1579
1606
  logger.info('Background sync done')
1580
1607
 
1581
1608
  async def _reverse_background_sync(self: 'Cli') -> None:
1609
+ remote_username = self.cde_type['value']['remote-username']
1610
+ if re.match(r'[^a-zA-Z0-9]', remote_username):
1611
+ raise Exception(f'Invalid remote username: "{remote_username}". Only alphanumeric characters are allowed.')
1612
+ hostname = self.hostname
1613
+ if re.match(r'[^a-zA-Z0-9]', hostname):
1614
+ raise Exception(f'Invalid hostname: "{hostname}". Only alphanumeric characters are allowed.')
1615
+ remote_source_directory = self.cde_type['value']['remote-source-directory']
1616
+ if re.match(r'[^a-zA-Z0-9/_.-]', remote_source_directory):
1617
+ raise Exception(f'Invalid remote source directory: "{remote_source_directory}". Only alphanumeric characters, slashes, underscores, dots and hyphens are allowed.')
1582
1618
  logger.debug('Starting reverse background sync')
1619
+ remote_output_directory = self.cde_type['value']['remote-output-directory']
1620
+ if re.match(r'[^a-zA-Z0-9/_.-]', remote_output_directory):
1621
+ raise Exception(f'Invalid remote output directory: "{remote_output_directory}". Only alphanumeric characters, slashes, underscores, dots and hyphens are allowed.')
1583
1622
  with contextlib.suppress(OSError):
1584
- self.sftp_client.mkdir(self.cde_type['value']['remote-output-directory'])
1623
+ self.sftp_client.mkdir(remote_output_directory)
1585
1624
  self.local_output_directory.mkdir(parents=True, exist_ok=True)
1586
1625
  try:
1587
- stdout, stderr = await run_subprocess(
1626
+ await run_subprocess(
1588
1627
  'rsync',
1589
1628
  [
1590
1629
  '-e', f'ssh -o ConnectTimeout=10 -o UserKnownHostsFile={self.known_hosts_file.name}',
1591
1630
  '--archive',
1592
1631
  '--exclude', '__pycache__',
1593
1632
  '--human-readable',
1594
- f"{self.cde_type['value']['remote-username']}@{self.hostname}:{self.cde_type['value']['remote-output-directory']}/",
1633
+ f'{remote_username}@{hostname}:{remote_output_directory}/',
1595
1634
  str(self.local_output_directory),
1596
1635
  ],
1597
1636
  name='Reverse background sync',
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: devstack-cli
3
- Version: 11.0.83
3
+ Version: 11.0.85
4
4
  Summary: Command-line access to Cloud Development Environments (CDEs) created by Cloudomation DevStack
5
5
  Author-email: Stefan Mückstein <stefan@cloudomation.com>
6
6
  Project-URL: Homepage, https://cloudomation.com/
@@ -0,0 +1,9 @@
1
+ __init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ cli.py,sha256=MB5BGbhMwEs1icEsDpheBQDrNk1ck9QeDV2vTzYU82s,83037
3
+ version.py,sha256=r-2CoTcqkWFSEi8k9zvRHtIGDang9cMMzAHm1742Mw4,180
4
+ devstack_cli-11.0.85.dist-info/LICENSE,sha256=OBXZbEUMtIHIzyISkJ9fJlf_imds3rcKqeQu9yiyUJI,1055
5
+ devstack_cli-11.0.85.dist-info/METADATA,sha256=mlxRxtkfa4SrPGytNn8P3B7Y41avi75e92NBxBZHtKw,4287
6
+ devstack_cli-11.0.85.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
7
+ devstack_cli-11.0.85.dist-info/entry_points.txt,sha256=f0xb4DIk0a7E5kyZ7YpoLhtjoagQj5VQpeBbW9a8A9Y,42
8
+ devstack_cli-11.0.85.dist-info/top_level.txt,sha256=lP8zvU46Am_G0MPcNmCI6f0sMfwpDUWpTROaPs-IEPk,21
9
+ devstack_cli-11.0.85.dist-info/RECORD,,
version.py CHANGED
@@ -4,6 +4,6 @@ constants, set by build
4
4
 
5
5
  MAJOR = '11'
6
6
  BRANCH_NAME = 'release-11'
7
- BUILD_DATE = '2025-02-10-044518'
8
- SHORT_SHA = 'b8aa0c3'
9
- VERSION = '11+release-11.2025-02-10-044518.b8aa0c3'
7
+ BUILD_DATE = '2025-02-10-115139'
8
+ SHORT_SHA = 'f9e1188'
9
+ VERSION = '11+release-11.2025-02-10-115139.f9e1188'
@@ -1,9 +0,0 @@
1
- __init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- cli.py,sha256=_k9ZnI2n6T9tPqBasTj5c0EECaHJvWRFwTcoxPRik2o,79526
3
- version.py,sha256=oQplPMp28xVgc4xkJB2_VKADgQ7VfXmif3LaFfGYAj8,180
4
- devstack_cli-11.0.83.dist-info/LICENSE,sha256=OBXZbEUMtIHIzyISkJ9fJlf_imds3rcKqeQu9yiyUJI,1055
5
- devstack_cli-11.0.83.dist-info/METADATA,sha256=UsbgvY8Vrs2AErciQXxOz5kk0fzVmC1FO684cq1QuVk,4287
6
- devstack_cli-11.0.83.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
7
- devstack_cli-11.0.83.dist-info/entry_points.txt,sha256=f0xb4DIk0a7E5kyZ7YpoLhtjoagQj5VQpeBbW9a8A9Y,42
8
- devstack_cli-11.0.83.dist-info/top_level.txt,sha256=lP8zvU46Am_G0MPcNmCI6f0sMfwpDUWpTROaPs-IEPk,21
9
- devstack_cli-11.0.83.dist-info/RECORD,,