cloudx-proxy 0.3.5__tar.gz → 0.3.7__tar.gz
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-0.3.5 → cloudx_proxy-0.3.7}/CHANGELOG.md +14 -0
- {cloudx_proxy-0.3.5 → cloudx_proxy-0.3.7}/PKG-INFO +1 -1
- {cloudx_proxy-0.3.5 → cloudx_proxy-0.3.7}/cloudx_proxy/_version.py +2 -2
- {cloudx_proxy-0.3.5 → cloudx_proxy-0.3.7}/cloudx_proxy/setup.py +129 -154
- {cloudx_proxy-0.3.5 → cloudx_proxy-0.3.7}/cloudx_proxy.egg-info/PKG-INFO +1 -1
- {cloudx_proxy-0.3.5 → cloudx_proxy-0.3.7}/.github/workflows/release.yml +0 -0
- {cloudx_proxy-0.3.5 → cloudx_proxy-0.3.7}/.gitignore +0 -0
- {cloudx_proxy-0.3.5 → cloudx_proxy-0.3.7}/.releaserc +0 -0
- {cloudx_proxy-0.3.5 → cloudx_proxy-0.3.7}/CONTRIBUTING.md +0 -0
- {cloudx_proxy-0.3.5 → cloudx_proxy-0.3.7}/LICENSE +0 -0
- {cloudx_proxy-0.3.5 → cloudx_proxy-0.3.7}/README.md +0 -0
- {cloudx_proxy-0.3.5 → cloudx_proxy-0.3.7}/cloudx_proxy/__init__.py +0 -0
- {cloudx_proxy-0.3.5 → cloudx_proxy-0.3.7}/cloudx_proxy/cli.py +0 -0
- {cloudx_proxy-0.3.5 → cloudx_proxy-0.3.7}/cloudx_proxy/core.py +0 -0
- {cloudx_proxy-0.3.5 → cloudx_proxy-0.3.7}/cloudx_proxy.egg-info/SOURCES.txt +0 -0
- {cloudx_proxy-0.3.5 → cloudx_proxy-0.3.7}/cloudx_proxy.egg-info/dependency_links.txt +0 -0
- {cloudx_proxy-0.3.5 → cloudx_proxy-0.3.7}/cloudx_proxy.egg-info/entry_points.txt +0 -0
- {cloudx_proxy-0.3.5 → cloudx_proxy-0.3.7}/cloudx_proxy.egg-info/requires.txt +0 -0
- {cloudx_proxy-0.3.5 → cloudx_proxy-0.3.7}/cloudx_proxy.egg-info/top_level.txt +0 -0
- {cloudx_proxy-0.3.5 → cloudx_proxy-0.3.7}/package.json +0 -0
- {cloudx_proxy-0.3.5 → cloudx_proxy-0.3.7}/pyproject.toml +0 -0
- {cloudx_proxy-0.3.5 → cloudx_proxy-0.3.7}/setup.cfg +0 -0
@@ -1,3 +1,17 @@
|
|
1
|
+
## [0.3.7](https://github.com/easytocloud/cloudX-proxy/compare/v0.3.6...v0.3.7) (2025-02-11)
|
2
|
+
|
3
|
+
|
4
|
+
### Bug Fixes
|
5
|
+
|
6
|
+
* removed 1Password support and improved instance monitoring ([c01009a](https://github.com/easytocloud/cloudX-proxy/commit/c01009afb6d90f43768dece909351a8c3ca597ce))
|
7
|
+
|
8
|
+
## [0.3.6](https://github.com/easytocloud/cloudX-proxy/compare/v0.3.5...v0.3.6) (2025-02-11)
|
9
|
+
|
10
|
+
|
11
|
+
### Bug Fixes
|
12
|
+
|
13
|
+
* 1Password integration repaired ([b8fd8eb](https://github.com/easytocloud/cloudX-proxy/commit/b8fd8eb445e2ccae107b01c796bc5bec1a9fa1d3))
|
14
|
+
|
1
15
|
## [0.3.5](https://github.com/easytocloud/cloudX-proxy/compare/v0.3.4...v0.3.5) (2025-02-11)
|
2
16
|
|
3
17
|
|
@@ -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,56 +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 and configure SSH agent.
|
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
|
-
print("Adding SSH key to 1Password SSH agent...")
|
209
|
-
try:
|
210
|
-
# First try to add to SSH agent
|
211
|
-
subprocess.run([
|
212
|
-
'op', 'ssh-add',
|
213
|
-
'--name', f'cloudx-proxy-{self.ssh_key}',
|
214
|
-
str(self.ssh_key_file)
|
215
|
-
], check=True)
|
216
|
-
|
217
|
-
# Read private key content before removing
|
218
|
-
with open(self.ssh_key_file, 'r') as f:
|
219
|
-
private_key = f.read()
|
220
|
-
|
221
|
-
# Store private key in 1Password as document for backup
|
222
|
-
subprocess.run([
|
223
|
-
'op', 'document', 'create',
|
224
|
-
'--title', f'cloudx-proxy SSH Key - {self.ssh_key}',
|
225
|
-
], input=private_key.encode(), check=True)
|
226
|
-
|
227
|
-
# Remove private key file but keep public key
|
228
|
-
os.remove(self.ssh_key_file)
|
229
|
-
self.print_status("Private key added to 1Password SSH agent and removed from disk", True, 2)
|
230
|
-
self.print_status("Backup copy stored in 1Password documents", True, 2)
|
231
|
-
return True
|
232
|
-
except subprocess.CalledProcessError as e:
|
233
|
-
self.print_status(f"Failed to add key to SSH agent: {e}", False, 2)
|
234
|
-
return False
|
235
|
-
except subprocess.CalledProcessError:
|
236
|
-
print("Error: 1Password CLI not installed or not signed in.")
|
237
|
-
return False
|
238
|
-
|
239
166
|
def _add_host_entry(self, cloudx_env: str, instance_id: str, hostname: str, current_config: str) -> bool:
|
240
167
|
"""Add settings to a specific host entry.
|
241
168
|
|
@@ -263,18 +190,7 @@ Host cloudx-{cloudx_env}-{hostname}
|
|
263
190
|
HostName {instance_id}
|
264
191
|
User ec2-user
|
265
192
|
"""
|
266
|
-
|
267
|
-
# Use platform-specific agent socket path
|
268
|
-
if platform.system() == 'Darwin': # macOS
|
269
|
-
agent_sock = "~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"
|
270
|
-
else: # Linux
|
271
|
-
agent_sock = "~/.1password/agent.sock"
|
272
|
-
host_entry += f""" IdentityAgent {agent_sock}
|
273
|
-
IdentityFile {self.ssh_key_file}.pub
|
274
|
-
IdentitiesOnly yes
|
275
|
-
"""
|
276
|
-
else:
|
277
|
-
host_entry += f""" IdentityFile {self.ssh_key_file}
|
193
|
+
host_entry += f""" IdentityFile {self.ssh_key_file}
|
278
194
|
IdentitiesOnly yes
|
279
195
|
"""
|
280
196
|
host_entry += f""" ProxyCommand {proxy_command}
|
@@ -384,19 +300,7 @@ Host cloudx-{cloudx_env}-*
|
|
384
300
|
User ec2-user
|
385
301
|
"""
|
386
302
|
# Add key configuration
|
387
|
-
|
388
|
-
# Use platform-specific agent socket path
|
389
|
-
if platform.system() == 'Darwin': # macOS
|
390
|
-
agent_sock = "~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"
|
391
|
-
else: # Linux
|
392
|
-
agent_sock = "~/.1password/agent.sock"
|
393
|
-
base_config += f""" IdentityAgent {agent_sock}
|
394
|
-
IdentityFile {self.ssh_key_file}.pub
|
395
|
-
IdentitiesOnly yes
|
396
|
-
|
397
|
-
"""
|
398
|
-
else:
|
399
|
-
base_config += f""" IdentityFile {self.ssh_key_file}
|
303
|
+
base_config += f""" IdentityFile {self.ssh_key_file}
|
400
304
|
IdentitiesOnly yes
|
401
305
|
|
402
306
|
"""
|
@@ -453,71 +357,93 @@ Host cloudx-{cloudx_env}-{hostname}
|
|
453
357
|
return True
|
454
358
|
return False
|
455
359
|
|
456
|
-
def check_instance_setup(self, instance_id: str) -> Tuple[bool, bool]:
|
360
|
+
def check_instance_setup(self, instance_id: str) -> Tuple[bool, bool, bool]:
|
457
361
|
"""Check if instance setup is complete.
|
458
362
|
|
459
363
|
Args:
|
460
364
|
instance_id: EC2 instance ID
|
461
365
|
|
462
366
|
Returns:
|
463
|
-
Tuple[bool, bool]: (is_running, is_setup_complete)
|
367
|
+
Tuple[bool, bool, bool]: (ssm_accessible, is_running, is_setup_complete)
|
464
368
|
"""
|
465
369
|
try:
|
466
370
|
session = boto3.Session(profile_name=self.profile)
|
467
371
|
ssm = session.client('ssm')
|
372
|
+
ec2 = session.client('ec2')
|
468
373
|
|
469
|
-
#
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
InstanceIds=[instance_id],
|
486
|
-
DocumentName='AWS-RunShellScript',
|
487
|
-
Parameters={
|
488
|
-
'commands': [
|
489
|
-
'test -f /home/ec2-user/.install-done && echo "DONE" || '
|
490
|
-
'test -f /home/ec2-user/.install-running && echo "RUNNING" || '
|
491
|
-
'echo "NOT_STARTED"'
|
492
|
-
]
|
493
|
-
}
|
494
|
-
)
|
495
|
-
|
496
|
-
command_id = response['Command']['CommandId']
|
374
|
+
# First check if instance exists and its power state
|
375
|
+
try:
|
376
|
+
response = ec2.describe_instances(InstanceIds=[instance_id])
|
377
|
+
if not response['Reservations']:
|
378
|
+
self.print_status("Instance not found", False, 4)
|
379
|
+
return False, False, False
|
380
|
+
|
381
|
+
instance = response['Reservations'][0]['Instances'][0]
|
382
|
+
state = instance['State']['Name']
|
383
|
+
|
384
|
+
if state != 'running':
|
385
|
+
self.print_status(f"Instance is {state}", False, 4)
|
386
|
+
return True, False, False
|
387
|
+
except Exception as e:
|
388
|
+
self.print_status(f"Error checking instance state: {e}", False, 4)
|
389
|
+
return False, False, False
|
497
390
|
|
498
|
-
#
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
InstanceId=instance_id
|
391
|
+
# Now check SSM connectivity
|
392
|
+
try:
|
393
|
+
self.print_status("Checking SSM connectivity...", None, 4)
|
394
|
+
response = ssm.describe_instance_information(
|
395
|
+
Filters=[{'Key': 'InstanceIds', 'Values': [instance_id]}]
|
504
396
|
)
|
505
|
-
if
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
status
|
514
|
-
|
515
|
-
|
516
|
-
|
397
|
+
if not response['InstanceInformationList']:
|
398
|
+
self.print_status("Instance is running but not yet accessible via SSM", False, 4)
|
399
|
+
self.print_status("This is normal if the instance is still configuring", None, 4)
|
400
|
+
return True, True, False
|
401
|
+
|
402
|
+
self.print_status("SSM connection established", True, 4)
|
403
|
+
|
404
|
+
# Check setup status using SSM command
|
405
|
+
self.print_status("Checking setup status...", None, 4)
|
406
|
+
response = ssm.send_command(
|
407
|
+
InstanceIds=[instance_id],
|
408
|
+
DocumentName='AWS-RunShellScript',
|
409
|
+
Parameters={
|
410
|
+
'commands': [
|
411
|
+
'test -f /home/ec2-user/.install-done && echo "DONE" || '
|
412
|
+
'test -f /home/ec2-user/.install-running && echo "RUNNING" || '
|
413
|
+
'echo "NOT_STARTED"'
|
414
|
+
]
|
415
|
+
}
|
416
|
+
)
|
417
|
+
|
418
|
+
command_id = response['Command']['CommandId']
|
419
|
+
|
420
|
+
# Wait for command completion
|
421
|
+
for _ in range(10): # 10 second timeout
|
422
|
+
time.sleep(1)
|
423
|
+
result = ssm.get_command_invocation(
|
424
|
+
CommandId=command_id,
|
425
|
+
InstanceId=instance_id
|
426
|
+
)
|
427
|
+
if result['Status'] in ['Success', 'Failed']:
|
428
|
+
break
|
429
|
+
|
430
|
+
is_setup_complete = result['Status'] == 'Success' and result['StandardOutputContent'].strip() == 'DONE'
|
431
|
+
|
432
|
+
if is_setup_complete:
|
433
|
+
self.print_status("Setup is complete", True, 4)
|
434
|
+
else:
|
435
|
+
status = result['StandardOutputContent'].strip()
|
436
|
+
self.print_status(f"Setup status: {status}", None, 4)
|
437
|
+
|
438
|
+
return True, True, is_setup_complete
|
439
|
+
|
440
|
+
except Exception as e:
|
441
|
+
self.print_status(f"Error checking SSM status: {e}", False, 4)
|
442
|
+
return False, True, False
|
517
443
|
|
518
444
|
except Exception as e:
|
519
445
|
self.print_status(f"\033[1;91mError:\033[0m {str(e)}", False, 4)
|
520
|
-
return False, False
|
446
|
+
return False, False, False
|
521
447
|
|
522
448
|
def wait_for_setup_completion(self, instance_id: str) -> bool:
|
523
449
|
"""Wait for instance setup to complete.
|
@@ -526,25 +452,66 @@ Host cloudx-{cloudx_env}-{hostname}
|
|
526
452
|
instance_id: EC2 instance ID
|
527
453
|
|
528
454
|
Returns:
|
529
|
-
bool: True if setup completed successfully
|
455
|
+
bool: True if setup completed successfully or user chose to continue
|
530
456
|
"""
|
531
457
|
self.print_header("Instance Setup Check")
|
532
|
-
self.print_status(f"Checking instance {instance_id}
|
458
|
+
self.print_status(f"Checking instance {instance_id} status...")
|
533
459
|
|
534
|
-
is_running, is_complete = self.check_instance_setup(instance_id)
|
460
|
+
ssm_accessible, is_running, is_complete = self.check_instance_setup(instance_id)
|
535
461
|
|
536
|
-
if not is_running:
|
537
|
-
self.print_status("Instance is not running or not accessible via SSM", False, 2)
|
462
|
+
if not ssm_accessible and not is_running:
|
538
463
|
continue_setup = self.prompt("Would you like to continue anyway?", "Y").lower() != 'n'
|
539
464
|
if continue_setup:
|
540
465
|
self.print_status("Continuing setup despite instance access issues", None, 2)
|
541
466
|
return True
|
542
467
|
return False
|
543
468
|
|
469
|
+
if not is_running:
|
470
|
+
start_instance = self.prompt("Would you like to start the instance?", "Y").lower() != 'n'
|
471
|
+
if not start_instance:
|
472
|
+
return False
|
473
|
+
|
474
|
+
try:
|
475
|
+
session = boto3.Session(profile_name=self.profile)
|
476
|
+
ec2 = session.client('ec2')
|
477
|
+
ec2.start_instances(InstanceIds=[instance_id])
|
478
|
+
self.print_status("Instance start requested. This may take a few minutes...", None, 2)
|
479
|
+
|
480
|
+
# Wait for instance to start
|
481
|
+
for _ in range(30): # 5 minute timeout
|
482
|
+
time.sleep(10)
|
483
|
+
ssm_accessible, is_running, is_complete = self.check_instance_setup(instance_id)
|
484
|
+
if is_running:
|
485
|
+
break
|
486
|
+
|
487
|
+
if not is_running:
|
488
|
+
self.print_status("Timeout waiting for instance to start", False, 2)
|
489
|
+
return False
|
490
|
+
except Exception as e:
|
491
|
+
self.print_status(f"Error starting instance: {e}", False, 2)
|
492
|
+
return False
|
493
|
+
|
544
494
|
if is_complete:
|
545
495
|
self.print_status("Instance setup is complete", True, 2)
|
546
496
|
return True
|
547
497
|
|
498
|
+
if not ssm_accessible:
|
499
|
+
self.print_status("Waiting for SSM access...", None, 2)
|
500
|
+
# Wait for SSM access
|
501
|
+
for _ in range(30): # 5 minute timeout
|
502
|
+
time.sleep(10)
|
503
|
+
ssm_accessible, is_running, is_complete = self.check_instance_setup(instance_id)
|
504
|
+
if ssm_accessible or not is_running:
|
505
|
+
break
|
506
|
+
|
507
|
+
if not ssm_accessible:
|
508
|
+
self.print_status("Timeout waiting for SSM access", False, 2)
|
509
|
+
continue_setup = self.prompt("Would you like to continue anyway?", "Y").lower() != 'n'
|
510
|
+
if continue_setup:
|
511
|
+
self.print_status("Continuing setup despite SSM access issues", None, 2)
|
512
|
+
return True
|
513
|
+
return False
|
514
|
+
|
548
515
|
wait = self.prompt("Instance setup is not complete. Would you like to wait?", "Y").lower() != 'n'
|
549
516
|
if not wait:
|
550
517
|
self.print_status("Skipping instance setup check", None, 2)
|
@@ -553,16 +520,24 @@ Host cloudx-{cloudx_env}-{hostname}
|
|
553
520
|
self.print_status("Waiting for setup to complete...", None, 2)
|
554
521
|
dots = 0
|
555
522
|
while True:
|
556
|
-
is_running, is_complete = self.check_instance_setup(instance_id)
|
523
|
+
ssm_accessible, is_running, is_complete = self.check_instance_setup(instance_id)
|
557
524
|
|
558
525
|
if not is_running:
|
559
|
-
self.print_status("Instance is no longer running
|
526
|
+
self.print_status("Instance is no longer running", False, 2)
|
560
527
|
continue_setup = self.prompt("Would you like to continue anyway?", "Y").lower() != 'n'
|
561
528
|
if continue_setup:
|
562
529
|
self.print_status("Continuing setup despite instance issues", None, 2)
|
563
530
|
return True
|
564
531
|
return False
|
565
532
|
|
533
|
+
if not ssm_accessible:
|
534
|
+
self.print_status("Lost SSM access to instance", False, 2)
|
535
|
+
continue_setup = self.prompt("Would you like to continue anyway?", "Y").lower() != 'n'
|
536
|
+
if continue_setup:
|
537
|
+
self.print_status("Continuing setup despite SSM access issues", None, 2)
|
538
|
+
return True
|
539
|
+
return False
|
540
|
+
|
566
541
|
if is_complete:
|
567
542
|
self.print_status("Instance setup completed successfully", True, 2)
|
568
543
|
return True
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|