devstack-cli 11.0.255__py3-none-any.whl → 12.0.0__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 +217 -90
- {devstack_cli-11.0.255.dist-info → devstack_cli-12.0.0.dist-info}/METADATA +1 -1
- devstack_cli-12.0.0.dist-info/RECORD +9 -0
- {devstack_cli-11.0.255.dist-info → devstack_cli-12.0.0.dist-info}/WHEEL +1 -1
- version.py +5 -5
- devstack_cli-11.0.255.dist-info/RECORD +0 -9
- {devstack_cli-11.0.255.dist-info → devstack_cli-12.0.0.dist-info}/entry_points.txt +0 -0
- {devstack_cli-11.0.255.dist-info → devstack_cli-12.0.0.dist-info}/licenses/LICENSE +0 -0
- {devstack_cli-11.0.255.dist-info → devstack_cli-12.0.0.dist-info}/top_level.txt +0 -0
cli.py
CHANGED
|
@@ -34,6 +34,7 @@ import rich.logging
|
|
|
34
34
|
import rich.markup
|
|
35
35
|
import rich.pretty
|
|
36
36
|
import rich.progress
|
|
37
|
+
import rich.table
|
|
37
38
|
import watchdog.events
|
|
38
39
|
import watchdog.observers
|
|
39
40
|
import yarl
|
|
@@ -237,11 +238,12 @@ class Cli:
|
|
|
237
238
|
self.password: typing.Optional[str] = None
|
|
238
239
|
self.session: typing.Optional[aiohttp.ClientSession] = None
|
|
239
240
|
self.workspace_url: typing.Optional[yarl.URL] = None
|
|
241
|
+
self.user_info: typing.Optional[dict] = None
|
|
240
242
|
self.sync_task: typing.Optional[asyncio.Task] = None
|
|
241
243
|
self.port_forwarding_task: typing.Optional[asyncio.Task] = None
|
|
242
244
|
self.logs_task: typing.Optional[asyncio.Task] = None
|
|
243
245
|
self.exit_stack: typing.Optional[contextlib.AsyncExitStack] = None
|
|
244
|
-
self.cdes: typing.
|
|
246
|
+
self.cdes: typing.Dict[str, dict] = {}
|
|
245
247
|
self.cde: typing.Optional[dict] = None
|
|
246
248
|
self.cde_type: typing.Optional[dict] = None
|
|
247
249
|
self.ssh_client: typing.Optional[paramiko.SSHClient] = None
|
|
@@ -358,7 +360,7 @@ class Cli:
|
|
|
358
360
|
parser.add_argument(
|
|
359
361
|
'--maximum-uptime-hours',
|
|
360
362
|
type=int,
|
|
361
|
-
help='
|
|
363
|
+
help='deprecated. accepted to not break backwards compatibility, please set `devstack_default_maximum_uptime_hours` metadata value on your user.',
|
|
362
364
|
)
|
|
363
365
|
parser.add_argument(
|
|
364
366
|
'-n', '--cde-name',
|
|
@@ -484,6 +486,59 @@ class Cli:
|
|
|
484
486
|
self.loop.remove_reader(sys.stdin)
|
|
485
487
|
termios.tcsetattr(self._fd, termios.TCSADRAIN, self._tcattr)
|
|
486
488
|
|
|
489
|
+
async def _migrate_maximum_uptime_hours(self: 'Cli') -> None:
|
|
490
|
+
assert self.session is not None
|
|
491
|
+
assert self.workspace_url is not None
|
|
492
|
+
assert self.user_info is not None
|
|
493
|
+
assert self.config is not None
|
|
494
|
+
response = await self.session.get(
|
|
495
|
+
url=self.workspace_url / 'api/latest/record_metadata',
|
|
496
|
+
params={
|
|
497
|
+
'filter': json.dumps({
|
|
498
|
+
'and': [
|
|
499
|
+
{
|
|
500
|
+
'field': 'record_id',
|
|
501
|
+
'op': 'eq',
|
|
502
|
+
'value': self.user_info['identity_id'],
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
'field': 'key',
|
|
506
|
+
'op': 'eq',
|
|
507
|
+
'value': 'devstack_default_maximum_uptime_hours',
|
|
508
|
+
},
|
|
509
|
+
],
|
|
510
|
+
}),
|
|
511
|
+
'fields': 'record_id,key,data,id',
|
|
512
|
+
'plain': 'True',
|
|
513
|
+
},
|
|
514
|
+
)
|
|
515
|
+
if not response.ok:
|
|
516
|
+
raise InitializationError('Failed to get devstack_default_maximum_uptime_hours metadata')
|
|
517
|
+
maximum_uptime_hours_value = await response.json()
|
|
518
|
+
if len(maximum_uptime_hours_value) > 0:
|
|
519
|
+
if not isinstance(maximum_uptime_hours_value[0]['data'], (str, int)):
|
|
520
|
+
response = await self.session.delete(url=self.workspace_url / 'api/latest/record_metadata' / maximum_uptime_hours_value[0]['id'])
|
|
521
|
+
if not response.ok:
|
|
522
|
+
raise InitializationError(f'Failed to delete invalid metadata: {response.reason} ({response.status}):\n{await response.text()}')
|
|
523
|
+
logger.warning('Deleted invalid metadata: %s', maximum_uptime_hours_value[0]['id'])
|
|
524
|
+
else:
|
|
525
|
+
if int(self.config['global']['maximum_uptime_hours']) != int(maximum_uptime_hours_value[0]['data']):
|
|
526
|
+
logger.warning('"maximum_uptime_hours" is deprecated and set, while the metadata key `devstack_default_maximum_uptime_hours` is also set, which will be used.')
|
|
527
|
+
return
|
|
528
|
+
logger.info('Migrating maximum_uptime_hours to Cloudomation metadata')
|
|
529
|
+
response = await self.session.post(
|
|
530
|
+
url=self.workspace_url / 'api/latest/record_metadata',
|
|
531
|
+
json={
|
|
532
|
+
'record_id': self.user_info['identity_id'],
|
|
533
|
+
'key': 'devstack_default_maximum_uptime_hours',
|
|
534
|
+
'data': int(self.config['global']['maximum_uptime_hours']),
|
|
535
|
+
},
|
|
536
|
+
)
|
|
537
|
+
if not response.ok:
|
|
538
|
+
raise InitializationError(f'Failed to migrate maximum_uptime_hours: {response.reason} ({response.status}):\n{await response.text()}')
|
|
539
|
+
del self.config['global']['maximum_uptime_hours']
|
|
540
|
+
|
|
541
|
+
|
|
487
542
|
async def _load_global_config(self: 'Cli') -> None:
|
|
488
543
|
self.config_file = pathlib.Path(os.path.expandvars(self.args.config_file))
|
|
489
544
|
self.config_file.parent.mkdir(parents=True, exist_ok=True) # make sure the config directory exists
|
|
@@ -496,6 +551,8 @@ class Cli:
|
|
|
496
551
|
config_str = await f.read()
|
|
497
552
|
self.config.read_string(config_str, source=self.config_file)
|
|
498
553
|
self.config.setdefault('global', {})
|
|
554
|
+
self.config['global'].setdefault('source_directory_template', '$HOME/{folder}')
|
|
555
|
+
self.config['global'].setdefault('output_directory_template', '$HOME/{folder}-output')
|
|
499
556
|
|
|
500
557
|
workspace_url = self.args.workspace_url or self.config['global'].get('workspace_url')
|
|
501
558
|
if not workspace_url:
|
|
@@ -513,18 +570,6 @@ class Cli:
|
|
|
513
570
|
self.password = self._console_input(f'Enter your password to authenticate "{user_name}" to {workspace_url}: ', password=True)
|
|
514
571
|
self.twofa_code = self._console_input('Enter a current two-factor authentication code (if enabled): ', password=True)
|
|
515
572
|
|
|
516
|
-
maximum_uptime_hours = self.args.maximum_uptime_hours or self.config['global'].get('maximum_uptime_hours')
|
|
517
|
-
if not maximum_uptime_hours:
|
|
518
|
-
while True:
|
|
519
|
-
maximum_uptime_hours = self._console_input('How many hours should an CDE remain started until it is automatically stopped: ', prefill='8')
|
|
520
|
-
try:
|
|
521
|
-
int(maximum_uptime_hours)
|
|
522
|
-
except ValueError:
|
|
523
|
-
logger.error('"%s" is not a valid number', maximum_uptime_hours) # noqa: TRY400
|
|
524
|
-
else:
|
|
525
|
-
break
|
|
526
|
-
self.config['global']['maximum_uptime_hours'] = maximum_uptime_hours
|
|
527
|
-
|
|
528
573
|
await self._write_config_file()
|
|
529
574
|
|
|
530
575
|
async def _write_config_file(self: 'Cli') -> None:
|
|
@@ -578,6 +623,9 @@ class Cli:
|
|
|
578
623
|
logger.info('Logged in to Cloudomation workspace')
|
|
579
624
|
json_logger.debug(json.dumps(self.user_info, indent=4, sort_keys=True))
|
|
580
625
|
|
|
626
|
+
if 'maximum_uptime_hours' in self.config['global']:
|
|
627
|
+
await self._migrate_maximum_uptime_hours()
|
|
628
|
+
|
|
581
629
|
response = await self.session.get(
|
|
582
630
|
url=self.workspace_url / 'api/latest/object_template/cde-type',
|
|
583
631
|
params={
|
|
@@ -590,6 +638,18 @@ class Cli:
|
|
|
590
638
|
logger.debug('The "cde-type" object template')
|
|
591
639
|
json_logger.debug(json.dumps(self.cde_type_template, indent=4, sort_keys=True))
|
|
592
640
|
|
|
641
|
+
response = await self.session.get(
|
|
642
|
+
url=self.workspace_url / 'api/latest/object_template/cde-snapshot',
|
|
643
|
+
params={
|
|
644
|
+
'by': 'name',
|
|
645
|
+
},
|
|
646
|
+
)
|
|
647
|
+
if response.status != 200:
|
|
648
|
+
raise InitializationError(f'Failed to fetch "cde-snapshot" object template: {response.reason} ({response.status}):\n{await response.text()}\nIs the "DevStack" bundle installed?')
|
|
649
|
+
self.cde_snapshot_template = (await response.json())['object_template']
|
|
650
|
+
logger.debug('The "cde-snapshot" object template')
|
|
651
|
+
json_logger.debug(json.dumps(self.cde_snapshot_template, indent=4, sort_keys=True))
|
|
652
|
+
|
|
593
653
|
response = await self.session.get(
|
|
594
654
|
url=self.workspace_url / 'api/latest/object_template/cde',
|
|
595
655
|
params={
|
|
@@ -619,6 +679,28 @@ class Cli:
|
|
|
619
679
|
logger.debug('The "cde-type" custom objects')
|
|
620
680
|
json_logger.debug(json.dumps(self.cde_types, indent=4, sort_keys=True))
|
|
621
681
|
|
|
682
|
+
response = await self.session.get(
|
|
683
|
+
url=self.workspace_url / 'api/latest/custom_object',
|
|
684
|
+
params={
|
|
685
|
+
'filter': json.dumps({
|
|
686
|
+
'and': [
|
|
687
|
+
{
|
|
688
|
+
'field': 'object_template_id',
|
|
689
|
+
'op': 'eq',
|
|
690
|
+
'value': self.cde_snapshot_template['id'],
|
|
691
|
+
},
|
|
692
|
+
],
|
|
693
|
+
}),
|
|
694
|
+
'plain': 'true',
|
|
695
|
+
},
|
|
696
|
+
)
|
|
697
|
+
if response.status != 200:
|
|
698
|
+
raise InitializationError(f'Failed to fetch "cde-snapshot" custom objects: {response.reason} ({response.status}):\n{await response.text()}')
|
|
699
|
+
self.cde_snapshots = await response.json()
|
|
700
|
+
self.cde_snapshots.sort(key=lambda cde_snapshot: cde_snapshot['name'])
|
|
701
|
+
logger.debug('The "cde-snapshot" custom objects')
|
|
702
|
+
json_logger.debug(json.dumps(self.cde_snapshots, indent=4, sort_keys=True))
|
|
703
|
+
|
|
622
704
|
# logger.info('Using configuration of CDE "%s"', self.cde_name)
|
|
623
705
|
# json_logger.debug(json.dumps(self.cde_config, indent=4, sort_keys=True))
|
|
624
706
|
|
|
@@ -642,7 +724,10 @@ class Cli:
|
|
|
642
724
|
table.add_section()
|
|
643
725
|
table.add_row('', '== CDE selection ==')
|
|
644
726
|
for i, cde in enumerate(self.cdes.values(), start=1):
|
|
645
|
-
|
|
727
|
+
cde_snapshot = await self._get_cde_snapshot_of_cde(cde)
|
|
728
|
+
if not cde_snapshot:
|
|
729
|
+
continue
|
|
730
|
+
cde_type = await self._get_cde_type_of_cde_snapshot(cde_snapshot)
|
|
646
731
|
if not cde_type:
|
|
647
732
|
continue
|
|
648
733
|
cde_type_name = cde_type['name']
|
|
@@ -822,24 +907,31 @@ class Cli:
|
|
|
822
907
|
self.sftp_client.close()
|
|
823
908
|
self.sftp_client = None
|
|
824
909
|
|
|
910
|
+
async def _get_cde_type_of_cde_snapshot(self: 'Cli', cde_snapshot: dict) -> typing.Optional[dict]:
|
|
911
|
+
try:
|
|
912
|
+
cde_type = next(cde_type for cde_type in self.cde_types if cde_type['id'] == cde_snapshot['value']['cde-type'])
|
|
913
|
+
except StopIteration:
|
|
914
|
+
logger.error('CDE type ID "%s" not found', cde_snapshot['value']['cde-type']) # noqa: TRY400
|
|
915
|
+
return None
|
|
916
|
+
return cde_type
|
|
825
917
|
|
|
826
|
-
async def
|
|
918
|
+
async def _get_cde_snapshot_of_cde(self: 'Cli', cde: dict) -> typing.Optional[dict]:
|
|
827
919
|
if cde['exists_remotely']:
|
|
828
920
|
try:
|
|
829
|
-
|
|
921
|
+
cde_snapshot = next(cde_snapshot for cde_snapshot in self.cde_snapshots if cde_snapshot['id'] == cde['value']['cde-snapshot'])
|
|
830
922
|
except StopIteration:
|
|
831
|
-
logger.error('CDE
|
|
923
|
+
logger.error('CDE snapshot ID "%s" not found', cde['value']['cde-snapshot']) # noqa: TRY400
|
|
832
924
|
return None
|
|
833
925
|
elif cde['exists_locally']:
|
|
834
926
|
try:
|
|
835
|
-
|
|
927
|
+
cde_snapshot = next(cde_snapshot for cde_snapshot in self.cde_snapshots if cde_snapshot['name'] == cde.get('cde_snapshot'))
|
|
836
928
|
except StopIteration:
|
|
837
|
-
logger.error('CDE
|
|
929
|
+
logger.error('CDE snapshot "%s" not found', cde.get('cde_snapshot')) # noqa: TRY400
|
|
838
930
|
return None
|
|
839
931
|
else:
|
|
840
932
|
logger.error('CDE does not exist')
|
|
841
933
|
return None
|
|
842
|
-
return
|
|
934
|
+
return cde_snapshot
|
|
843
935
|
|
|
844
936
|
async def _process_args(self: 'Cli') -> None:
|
|
845
937
|
if self.args.cde_name:
|
|
@@ -936,32 +1028,60 @@ class Cli:
|
|
|
936
1028
|
|
|
937
1029
|
async def _create_cde(self: 'Cli') -> None:
|
|
938
1030
|
logger.info('Creating new CDE')
|
|
939
|
-
|
|
1031
|
+
|
|
1032
|
+
response = await self.session.get(
|
|
1033
|
+
url=self.workspace_url / 'api/latest/custom_object',
|
|
1034
|
+
params={
|
|
1035
|
+
'filter': json.dumps({
|
|
1036
|
+
'and': [
|
|
1037
|
+
{
|
|
1038
|
+
'field': 'object_template_id',
|
|
1039
|
+
'op': 'eq',
|
|
1040
|
+
'value': self.cde_snapshot_template['id'],
|
|
1041
|
+
},
|
|
1042
|
+
{
|
|
1043
|
+
'field': 'provisioning_state',
|
|
1044
|
+
'op': 'eq',
|
|
1045
|
+
'value': 'READY',
|
|
1046
|
+
},
|
|
1047
|
+
],
|
|
1048
|
+
}),
|
|
1049
|
+
'plain': 'true',
|
|
1050
|
+
},
|
|
1051
|
+
)
|
|
1052
|
+
if response.status != 200:
|
|
1053
|
+
raise InitializationError(f'Failed to fetch "cde-snapshot" custom objects: {response.reason} ({response.status}):\n{await response.text()}')
|
|
1054
|
+
self.cde_snapshots = await response.json()
|
|
1055
|
+
self.cde_snapshots.sort(key=lambda cde_snapshot: cde_snapshot['name'])
|
|
1056
|
+
logger.debug('The "cde-snapshot" custom objects')
|
|
1057
|
+
json_logger.debug(json.dumps(self.cde_snapshots, indent=4, sort_keys=True))
|
|
1058
|
+
|
|
1059
|
+
table = rich.table.Table(title='CDE snapshots')
|
|
940
1060
|
table.add_column('Key', style='cyan bold')
|
|
941
1061
|
table.add_column('Name')
|
|
942
1062
|
table.add_column('Description')
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1063
|
+
table.add_column('Status')
|
|
1064
|
+
for i, cde_snapshot in enumerate(self.cde_snapshots, start=1):
|
|
1065
|
+
table.add_row(str(i), cde_snapshot['name'], cde_snapshot['description'], cde_snapshot['provisioning_state'])
|
|
1066
|
+
#table.add_row('ESC', 'Cancel')
|
|
946
1067
|
rich.print(table)
|
|
947
|
-
|
|
948
|
-
key_press = await self.key_queue.get()
|
|
949
|
-
if key_press == chr(27):
|
|
950
|
-
logger.warning('Aborting')
|
|
951
|
-
return
|
|
1068
|
+
cde_snapshot_index = self._console_input(f'Choose a CDE snapshot (1-{len(self.cde_snapshots)}): ')
|
|
952
1069
|
try:
|
|
953
|
-
|
|
1070
|
+
cde_snapshot = self.cde_snapshots[int(cde_snapshot_index)-1]
|
|
954
1071
|
except (IndexError, ValueError):
|
|
955
|
-
logger.error('Invalid choice "%s"',
|
|
1072
|
+
logger.error('Invalid choice "%s"', cde_snapshot_index) # noqa: TRY400
|
|
1073
|
+
return
|
|
1074
|
+
if cde_snapshot['provisioning_state'] != 'READY':
|
|
1075
|
+
logger.error('CDE snapshot "%s" is not ready', cde_snapshot['name'])
|
|
956
1076
|
return
|
|
957
|
-
cde_name = self._console_input('Choose a name for your CDE: ', prefill=f"{self.user_info['name']}-{
|
|
958
|
-
await self._create_cde_api_call(cde_name,
|
|
1077
|
+
cde_name = self._console_input('Choose a name for your CDE: ', prefill=f"{self.user_info['name']}-{cde_snapshot['name']}")
|
|
1078
|
+
await self._create_cde_api_call(cde_name, cde_snapshot['id'])
|
|
959
1079
|
await self._update_cde_list()
|
|
960
1080
|
await self._select_cde(cde_name)
|
|
1081
|
+
if self.cde['status'] == 'not configured':
|
|
1082
|
+
await self._configure_cde(silent=True)
|
|
961
1083
|
|
|
962
|
-
async def _create_cde_api_call(self: 'Cli', cde_name: str,
|
|
963
|
-
maximum_uptime_hours = int(self.config['global'].get('maximum_uptime_hours', '8'))
|
|
964
|
-
stop_at = (datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=maximum_uptime_hours)).isoformat()
|
|
1084
|
+
async def _create_cde_api_call(self: 'Cli', cde_name: str, cde_snapshot_id: str) -> None:
|
|
965
1085
|
try:
|
|
966
1086
|
response = await self.session.post(
|
|
967
1087
|
url=self.workspace_url / 'api/latest/custom_object',
|
|
@@ -969,10 +1089,9 @@ class Cli:
|
|
|
969
1089
|
'name': cde_name,
|
|
970
1090
|
'object_template_id': self.cde_template['id'],
|
|
971
1091
|
'value': {
|
|
972
|
-
'cde-
|
|
1092
|
+
'cde-snapshot': cde_snapshot_id,
|
|
973
1093
|
'user': self.user_info['identity_id'],
|
|
974
1094
|
'feature-branch-mapping': None,
|
|
975
|
-
'stop-at': stop_at,
|
|
976
1095
|
},
|
|
977
1096
|
},
|
|
978
1097
|
params={
|
|
@@ -985,18 +1104,28 @@ class Cli:
|
|
|
985
1104
|
if response.status != 200:
|
|
986
1105
|
logger.error('Failed to create CDE: %s (%s):\n%s', response.reason, response.status, await response.text())
|
|
987
1106
|
return
|
|
1107
|
+
response_json = await response.json()
|
|
1108
|
+
if response_json['id'] in { c['id'] for c in self.cdes.values() if 'id' in c}:
|
|
1109
|
+
logger.warning('Did not create a new CDE since a CDE with the name you chose already exists')
|
|
988
1110
|
|
|
989
1111
|
async def _select_cde(self: 'Cli', cde_name: str, *, quiet: bool = False) -> None:
|
|
990
1112
|
if self.cde is not None and self.cde['name'] != cde_name and self.ssh_client is not None:
|
|
991
1113
|
await self._disconnect_cde()
|
|
992
1114
|
try:
|
|
993
1115
|
self.cde = self.cdes[cde_name]
|
|
994
|
-
except
|
|
1116
|
+
except KeyError:
|
|
995
1117
|
logger.error('Cannot select CDE "%s". No such CDE', cde_name) # noqa: TRY400
|
|
996
1118
|
return
|
|
997
1119
|
if not quiet:
|
|
998
1120
|
logger.info('Selecting "%s" CDE', self.cde_name)
|
|
999
|
-
self.
|
|
1121
|
+
self.cde_snapshot = await self._get_cde_snapshot_of_cde(self.cde)
|
|
1122
|
+
if not self.cde_snapshot:
|
|
1123
|
+
logger.error('Cannot select CDE "%s". No such CDE snapshot', cde_name) # noqa: TRY400
|
|
1124
|
+
return
|
|
1125
|
+
self.cde_type = await self._get_cde_type_of_cde_snapshot(self.cde_snapshot)
|
|
1126
|
+
if not self.cde_type:
|
|
1127
|
+
logger.error('Cannot select CDE "%s". No such CDE type', cde_name) # noqa: TRY400
|
|
1128
|
+
return
|
|
1000
1129
|
self.config['global']['last_cde_name'] = self.cde_name
|
|
1001
1130
|
await self._write_config_file()
|
|
1002
1131
|
|
|
@@ -1022,8 +1151,10 @@ class Cli:
|
|
|
1022
1151
|
elif self.cde['status'] == 'connected':
|
|
1023
1152
|
await self._disconnect_cde()
|
|
1024
1153
|
else:
|
|
1025
|
-
|
|
1026
|
-
|
|
1154
|
+
await self._start_cde()
|
|
1155
|
+
await self._wait_running()
|
|
1156
|
+
# logger.error('CDE is not running. Cannot connect.')
|
|
1157
|
+
# return
|
|
1027
1158
|
|
|
1028
1159
|
async def _connect_cde(self: 'Cli') -> None:
|
|
1029
1160
|
logger.info('Connecting to CDE')
|
|
@@ -1077,7 +1208,7 @@ class Cli:
|
|
|
1077
1208
|
self.known_hosts_file = None
|
|
1078
1209
|
logger.debug('Disconnected from CDE')
|
|
1079
1210
|
|
|
1080
|
-
async def _configure_cde(self: 'Cli') -> None:
|
|
1211
|
+
async def _configure_cde(self: 'Cli', *, silent: bool = False) -> None:
|
|
1081
1212
|
await self._update_cde_list()
|
|
1082
1213
|
if not self.cde:
|
|
1083
1214
|
logger.error('No CDE is selected. Cannot configure CDE.')
|
|
@@ -1087,17 +1218,26 @@ class Cli:
|
|
|
1087
1218
|
logger.info('Creating new configuration for CDE "%s".', self.cde_name)
|
|
1088
1219
|
self.config[cde_config_key] = {
|
|
1089
1220
|
'cde_type': self.cde_type['name'],
|
|
1221
|
+
'cde_snapshot': self.cde_snapshot['name'],
|
|
1090
1222
|
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1223
|
+
default_source_directory = self.config['global']['source_directory_template'].format(folder=self.cde_type['name'].replace(' ', '-'))
|
|
1224
|
+
if silent:
|
|
1225
|
+
source_directory = default_source_directory
|
|
1226
|
+
else:
|
|
1227
|
+
source_directory = self._console_input(
|
|
1228
|
+
f'Choose a local directory where the sources of the "{self.cde_name}" CDE will be stored: ',
|
|
1229
|
+
prefill=self.config[cde_config_key].get('source_directory', default_source_directory),
|
|
1230
|
+
)
|
|
1095
1231
|
self.config[cde_config_key]['source_directory'] = source_directory
|
|
1096
1232
|
while True:
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1233
|
+
default_output_directory = self.config['global']['output_directory_template'].format(folder=self.cde_type['name'].replace(' ', '-'))
|
|
1234
|
+
if silent:
|
|
1235
|
+
output_directory = default_output_directory
|
|
1236
|
+
else:
|
|
1237
|
+
output_directory = self._console_input(
|
|
1238
|
+
f'Choose a local directory where the outputs of the "{self.cde_name}" CDE will be stored: ',
|
|
1239
|
+
prefill=self.config[cde_config_key].get('output_directory', default_output_directory),
|
|
1240
|
+
)
|
|
1101
1241
|
if (
|
|
1102
1242
|
_is_relative_to(source_directory, output_directory)
|
|
1103
1243
|
or _is_relative_to(output_directory, source_directory)
|
|
@@ -1106,18 +1246,6 @@ class Cli:
|
|
|
1106
1246
|
else:
|
|
1107
1247
|
break
|
|
1108
1248
|
self.config[cde_config_key]['output_directory'] = output_directory
|
|
1109
|
-
while True:
|
|
1110
|
-
maximum_uptime_hours = self._console_input(
|
|
1111
|
-
'How many hours should this CDE remain started until it is automatically stopped: ',
|
|
1112
|
-
prefill=self.config['global'].get('maximum_uptime_hours', '8'),
|
|
1113
|
-
)
|
|
1114
|
-
try:
|
|
1115
|
-
int(maximum_uptime_hours)
|
|
1116
|
-
except ValueError:
|
|
1117
|
-
logger.error('"%s" is not a valid number', maximum_uptime_hours) # noqa: TRY400
|
|
1118
|
-
else:
|
|
1119
|
-
break
|
|
1120
|
-
self.config[cde_config_key]['maximum_uptime_hours'] = maximum_uptime_hours
|
|
1121
1249
|
|
|
1122
1250
|
await self._write_config_file()
|
|
1123
1251
|
logger.info('CDE "%s" configured.', self.cde_name)
|
|
@@ -1157,15 +1285,12 @@ class Cli:
|
|
|
1157
1285
|
self.ssh_client = None
|
|
1158
1286
|
|
|
1159
1287
|
async def _start_cde_api_call(self: 'Cli') -> None:
|
|
1160
|
-
maximum_uptime_hours = int(self.config['global'].get('maximum_uptime_hours', '8'))
|
|
1161
|
-
stop_at = (datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=maximum_uptime_hours)).isoformat()
|
|
1162
1288
|
try:
|
|
1163
1289
|
response = await self.session.patch(
|
|
1164
1290
|
url=self.workspace_url / 'api/latest/custom_object' / self.cde['id'],
|
|
1165
1291
|
json={
|
|
1166
1292
|
'value': {
|
|
1167
1293
|
'is-running': True,
|
|
1168
|
-
'stop-at': stop_at,
|
|
1169
1294
|
},
|
|
1170
1295
|
},
|
|
1171
1296
|
)
|
|
@@ -1199,19 +1324,21 @@ class Cli:
|
|
|
1199
1324
|
logger.error('No CDE is selected. Cannot delete CDE.')
|
|
1200
1325
|
return
|
|
1201
1326
|
logger.info('Deleting CDE "%s"', self.cde_name)
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1327
|
+
if self.cde['exists_remotely']:
|
|
1328
|
+
try:
|
|
1329
|
+
response = await self.session.delete(
|
|
1330
|
+
url=self.workspace_url / 'api/latest/custom_object' / self.cde['id'],
|
|
1331
|
+
params={
|
|
1332
|
+
'permanently': 'true',
|
|
1333
|
+
'wait': 'false',
|
|
1334
|
+
},
|
|
1335
|
+
)
|
|
1336
|
+
except (aiohttp.ClientError, aiohttp.ClientResponseError) as ex:
|
|
1337
|
+
logger.error('Failed to delete CDE: %s', str(ex)) # noqa: TRY400
|
|
1338
|
+
return
|
|
1339
|
+
if response.status != 204:
|
|
1340
|
+
logger.error('Failed to delete CDE: %s (%s)', response.reason, response.status)
|
|
1341
|
+
return
|
|
1215
1342
|
if self.sync_task:
|
|
1216
1343
|
self.sync_task.cancel()
|
|
1217
1344
|
self.sync_task = None
|
|
@@ -1224,7 +1351,9 @@ class Cli:
|
|
|
1224
1351
|
if self.ssh_client is not None:
|
|
1225
1352
|
self.ssh_client.close()
|
|
1226
1353
|
self.ssh_client = None
|
|
1227
|
-
|
|
1354
|
+
self.config.remove_section(f'cde.{self.cde_name}')
|
|
1355
|
+
self.config['global'].pop('last_cde_name', None)
|
|
1356
|
+
await self._write_config_file()
|
|
1228
1357
|
#####
|
|
1229
1358
|
##### PORT FORWARDING
|
|
1230
1359
|
#####
|
|
@@ -1240,11 +1369,11 @@ class Cli:
|
|
|
1240
1369
|
logger.error('No CDE is selected. Cannot start port forwarding.')
|
|
1241
1370
|
return
|
|
1242
1371
|
if not self.is_cde_running:
|
|
1243
|
-
|
|
1244
|
-
|
|
1372
|
+
await self._start_cde()
|
|
1373
|
+
await self._wait_running()
|
|
1374
|
+
await self._connect_cde()
|
|
1245
1375
|
if self.ssh_client is None:
|
|
1246
|
-
|
|
1247
|
-
return
|
|
1376
|
+
await self._connect_cde()
|
|
1248
1377
|
self.port_forwarding_task = asyncio.create_task(self._bg_port_forwarding())
|
|
1249
1378
|
|
|
1250
1379
|
async def _stop_port_forwarding(self: 'Cli') -> None:
|
|
@@ -1347,7 +1476,7 @@ class Cli:
|
|
|
1347
1476
|
logger.info('Starting file sync')
|
|
1348
1477
|
try:
|
|
1349
1478
|
await self._init_local_cache()
|
|
1350
|
-
except
|
|
1479
|
+
except Exception as ex:
|
|
1351
1480
|
logger.error('Failed to initialize local cache: %s', str(ex)) # noqa: TRY400
|
|
1352
1481
|
return
|
|
1353
1482
|
filesystem_event_queue = asyncio.Queue()
|
|
@@ -1757,8 +1886,7 @@ class Cli:
|
|
|
1757
1886
|
logger.error('CDE is not running. Cannot follow logs.')
|
|
1758
1887
|
return
|
|
1759
1888
|
if self.ssh_client is None:
|
|
1760
|
-
|
|
1761
|
-
return
|
|
1889
|
+
await self._connect_cde()
|
|
1762
1890
|
self.logs_task = asyncio.create_task(self._bg_logs())
|
|
1763
1891
|
|
|
1764
1892
|
async def _stop_logs(self: 'Cli') -> None:
|
|
@@ -1803,8 +1931,7 @@ class Cli:
|
|
|
1803
1931
|
logger.error('CDE is not running. Cannot open terminal.')
|
|
1804
1932
|
return
|
|
1805
1933
|
if self.ssh_client is None:
|
|
1806
|
-
|
|
1807
|
-
return
|
|
1934
|
+
await self._connect_cde()
|
|
1808
1935
|
while True:
|
|
1809
1936
|
logger.info('Opening interactive terminal (press CTRL+D or enter "exit" to close)')
|
|
1810
1937
|
await self._reset_keyboard()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devstack-cli
|
|
3
|
-
Version:
|
|
3
|
+
Version: 12.0.0
|
|
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=EM-t3d1nXuJSNZgVPZhrnG9B1zoLmMxx42-Jgkr4NZc,88508
|
|
3
|
+
version.py,sha256=mNtENIUhfnFiRjZrydPEVD5_nACqcShUrR1gCwMAPPs,180
|
|
4
|
+
devstack_cli-12.0.0.dist-info/licenses/LICENSE,sha256=OBXZbEUMtIHIzyISkJ9fJlf_imds3rcKqeQu9yiyUJI,1055
|
|
5
|
+
devstack_cli-12.0.0.dist-info/METADATA,sha256=EOdNXkjxNxeI7BVl_up7U_yFxKmeR--fKVuinXGPMBg,4308
|
|
6
|
+
devstack_cli-12.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
7
|
+
devstack_cli-12.0.0.dist-info/entry_points.txt,sha256=f0xb4DIk0a7E5kyZ7YpoLhtjoagQj5VQpeBbW9a8A9Y,42
|
|
8
|
+
devstack_cli-12.0.0.dist-info/top_level.txt,sha256=lP8zvU46Am_G0MPcNmCI6f0sMfwpDUWpTROaPs-IEPk,21
|
|
9
|
+
devstack_cli-12.0.0.dist-info/RECORD,,
|
version.py
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
constants, set by build
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
MAJOR = '
|
|
6
|
-
BRANCH_NAME = 'release-
|
|
7
|
-
BUILD_DATE = '2026-
|
|
8
|
-
SHORT_SHA = '
|
|
9
|
-
VERSION = '
|
|
5
|
+
MAJOR = '12'
|
|
6
|
+
BRANCH_NAME = 'release-12'
|
|
7
|
+
BUILD_DATE = '2026-08-19-223353'
|
|
8
|
+
SHORT_SHA = '7b791d7'
|
|
9
|
+
VERSION = '12+release-12.2026-08-19-223353.7b791d7'
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
cli.py,sha256=DRhrl-OwH8pJr6pvSXJtKNNkoIzINEl-LNNmYXwe8RY,81907
|
|
3
|
-
version.py,sha256=NOsGwzvQoezBQy1dq3Q2XoNsuGtiNxTAIvxokDGGpOA,180
|
|
4
|
-
devstack_cli-11.0.255.dist-info/licenses/LICENSE,sha256=OBXZbEUMtIHIzyISkJ9fJlf_imds3rcKqeQu9yiyUJI,1055
|
|
5
|
-
devstack_cli-11.0.255.dist-info/METADATA,sha256=AVL6lUn-Da7JR4IAUyH6KHtnkyVtTLuT_6Fd_IKFH98,4310
|
|
6
|
-
devstack_cli-11.0.255.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
7
|
-
devstack_cli-11.0.255.dist-info/entry_points.txt,sha256=f0xb4DIk0a7E5kyZ7YpoLhtjoagQj5VQpeBbW9a8A9Y,42
|
|
8
|
-
devstack_cli-11.0.255.dist-info/top_level.txt,sha256=lP8zvU46Am_G0MPcNmCI6f0sMfwpDUWpTROaPs-IEPk,21
|
|
9
|
-
devstack_cli-11.0.255.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|