cloudx-proxy 0.3.6__py3-none-any.whl → 0.3.8__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.
- cloudx_proxy/_version.py +2 -2
- cloudx_proxy/setup.py +130 -142
- {cloudx_proxy-0.3.6.dist-info → cloudx_proxy-0.3.8.dist-info}/METADATA +1 -1
- cloudx_proxy-0.3.8.dist-info/RECORD +11 -0
- cloudx_proxy-0.3.6.dist-info/RECORD +0 -11
- {cloudx_proxy-0.3.6.dist-info → cloudx_proxy-0.3.8.dist-info}/LICENSE +0 -0
- {cloudx_proxy-0.3.6.dist-info → cloudx_proxy-0.3.8.dist-info}/WHEEL +0 -0
- {cloudx_proxy-0.3.6.dist-info → cloudx_proxy-0.3.8.dist-info}/entry_points.txt +0 -0
- {cloudx_proxy-0.3.6.dist-info → cloudx_proxy-0.3.8.dist-info}/top_level.txt +0 -0
cloudx_proxy/_version.py
CHANGED
cloudx_proxy/setup.py
CHANGED
@@ -24,7 +24,6 @@ class CloudXSetup:
|
|
24
24
|
self.ssh_dir = Path(self.home_dir) / ".ssh" / "vscode"
|
25
25
|
self.ssh_config_file = self.ssh_dir / "config"
|
26
26
|
self.ssh_key_file = self.ssh_dir / f"{ssh_key}"
|
27
|
-
self.using_1password = False
|
28
27
|
self.default_env = None
|
29
28
|
|
30
29
|
def print_header(self, text: str) -> None:
|
@@ -143,17 +142,6 @@ class CloudXSetup:
|
|
143
142
|
|
144
143
|
if key_exists:
|
145
144
|
self.print_status(f"SSH key '{self.ssh_key}' exists", True, 2)
|
146
|
-
if platform.system() != 'Windows':
|
147
|
-
self.using_1password = self.prompt("Would you like to use 1Password SSH agent?", "N").lower() == 'y'
|
148
|
-
if self.using_1password:
|
149
|
-
self.print_status("Using 1Password SSH agent", True, 2)
|
150
|
-
else:
|
151
|
-
store_in_1password = self.prompt("Would you like to store the private key in 1Password?", "N").lower() == 'y'
|
152
|
-
if store_in_1password:
|
153
|
-
if self._store_key_in_1password():
|
154
|
-
self.print_status("Private key stored in 1Password", True, 2)
|
155
|
-
else:
|
156
|
-
self.print_status("Failed to store private key in 1Password", False, 2)
|
157
145
|
else:
|
158
146
|
self.print_status(f"Generating new SSH key '{self.ssh_key}'...", None, 2)
|
159
147
|
subprocess.run([
|
@@ -164,17 +152,6 @@ class CloudXSetup:
|
|
164
152
|
], check=True)
|
165
153
|
self.print_status("SSH key generated", True, 2)
|
166
154
|
|
167
|
-
if platform.system() != 'Windows':
|
168
|
-
self.using_1password = self.prompt("Would you like to use 1Password SSH agent?", "N").lower() == 'y'
|
169
|
-
if self.using_1password:
|
170
|
-
self.print_status("Using 1Password SSH agent", True, 2)
|
171
|
-
else:
|
172
|
-
store_in_1password = self.prompt("Would you like to store the private key in 1Password?", "N").lower() == 'y'
|
173
|
-
if store_in_1password:
|
174
|
-
if self._store_key_in_1password():
|
175
|
-
self.print_status("Private key stored in 1Password", True, 2)
|
176
|
-
else:
|
177
|
-
self.print_status("Failed to store private key in 1Password", False, 2)
|
178
155
|
|
179
156
|
return True
|
180
157
|
|
@@ -186,44 +163,6 @@ class CloudXSetup:
|
|
186
163
|
return True
|
187
164
|
return False
|
188
165
|
|
189
|
-
def _store_key_in_1password(self) -> bool:
|
190
|
-
"""Store SSH private key in 1Password.
|
191
|
-
|
192
|
-
Returns:
|
193
|
-
bool: True if key was stored successfully
|
194
|
-
"""
|
195
|
-
try:
|
196
|
-
# Check if 1Password CLI is available
|
197
|
-
subprocess.run(['op', '--version'], check=True, capture_output=True)
|
198
|
-
|
199
|
-
# Check if 1Password SSH agent is running
|
200
|
-
agent_sock = Path.home() / ".1password" / "agent.sock"
|
201
|
-
if platform.system() == 'Darwin':
|
202
|
-
agent_sock = Path.home() / "Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"
|
203
|
-
|
204
|
-
if not agent_sock.exists():
|
205
|
-
self.print_status("1Password SSH agent not running. Please enable it in 1Password settings.", False, 2)
|
206
|
-
return False
|
207
|
-
|
208
|
-
# Read private key content
|
209
|
-
with open(self.ssh_key_file, 'r') as f:
|
210
|
-
private_key = f.read()
|
211
|
-
|
212
|
-
# Store private key in 1Password as document
|
213
|
-
subprocess.run([
|
214
|
-
'op', 'document', 'create',
|
215
|
-
'--title', f'cloudx-proxy SSH Key - {self.ssh_key}',
|
216
|
-
], input=private_key.encode(), check=True)
|
217
|
-
|
218
|
-
# Remove private key file but keep public key
|
219
|
-
os.remove(self.ssh_key_file)
|
220
|
-
self.print_status("Private key stored in 1Password and removed from disk", True, 2)
|
221
|
-
self.print_status("Please import the key into 1Password SSH agent through the desktop app", True, 2)
|
222
|
-
return True
|
223
|
-
except subprocess.CalledProcessError:
|
224
|
-
print("Error: 1Password CLI not installed or not signed in.")
|
225
|
-
return False
|
226
|
-
|
227
166
|
def _add_host_entry(self, cloudx_env: str, instance_id: str, hostname: str, current_config: str) -> bool:
|
228
167
|
"""Add settings to a specific host entry.
|
229
168
|
|
@@ -251,18 +190,7 @@ Host cloudx-{cloudx_env}-{hostname}
|
|
251
190
|
HostName {instance_id}
|
252
191
|
User ec2-user
|
253
192
|
"""
|
254
|
-
|
255
|
-
# Use platform-specific agent socket path
|
256
|
-
if platform.system() == 'Darwin': # macOS
|
257
|
-
agent_sock = "~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"
|
258
|
-
else: # Linux
|
259
|
-
agent_sock = "~/.1password/agent.sock"
|
260
|
-
host_entry += f""" IdentityAgent {agent_sock}
|
261
|
-
IdentityFile {self.ssh_key_file}.pub
|
262
|
-
IdentitiesOnly yes
|
263
|
-
"""
|
264
|
-
else:
|
265
|
-
host_entry += f""" IdentityFile {self.ssh_key_file}
|
193
|
+
host_entry += f""" IdentityFile {self.ssh_key_file}
|
266
194
|
IdentitiesOnly yes
|
267
195
|
"""
|
268
196
|
host_entry += f""" ProxyCommand {proxy_command}
|
@@ -372,19 +300,7 @@ Host cloudx-{cloudx_env}-*
|
|
372
300
|
User ec2-user
|
373
301
|
"""
|
374
302
|
# Add key configuration
|
375
|
-
|
376
|
-
# Use platform-specific agent socket path
|
377
|
-
if platform.system() == 'Darwin': # macOS
|
378
|
-
agent_sock = "~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"
|
379
|
-
else: # Linux
|
380
|
-
agent_sock = "~/.1password/agent.sock"
|
381
|
-
base_config += f""" IdentityAgent {agent_sock}
|
382
|
-
IdentityFile {self.ssh_key_file}.pub
|
383
|
-
IdentitiesOnly yes
|
384
|
-
|
385
|
-
"""
|
386
|
-
else:
|
387
|
-
base_config += f""" IdentityFile {self.ssh_key_file}
|
303
|
+
base_config += f""" IdentityFile {self.ssh_key_file}
|
388
304
|
IdentitiesOnly yes
|
389
305
|
|
390
306
|
"""
|
@@ -441,71 +357,94 @@ Host cloudx-{cloudx_env}-{hostname}
|
|
441
357
|
return True
|
442
358
|
return False
|
443
359
|
|
444
|
-
def check_instance_setup(self, instance_id: str) -> Tuple[bool, bool]:
|
360
|
+
def check_instance_setup(self, instance_id: str) -> Tuple[bool, bool, bool]:
|
445
361
|
"""Check if instance setup is complete.
|
446
362
|
|
447
363
|
Args:
|
448
364
|
instance_id: EC2 instance ID
|
449
365
|
|
450
366
|
Returns:
|
451
|
-
Tuple[bool, bool]: (is_running, is_setup_complete)
|
367
|
+
Tuple[bool, bool, bool]: (ssm_accessible, is_running, is_setup_complete)
|
452
368
|
"""
|
453
369
|
try:
|
454
370
|
session = boto3.Session(profile_name=self.profile)
|
455
371
|
ssm = session.client('ssm')
|
372
|
+
ec2 = session.client('ec2')
|
456
373
|
|
457
|
-
#
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
DocumentName='AWS-RunShellScript',
|
475
|
-
Parameters={
|
476
|
-
'commands': [
|
477
|
-
'test -f /home/ec2-user/.install-done && echo "DONE" || '
|
478
|
-
'test -f /home/ec2-user/.install-running && echo "RUNNING" || '
|
479
|
-
'echo "NOT_STARTED"'
|
480
|
-
]
|
481
|
-
}
|
482
|
-
)
|
483
|
-
|
484
|
-
command_id = response['Command']['CommandId']
|
374
|
+
# First check if instance exists and its power state
|
375
|
+
try:
|
376
|
+
# Use the provided profile for EC2 operations
|
377
|
+
response = session.client('ec2').describe_instances(InstanceIds=[instance_id])
|
378
|
+
if not response['Reservations']:
|
379
|
+
self.print_status("Instance not found", False, 4)
|
380
|
+
return False, False, False
|
381
|
+
|
382
|
+
instance = response['Reservations'][0]['Instances'][0]
|
383
|
+
state = instance['State']['Name']
|
384
|
+
|
385
|
+
if state != 'running':
|
386
|
+
self.print_status(f"Instance is {state}", False, 4)
|
387
|
+
return True, False, False
|
388
|
+
except Exception as e:
|
389
|
+
self.print_status(f"Error checking instance state: {e}", False, 4)
|
390
|
+
return False, False, False
|
485
391
|
|
486
|
-
#
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
InstanceId=instance_id
|
392
|
+
# Now check SSM connectivity
|
393
|
+
try:
|
394
|
+
self.print_status("Checking SSM connectivity...", None, 4)
|
395
|
+
response = ssm.describe_instance_information(
|
396
|
+
Filters=[{'Key': 'InstanceIds', 'Values': [instance_id]}]
|
492
397
|
)
|
493
|
-
if
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
status
|
502
|
-
|
503
|
-
|
504
|
-
|
398
|
+
if not response['InstanceInformationList']:
|
399
|
+
self.print_status("Instance is running but not yet accessible via SSM", False, 4)
|
400
|
+
self.print_status("This is normal if the instance is still configuring", None, 4)
|
401
|
+
return True, True, False
|
402
|
+
|
403
|
+
self.print_status("SSM connection established", True, 4)
|
404
|
+
|
405
|
+
# Check setup status using SSM command
|
406
|
+
self.print_status("Checking setup status...", None, 4)
|
407
|
+
response = ssm.send_command(
|
408
|
+
InstanceIds=[instance_id],
|
409
|
+
DocumentName='AWS-RunShellScript',
|
410
|
+
Parameters={
|
411
|
+
'commands': [
|
412
|
+
'test -f /home/ec2-user/.install-done && echo "DONE" || '
|
413
|
+
'test -f /home/ec2-user/.install-running && echo "RUNNING" || '
|
414
|
+
'echo "NOT_STARTED"'
|
415
|
+
]
|
416
|
+
}
|
417
|
+
)
|
418
|
+
|
419
|
+
command_id = response['Command']['CommandId']
|
420
|
+
|
421
|
+
# Wait for command completion
|
422
|
+
for _ in range(10): # 10 second timeout
|
423
|
+
time.sleep(1)
|
424
|
+
result = ssm.get_command_invocation(
|
425
|
+
CommandId=command_id,
|
426
|
+
InstanceId=instance_id
|
427
|
+
)
|
428
|
+
if result['Status'] in ['Success', 'Failed']:
|
429
|
+
break
|
430
|
+
|
431
|
+
is_setup_complete = result['Status'] == 'Success' and result['StandardOutputContent'].strip() == 'DONE'
|
432
|
+
|
433
|
+
if is_setup_complete:
|
434
|
+
self.print_status("Setup is complete", True, 4)
|
435
|
+
else:
|
436
|
+
status = result['StandardOutputContent'].strip()
|
437
|
+
self.print_status(f"Setup status: {status}", None, 4)
|
438
|
+
|
439
|
+
return True, True, is_setup_complete
|
440
|
+
|
441
|
+
except Exception as e:
|
442
|
+
self.print_status(f"Error checking SSM status: {e}", False, 4)
|
443
|
+
return False, True, False
|
505
444
|
|
506
445
|
except Exception as e:
|
507
446
|
self.print_status(f"\033[1;91mError:\033[0m {str(e)}", False, 4)
|
508
|
-
return False, False
|
447
|
+
return False, False, False
|
509
448
|
|
510
449
|
def wait_for_setup_completion(self, instance_id: str) -> bool:
|
511
450
|
"""Wait for instance setup to complete.
|
@@ -514,25 +453,66 @@ Host cloudx-{cloudx_env}-{hostname}
|
|
514
453
|
instance_id: EC2 instance ID
|
515
454
|
|
516
455
|
Returns:
|
517
|
-
bool: True if setup completed successfully
|
456
|
+
bool: True if setup completed successfully or user chose to continue
|
518
457
|
"""
|
519
458
|
self.print_header("Instance Setup Check")
|
520
|
-
self.print_status(f"Checking instance {instance_id}
|
459
|
+
self.print_status(f"Checking instance {instance_id} status...")
|
521
460
|
|
522
|
-
is_running, is_complete = self.check_instance_setup(instance_id)
|
461
|
+
ssm_accessible, is_running, is_complete = self.check_instance_setup(instance_id)
|
523
462
|
|
524
|
-
if not is_running:
|
525
|
-
self.print_status("Instance is not running or not accessible via SSM", False, 2)
|
463
|
+
if not ssm_accessible and not is_running:
|
526
464
|
continue_setup = self.prompt("Would you like to continue anyway?", "Y").lower() != 'n'
|
527
465
|
if continue_setup:
|
528
466
|
self.print_status("Continuing setup despite instance access issues", None, 2)
|
529
467
|
return True
|
530
468
|
return False
|
531
469
|
|
470
|
+
if not is_running:
|
471
|
+
start_instance = self.prompt("Would you like to start the instance?", "Y").lower() != 'n'
|
472
|
+
if not start_instance:
|
473
|
+
return False
|
474
|
+
|
475
|
+
try:
|
476
|
+
# Use the provided profile for EC2 operations
|
477
|
+
session = boto3.Session(profile_name=self.profile)
|
478
|
+
session.client('ec2').start_instances(InstanceIds=[instance_id])
|
479
|
+
self.print_status("Instance start requested. This may take a few minutes...", None, 2)
|
480
|
+
|
481
|
+
# Wait for instance to start
|
482
|
+
for _ in range(30): # 5 minute timeout
|
483
|
+
time.sleep(10)
|
484
|
+
ssm_accessible, is_running, is_complete = self.check_instance_setup(instance_id)
|
485
|
+
if is_running:
|
486
|
+
break
|
487
|
+
|
488
|
+
if not is_running:
|
489
|
+
self.print_status("Timeout waiting for instance to start", False, 2)
|
490
|
+
return False
|
491
|
+
except Exception as e:
|
492
|
+
self.print_status(f"Error starting instance: {e}", False, 2)
|
493
|
+
return False
|
494
|
+
|
532
495
|
if is_complete:
|
533
496
|
self.print_status("Instance setup is complete", True, 2)
|
534
497
|
return True
|
535
498
|
|
499
|
+
if not ssm_accessible:
|
500
|
+
self.print_status("Waiting for SSM access...", None, 2)
|
501
|
+
# Wait for SSM access
|
502
|
+
for _ in range(30): # 5 minute timeout
|
503
|
+
time.sleep(10)
|
504
|
+
ssm_accessible, is_running, is_complete = self.check_instance_setup(instance_id)
|
505
|
+
if ssm_accessible or not is_running:
|
506
|
+
break
|
507
|
+
|
508
|
+
if not ssm_accessible:
|
509
|
+
self.print_status("Timeout waiting for SSM access", False, 2)
|
510
|
+
continue_setup = self.prompt("Would you like to continue anyway?", "Y").lower() != 'n'
|
511
|
+
if continue_setup:
|
512
|
+
self.print_status("Continuing setup despite SSM access issues", None, 2)
|
513
|
+
return True
|
514
|
+
return False
|
515
|
+
|
536
516
|
wait = self.prompt("Instance setup is not complete. Would you like to wait?", "Y").lower() != 'n'
|
537
517
|
if not wait:
|
538
518
|
self.print_status("Skipping instance setup check", None, 2)
|
@@ -541,16 +521,24 @@ Host cloudx-{cloudx_env}-{hostname}
|
|
541
521
|
self.print_status("Waiting for setup to complete...", None, 2)
|
542
522
|
dots = 0
|
543
523
|
while True:
|
544
|
-
is_running, is_complete = self.check_instance_setup(instance_id)
|
524
|
+
ssm_accessible, is_running, is_complete = self.check_instance_setup(instance_id)
|
545
525
|
|
546
526
|
if not is_running:
|
547
|
-
self.print_status("Instance is no longer running
|
527
|
+
self.print_status("Instance is no longer running", False, 2)
|
548
528
|
continue_setup = self.prompt("Would you like to continue anyway?", "Y").lower() != 'n'
|
549
529
|
if continue_setup:
|
550
530
|
self.print_status("Continuing setup despite instance issues", None, 2)
|
551
531
|
return True
|
552
532
|
return False
|
553
533
|
|
534
|
+
if not ssm_accessible:
|
535
|
+
self.print_status("Lost SSM access to instance", False, 2)
|
536
|
+
continue_setup = self.prompt("Would you like to continue anyway?", "Y").lower() != 'n'
|
537
|
+
if continue_setup:
|
538
|
+
self.print_status("Continuing setup despite SSM access issues", None, 2)
|
539
|
+
return True
|
540
|
+
return False
|
541
|
+
|
554
542
|
if is_complete:
|
555
543
|
self.print_status("Instance setup completed successfully", True, 2)
|
556
544
|
return True
|
@@ -0,0 +1,11 @@
|
|
1
|
+
cloudx_proxy/__init__.py,sha256=ZZ2O_m9OFJm18AxMSuYJt4UjSuSqyJlYRaZMoets498,61
|
2
|
+
cloudx_proxy/_version.py,sha256=FAr0t5Ub_Olk5Ke3Xi4Oeu5jcLPAvKpdk9naAtMuou8,411
|
3
|
+
cloudx_proxy/cli.py,sha256=Ph-m8lDsdU2zZab9Y6YgBBzd_UDouBnfNrYFFx0bI_E,3426
|
4
|
+
cloudx_proxy/core.py,sha256=WjKoqMmmnt6e_4JMeq4gTka75JAvQcMUs9r9XUBLmFE,7289
|
5
|
+
cloudx_proxy/setup.py,sha256=96FbRtGKHYa-V26wTP0m-JtkAFhGULgnCOO0PStB8ds,23725
|
6
|
+
cloudx_proxy-0.3.8.dist-info/LICENSE,sha256=i7P2OR4zsJYsMWcCUDe_B9ZfGi9bU0K5I2nKfDrW_N8,1068
|
7
|
+
cloudx_proxy-0.3.8.dist-info/METADATA,sha256=EdfWsWYRH1GvR6scuD3HZTEqnMk3ljD_VCOaL4YNkx8,14037
|
8
|
+
cloudx_proxy-0.3.8.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
9
|
+
cloudx_proxy-0.3.8.dist-info/entry_points.txt,sha256=HGt743N2lVlKd7O1qWq3C0aEHyS5PjPnxzDHh7hwtSg,54
|
10
|
+
cloudx_proxy-0.3.8.dist-info/top_level.txt,sha256=2wtEote1db21j-VvkCJFfT-dLlauuG5indjggYh3xDg,13
|
11
|
+
cloudx_proxy-0.3.8.dist-info/RECORD,,
|
@@ -1,11 +0,0 @@
|
|
1
|
-
cloudx_proxy/__init__.py,sha256=ZZ2O_m9OFJm18AxMSuYJt4UjSuSqyJlYRaZMoets498,61
|
2
|
-
cloudx_proxy/_version.py,sha256=IKAQ4gPrCQ2FWMXOFRqouULC2EQI1zCb4iXHsnfbmTQ,411
|
3
|
-
cloudx_proxy/cli.py,sha256=Ph-m8lDsdU2zZab9Y6YgBBzd_UDouBnfNrYFFx0bI_E,3426
|
4
|
-
cloudx_proxy/core.py,sha256=WjKoqMmmnt6e_4JMeq4gTka75JAvQcMUs9r9XUBLmFE,7289
|
5
|
-
cloudx_proxy/setup.py,sha256=3Kz-VWid0EL4rIEyf30UuLEe3Rvo1Mn8rKIa5WnysFE,24378
|
6
|
-
cloudx_proxy-0.3.6.dist-info/LICENSE,sha256=i7P2OR4zsJYsMWcCUDe_B9ZfGi9bU0K5I2nKfDrW_N8,1068
|
7
|
-
cloudx_proxy-0.3.6.dist-info/METADATA,sha256=KwikAp1MVPQDaSABlt8esDM9448EIRgMitb394SH4JM,14037
|
8
|
-
cloudx_proxy-0.3.6.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
9
|
-
cloudx_proxy-0.3.6.dist-info/entry_points.txt,sha256=HGt743N2lVlKd7O1qWq3C0aEHyS5PjPnxzDHh7hwtSg,54
|
10
|
-
cloudx_proxy-0.3.6.dist-info/top_level.txt,sha256=2wtEote1db21j-VvkCJFfT-dLlauuG5indjggYh3xDg,13
|
11
|
-
cloudx_proxy-0.3.6.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|