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 +72 -33
- {devstack_cli-11.0.83.dist-info → devstack_cli-11.0.85.dist-info}/METADATA +1 -1
- devstack_cli-11.0.85.dist-info/RECORD +9 -0
- version.py +3 -3
- devstack_cli-11.0.83.dist-info/RECORD +0 -9
- {devstack_cli-11.0.83.dist-info → devstack_cli-11.0.85.dist-info}/LICENSE +0 -0
- {devstack_cli-11.0.83.dist-info → devstack_cli-11.0.85.dist-info}/WHEEL +0 -0
- {devstack_cli-11.0.83.dist-info → devstack_cli-11.0.85.dist-info}/entry_points.txt +0 -0
- {devstack_cli-11.0.83.dist-info → devstack_cli-11.0.85.dist-info}/top_level.txt +0 -0
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.
|
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
|
-
|
1036
|
-
|
1037
|
-
|
1038
|
-
|
1039
|
-
|
1040
|
-
|
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
|
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
|
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
|
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
|
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
|
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(
|
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
|
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(
|
1623
|
+
self.sftp_client.mkdir(remote_output_directory)
|
1585
1624
|
self.local_output_directory.mkdir(parents=True, exist_ok=True)
|
1586
1625
|
try:
|
1587
|
-
|
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
|
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.
|
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-
|
8
|
-
SHORT_SHA = '
|
9
|
-
VERSION = '11+release-11.2025-02-10-
|
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,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|