cloudx-proxy 0.3.14__tar.gz → 0.4.0__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.14 → cloudx_proxy-0.4.0}/CHANGELOG.md +15 -0
- {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/PKG-INFO +1 -1
- {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/cloudx_proxy/_version.py +2 -2
- {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/cloudx_proxy/cli.py +15 -3
- {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/cloudx_proxy/core.py +23 -5
- {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/cloudx_proxy/setup.py +334 -77
- {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/cloudx_proxy.egg-info/PKG-INFO +1 -1
- {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/.github/workflows/release.yml +0 -0
- {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/.gitignore +0 -0
- {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/.releaserc +0 -0
- {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/CONTRIBUTING.md +0 -0
- {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/LICENSE +0 -0
- {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/README.md +0 -0
- {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/cloudx_proxy/__init__.py +0 -0
- {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/cloudx_proxy.egg-info/SOURCES.txt +0 -0
- {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/cloudx_proxy.egg-info/dependency_links.txt +0 -0
- {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/cloudx_proxy.egg-info/entry_points.txt +0 -0
- {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/cloudx_proxy.egg-info/requires.txt +0 -0
- {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/cloudx_proxy.egg-info/top_level.txt +0 -0
- {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/package.json +0 -0
- {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/pyproject.toml +0 -0
- {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/setup.cfg +0 -0
@@ -1,3 +1,18 @@
|
|
1
|
+
# [0.4.0](https://github.com/easytocloud/cloudX-proxy/compare/v0.3.15...v0.4.0) (2025-03-06)
|
2
|
+
|
3
|
+
|
4
|
+
### Features
|
5
|
+
|
6
|
+
* added 1password integration ([4ac2340](https://github.com/easytocloud/cloudX-proxy/commit/4ac2340d6174ded129482e0fcd91a9cef0ab136d))
|
7
|
+
|
8
|
+
## [0.3.15](https://github.com/easytocloud/cloudX-proxy/compare/v0.3.14...v0.3.15) (2025-03-06)
|
9
|
+
|
10
|
+
|
11
|
+
### Bug Fixes
|
12
|
+
|
13
|
+
* added --ssh-config ([893d919](https://github.com/easytocloud/cloudX-proxy/commit/893d919f7ef30dc5fd41a06b2c032d0035180e80))
|
14
|
+
* added --ssh-config also in connect ([75b7b3b](https://github.com/easytocloud/cloudX-proxy/commit/75b7b3b5ecac5f1a1012ce9d4905bc5aed444915))
|
15
|
+
|
1
16
|
## [0.3.14](https://github.com/easytocloud/cloudX-proxy/compare/v0.3.13...v0.3.14) (2025-03-06)
|
2
17
|
|
3
18
|
|
@@ -17,8 +17,9 @@ def cli():
|
|
17
17
|
@click.option('--profile', default='vscode', help='AWS profile to use (default: vscode)')
|
18
18
|
@click.option('--region', help='AWS region (default: from profile, or eu-west-1 if not set)')
|
19
19
|
@click.option('--ssh-key', default='vscode', help='SSH key name to use (default: vscode)')
|
20
|
+
@click.option('--ssh-config', help='SSH config file to use (default: ~/.ssh/vscode/config)')
|
20
21
|
@click.option('--aws-env', help='AWS environment directory (default: ~/.aws, use name of directory in ~/.aws/aws-envs/)')
|
21
|
-
def connect(instance_id: str, port: int, profile: str, region: str, ssh_key: str, aws_env: str):
|
22
|
+
def connect(instance_id: str, port: int, profile: str, region: str, ssh_key: str, ssh_config: str, aws_env: str):
|
22
23
|
"""Connect to an EC2 instance via SSM.
|
23
24
|
|
24
25
|
INSTANCE_ID is the EC2 instance ID to connect to (e.g., i-0123456789abcdef0)
|
@@ -35,6 +36,7 @@ def connect(instance_id: str, port: int, profile: str, region: str, ssh_key: str
|
|
35
36
|
profile=profile,
|
36
37
|
region=region,
|
37
38
|
ssh_key=ssh_key,
|
39
|
+
ssh_config=ssh_config,
|
38
40
|
aws_env=aws_env
|
39
41
|
)
|
40
42
|
|
@@ -48,8 +50,10 @@ def connect(instance_id: str, port: int, profile: str, region: str, ssh_key: str
|
|
48
50
|
@cli.command()
|
49
51
|
@click.option('--profile', default='vscode', help='AWS profile to use (default: vscode)')
|
50
52
|
@click.option('--ssh-key', default='vscode', help='SSH key name to use (default: vscode)')
|
53
|
+
@click.option('--ssh-config', help='SSH config file to use (default: ~/.ssh/vscode/config)')
|
51
54
|
@click.option('--aws-env', help='AWS environment directory (default: ~/.aws, use name of directory in ~/.aws/aws-envs/)')
|
52
|
-
|
55
|
+
@click.option('--1password', 'use_1password', is_flag=True, help='Use 1Password SSH agent for SSH authentication')
|
56
|
+
def setup(profile: str, ssh_key: str, ssh_config: str, aws_env: str, use_1password: bool):
|
53
57
|
"""Set up AWS profile, SSH keys, and configuration for CloudX.
|
54
58
|
|
55
59
|
This command will:
|
@@ -61,9 +65,17 @@ def setup(profile: str, ssh_key: str, aws_env: str):
|
|
61
65
|
Example usage:
|
62
66
|
cloudx-proxy setup
|
63
67
|
cloudx-proxy setup --profile myprofile --ssh-key mykey
|
68
|
+
cloudx-proxy setup --ssh-config ~/.ssh/cloudx/config
|
69
|
+
cloudx-proxy setup --1password
|
64
70
|
"""
|
65
71
|
try:
|
66
|
-
setup = CloudXSetup(
|
72
|
+
setup = CloudXSetup(
|
73
|
+
profile=profile,
|
74
|
+
ssh_key=ssh_key,
|
75
|
+
ssh_config=ssh_config,
|
76
|
+
aws_env=aws_env,
|
77
|
+
use_1password=use_1password
|
78
|
+
)
|
67
79
|
|
68
80
|
print("\n\033[1;95m=== cloudx-proxy Setup ===\033[0m\n")
|
69
81
|
|
@@ -7,7 +7,7 @@ from botocore.exceptions import ClientError
|
|
7
7
|
|
8
8
|
class CloudXProxy:
|
9
9
|
def __init__(self, instance_id: str, port: int = 22, profile: str = "vscode",
|
10
|
-
region: str = None, ssh_key: str = "vscode", aws_env: str = None):
|
10
|
+
region: str = None, ssh_key: str = "vscode", ssh_config: str = None, aws_env: str = None):
|
11
11
|
"""Initialize CloudX client for SSH tunneling via AWS SSM.
|
12
12
|
|
13
13
|
Args:
|
@@ -39,8 +39,14 @@ class CloudXProxy:
|
|
39
39
|
self.ec2 = self.session.client('ec2')
|
40
40
|
self.ec2_connect = self.session.client('ec2-instance-connect')
|
41
41
|
|
42
|
-
# Set up SSH key
|
43
|
-
|
42
|
+
# Set up SSH configuration and key paths
|
43
|
+
if ssh_config:
|
44
|
+
self.ssh_config_file = os.path.expanduser(ssh_config)
|
45
|
+
self.ssh_dir = os.path.dirname(self.ssh_config_file)
|
46
|
+
else:
|
47
|
+
self.ssh_dir = os.path.expanduser("~/.ssh/vscode")
|
48
|
+
self.ssh_config_file = os.path.join(self.ssh_dir, "config")
|
49
|
+
|
44
50
|
self.ssh_key = os.path.join(self.ssh_dir, f"{ssh_key}.pub")
|
45
51
|
|
46
52
|
def log(self, message: str) -> None:
|
@@ -85,9 +91,21 @@ class CloudXProxy:
|
|
85
91
|
return False
|
86
92
|
|
87
93
|
def push_ssh_key(self) -> bool:
|
88
|
-
"""Push SSH public key to instance via EC2 Instance Connect.
|
94
|
+
"""Push SSH public key to instance via EC2 Instance Connect.
|
95
|
+
|
96
|
+
Determines which SSH key to use (regular key or 1Password-managed key),
|
97
|
+
then pushes the correct public key to the instance.
|
98
|
+
"""
|
89
99
|
try:
|
90
|
-
with
|
100
|
+
# Check if file exists with .pub extension (could be a non-1Password key)
|
101
|
+
# or if the .pub extension is already part of self.ssh_key (because of 1Password integration)
|
102
|
+
key_path = self.ssh_key
|
103
|
+
if not key_path.endswith('.pub'):
|
104
|
+
key_path += '.pub'
|
105
|
+
|
106
|
+
self.log(f"Using public key: {key_path}")
|
107
|
+
|
108
|
+
with open(key_path) as f:
|
91
109
|
public_key = f.read()
|
92
110
|
|
93
111
|
self.ec2_connect.send_ssh_public_key(
|
@@ -9,20 +9,32 @@ import boto3
|
|
9
9
|
from botocore.exceptions import ClientError
|
10
10
|
|
11
11
|
class CloudXSetup:
|
12
|
-
def __init__(self, profile: str = "vscode", ssh_key: str = "vscode",
|
12
|
+
def __init__(self, profile: str = "vscode", ssh_key: str = "vscode", ssh_config: str = None,
|
13
|
+
aws_env: str = None, use_1password: bool = False):
|
13
14
|
"""Initialize cloudx-proxy setup.
|
14
15
|
|
15
16
|
Args:
|
16
17
|
profile: AWS profile name (default: "vscode")
|
17
18
|
ssh_key: SSH key name (default: "vscode")
|
19
|
+
ssh_config: SSH config file path (default: None, uses ~/.ssh/vscode/config)
|
18
20
|
aws_env: AWS environment directory (default: None)
|
21
|
+
use_1password: Use 1Password SSH agent for authentication (default: False)
|
19
22
|
"""
|
20
23
|
self.profile = profile
|
21
24
|
self.ssh_key = ssh_key
|
22
25
|
self.aws_env = aws_env
|
26
|
+
self.use_1password = use_1password
|
23
27
|
self.home_dir = str(Path.home())
|
24
|
-
self.
|
25
|
-
|
28
|
+
self.onepassword_agent_sock = Path(self.home_dir) / ".1password" / "agent.sock"
|
29
|
+
|
30
|
+
# Set up ssh config paths based on provided config or default
|
31
|
+
if ssh_config:
|
32
|
+
self.ssh_config_file = Path(os.path.expanduser(ssh_config))
|
33
|
+
self.ssh_dir = self.ssh_config_file.parent
|
34
|
+
else:
|
35
|
+
self.ssh_dir = Path(self.home_dir) / ".ssh" / "vscode"
|
36
|
+
self.ssh_config_file = self.ssh_dir / "config"
|
37
|
+
|
26
38
|
self.ssh_key_file = self.ssh_dir / f"{ssh_key}"
|
27
39
|
self.default_env = None
|
28
40
|
|
@@ -143,6 +155,171 @@ class CloudXSetup:
|
|
143
155
|
self.print_status(f"\033[1;91mError:\033[0m {str(e)}", False, 2)
|
144
156
|
return False
|
145
157
|
|
158
|
+
def _check_1password_availability(self) -> bool:
|
159
|
+
"""Check if 1Password CLI and SSH agent are available.
|
160
|
+
|
161
|
+
Returns:
|
162
|
+
bool: True if 1Password is available and configured
|
163
|
+
"""
|
164
|
+
if not self.use_1password:
|
165
|
+
return False
|
166
|
+
|
167
|
+
self.print_status("Checking 1Password availability...")
|
168
|
+
|
169
|
+
# Check if 1Password CLI is installed
|
170
|
+
try:
|
171
|
+
result = subprocess.run(
|
172
|
+
['op', '--version'],
|
173
|
+
capture_output=True,
|
174
|
+
text=True,
|
175
|
+
check=False
|
176
|
+
)
|
177
|
+
if result.returncode != 0:
|
178
|
+
self.print_status("1Password CLI not found. Please install it from https://1password.com/downloads/command-line/", False, 2)
|
179
|
+
return False
|
180
|
+
|
181
|
+
self.print_status(f"1Password CLI {result.stdout.strip()} installed", True, 2)
|
182
|
+
|
183
|
+
# Check if 1Password SSH agent is running
|
184
|
+
agent_sock_exists = self.onepassword_agent_sock.exists()
|
185
|
+
if not agent_sock_exists:
|
186
|
+
self.print_status("1Password SSH agent socket not found at ~/.1password/agent.sock", False, 2)
|
187
|
+
self.print_status("Please ensure 1Password SSH agent is enabled in 1Password settings", None, 2)
|
188
|
+
return False
|
189
|
+
|
190
|
+
self.print_status("1Password SSH agent socket found", True, 2)
|
191
|
+
|
192
|
+
# Check if agent is active by trying to list identities
|
193
|
+
result = subprocess.run(
|
194
|
+
['ssh-add', '-l'],
|
195
|
+
env={**os.environ, 'SSH_AUTH_SOCK': str(self.onepassword_agent_sock)},
|
196
|
+
capture_output=True,
|
197
|
+
text=True,
|
198
|
+
check=False
|
199
|
+
)
|
200
|
+
|
201
|
+
if "Could not open a connection to your authentication agent" in result.stderr:
|
202
|
+
self.print_status("1Password SSH agent is not running", False, 2)
|
203
|
+
return False
|
204
|
+
|
205
|
+
self.print_status("1Password SSH agent is running", True, 2)
|
206
|
+
|
207
|
+
# Check if 1Password CLI is authenticated
|
208
|
+
result = subprocess.run(
|
209
|
+
['op', 'account', 'list'],
|
210
|
+
capture_output=True,
|
211
|
+
text=True,
|
212
|
+
check=False
|
213
|
+
)
|
214
|
+
|
215
|
+
if result.returncode != 0:
|
216
|
+
self.print_status("1Password CLI is not authenticated. Run 'op signin' first.", False, 2)
|
217
|
+
return False
|
218
|
+
|
219
|
+
self.print_status("1Password CLI is authenticated", True, 2)
|
220
|
+
return True
|
221
|
+
|
222
|
+
except FileNotFoundError:
|
223
|
+
self.print_status("1Password CLI not found. Please install it from https://1password.com/downloads/command-line/", False, 2)
|
224
|
+
return False
|
225
|
+
except Exception as e:
|
226
|
+
self.print_status(f"Error checking 1Password: {str(e)}", False, 2)
|
227
|
+
return False
|
228
|
+
|
229
|
+
def _store_key_in_1password(self) -> bool:
|
230
|
+
"""Store the SSH key in 1Password and offer to remove it from the filesystem.
|
231
|
+
|
232
|
+
Returns:
|
233
|
+
bool: True if successful
|
234
|
+
"""
|
235
|
+
try:
|
236
|
+
# If key exists on filesystem, offer to store it in 1Password
|
237
|
+
key_exists = self.ssh_key_file.exists() and (self.ssh_key_file.with_suffix('.pub')).exists()
|
238
|
+
|
239
|
+
if not key_exists:
|
240
|
+
self.print_status("No SSH key found to store in 1Password", False, 2)
|
241
|
+
return False
|
242
|
+
|
243
|
+
# Ask if user wants to store the key in 1Password
|
244
|
+
store_in_1password = self.prompt("Would you like to store this SSH key in 1Password?", "Y").lower() == "y"
|
245
|
+
if not store_in_1password:
|
246
|
+
return True
|
247
|
+
|
248
|
+
# Get vault to store the key in
|
249
|
+
result = subprocess.run(
|
250
|
+
['op', 'vault', 'list', '--format=json'],
|
251
|
+
capture_output=True,
|
252
|
+
text=True,
|
253
|
+
check=True
|
254
|
+
)
|
255
|
+
|
256
|
+
if result.returncode != 0:
|
257
|
+
self.print_status("Error listing 1Password vaults", False, 2)
|
258
|
+
return False
|
259
|
+
|
260
|
+
vaults = json.loads(result.stdout)
|
261
|
+
if not vaults:
|
262
|
+
self.print_status("No 1Password vaults found", False, 2)
|
263
|
+
return False
|
264
|
+
|
265
|
+
# Display available vaults
|
266
|
+
print("\n\033[96mAvailable 1Password vaults:\033[0m")
|
267
|
+
for i, vault in enumerate(vaults):
|
268
|
+
print(f" {i+1}. {vault['name']}")
|
269
|
+
|
270
|
+
# Let user select vault
|
271
|
+
vault_num = self.prompt("Select vault number to store SSH key", "1")
|
272
|
+
try:
|
273
|
+
vault_idx = int(vault_num) - 1
|
274
|
+
if vault_idx < 0 or vault_idx >= len(vaults):
|
275
|
+
self.print_status("Invalid vault number", False, 2)
|
276
|
+
return False
|
277
|
+
selected_vault = vaults[vault_idx]['id']
|
278
|
+
except ValueError:
|
279
|
+
self.print_status("Invalid input", False, 2)
|
280
|
+
return False
|
281
|
+
|
282
|
+
# Read private key
|
283
|
+
private_key = self.ssh_key_file.read_text()
|
284
|
+
|
285
|
+
# Create a title for the 1Password item
|
286
|
+
ssh_key_title = f"cloudX SSH Key - {self.ssh_key}"
|
287
|
+
|
288
|
+
# Create item in 1Password
|
289
|
+
# Format: op item create --category=sshkey --title="..." --vault="..." "privateKey=<private key>"
|
290
|
+
result = subprocess.run(
|
291
|
+
[
|
292
|
+
'op', 'item', 'create',
|
293
|
+
'--category=sshkey',
|
294
|
+
f'--title={ssh_key_title}',
|
295
|
+
f'--vault={selected_vault}',
|
296
|
+
f'privateKey={private_key}'
|
297
|
+
],
|
298
|
+
capture_output=True,
|
299
|
+
text=True,
|
300
|
+
check=False
|
301
|
+
)
|
302
|
+
|
303
|
+
if result.returncode != 0:
|
304
|
+
self.print_status(f"Error storing SSH key in 1Password: {result.stderr}", False, 2)
|
305
|
+
return False
|
306
|
+
|
307
|
+
self.print_status("SSH key stored in 1Password successfully", True, 2)
|
308
|
+
|
309
|
+
# Ask if user wants to remove private key from filesystem
|
310
|
+
remove_private_key = self.prompt("Would you like to remove the private key from the filesystem? (for security)", "Y").lower() == "y"
|
311
|
+
if remove_private_key:
|
312
|
+
# We should only remove the private key, keep the public key
|
313
|
+
self.ssh_key_file.unlink()
|
314
|
+
self.print_status("Private key removed from filesystem", True, 2)
|
315
|
+
self.print_status("\033[93mImportant: If you need to recover this key, it's now only available in 1Password\033[0m", None, 2)
|
316
|
+
|
317
|
+
return True
|
318
|
+
|
319
|
+
except Exception as e:
|
320
|
+
self.print_status(f"Error storing key in 1Password: {str(e)}", False, 2)
|
321
|
+
return False
|
322
|
+
|
146
323
|
def setup_ssh_key(self) -> bool:
|
147
324
|
"""Set up SSH key pair.
|
148
325
|
|
@@ -150,6 +327,34 @@ class CloudXSetup:
|
|
150
327
|
bool: True if key was set up successfully
|
151
328
|
"""
|
152
329
|
self.print_header("SSH Key Configuration")
|
330
|
+
|
331
|
+
# Check 1Password integration if requested
|
332
|
+
if self.use_1password:
|
333
|
+
op_available = self._check_1password_availability()
|
334
|
+
if op_available:
|
335
|
+
self.print_status("Using 1Password SSH agent for authentication", True, 2)
|
336
|
+
|
337
|
+
# Generate a key if it doesn't exist
|
338
|
+
key_exists = self.ssh_key_file.exists() and (self.ssh_key_file.with_suffix('.pub')).exists()
|
339
|
+
if not key_exists:
|
340
|
+
self.print_status(f"No SSH key found for '{self.ssh_key}'", None, 2)
|
341
|
+
create_key = self.prompt("Would you like to create a new SSH key?", "Y").lower() == "y"
|
342
|
+
if create_key:
|
343
|
+
# We'll continue to the standard key creation flow
|
344
|
+
pass
|
345
|
+
else:
|
346
|
+
# User doesn't want a key, so we'll skip and just rely on 1Password
|
347
|
+
return True
|
348
|
+
else:
|
349
|
+
# Key exists, offer to store it in 1Password
|
350
|
+
self._store_key_in_1password()
|
351
|
+
return True
|
352
|
+
else:
|
353
|
+
proceed = self.prompt("1Password integration not available. Continue with standard SSH key setup?", "Y").lower() != "n"
|
354
|
+
if not proceed:
|
355
|
+
return False
|
356
|
+
self.use_1password = False # Fallback to standard setup
|
357
|
+
|
153
358
|
self.print_status(f"Checking SSH key '{self.ssh_key}' configuration...")
|
154
359
|
|
155
360
|
try:
|
@@ -188,6 +393,14 @@ class CloudXSetup:
|
|
188
393
|
self.ssh_key_file.with_suffix('.pub').chmod(stat.S_IRUSR | stat.S_IWUSR | stat.S_IROTH | stat.S_IRGRP) # 644 permissions
|
189
394
|
self.print_status("Set key file permissions", True, 2)
|
190
395
|
|
396
|
+
# For both standard setup and 1Password when a key needs to be created,
|
397
|
+
# we'll use the same code to generate the key
|
398
|
+
self.print_status("Key generated successfully", True, 2)
|
399
|
+
|
400
|
+
# If using 1Password, offer to store the key there
|
401
|
+
if self.use_1password:
|
402
|
+
self._store_key_in_1password()
|
403
|
+
|
191
404
|
return True
|
192
405
|
|
193
406
|
except Exception as e:
|
@@ -198,6 +411,75 @@ class CloudXSetup:
|
|
198
411
|
return True
|
199
412
|
return False
|
200
413
|
|
414
|
+
def _build_proxy_command(self) -> str:
|
415
|
+
"""Build the ProxyCommand with appropriate parameters.
|
416
|
+
|
417
|
+
Returns:
|
418
|
+
str: The complete ProxyCommand string
|
419
|
+
"""
|
420
|
+
proxy_command = "uvx cloudx-proxy connect %h %p"
|
421
|
+
if self.profile != "vscode":
|
422
|
+
proxy_command += f" --profile {self.profile}"
|
423
|
+
if self.aws_env:
|
424
|
+
proxy_command += f" --aws-env {self.aws_env}"
|
425
|
+
if self.ssh_key != "vscode":
|
426
|
+
proxy_command += f" --ssh-key {self.ssh_key}"
|
427
|
+
|
428
|
+
return proxy_command
|
429
|
+
|
430
|
+
def _build_auth_config(self) -> str:
|
431
|
+
"""Build the authentication configuration block.
|
432
|
+
|
433
|
+
Returns:
|
434
|
+
str: SSH config authentication section
|
435
|
+
"""
|
436
|
+
if self.use_1password:
|
437
|
+
# When using 1Password:
|
438
|
+
# 1. Set IdentityAgent to the 1Password socket
|
439
|
+
# 2. Set IdentityFile to the PUBLIC key (.pub) to limit key search
|
440
|
+
# 3. Set IdentitiesOnly to yes to avoid using ssh-agent keys
|
441
|
+
return f""" IdentityAgent {self.onepassword_agent_sock}
|
442
|
+
IdentityFile {self.ssh_key_file}.pub
|
443
|
+
IdentitiesOnly yes
|
444
|
+
"""
|
445
|
+
else:
|
446
|
+
# Standard SSH key configuration
|
447
|
+
return f""" IdentityFile {self.ssh_key_file}
|
448
|
+
IdentitiesOnly yes
|
449
|
+
"""
|
450
|
+
|
451
|
+
def _build_host_config(self, cloudx_env: str, hostname: str, instance_id: str, include_proxy: bool = True) -> str:
|
452
|
+
"""Build a host configuration block.
|
453
|
+
|
454
|
+
Args:
|
455
|
+
cloudx_env: CloudX environment
|
456
|
+
hostname: Hostname for the instance
|
457
|
+
instance_id: EC2 instance ID (None for wildcard entries)
|
458
|
+
include_proxy: Whether to include the ProxyCommand (default: True)
|
459
|
+
|
460
|
+
Returns:
|
461
|
+
str: Complete host configuration block
|
462
|
+
"""
|
463
|
+
host_pattern = hostname if hostname else "*"
|
464
|
+
host_entry = f"""
|
465
|
+
Host cloudx-{cloudx_env}-{host_pattern}
|
466
|
+
"""
|
467
|
+
# Add HostName only for specific hosts, not for wildcard entries
|
468
|
+
if instance_id:
|
469
|
+
host_entry += f""" HostName {instance_id}
|
470
|
+
"""
|
471
|
+
host_entry += """ User ec2-user
|
472
|
+
"""
|
473
|
+
# Add authentication configuration
|
474
|
+
host_entry += self._build_auth_config()
|
475
|
+
|
476
|
+
# Add proxy command if requested
|
477
|
+
if include_proxy:
|
478
|
+
host_entry += f""" ProxyCommand {self._build_proxy_command()}
|
479
|
+
"""
|
480
|
+
|
481
|
+
return host_entry
|
482
|
+
|
201
483
|
def _add_host_entry(self, cloudx_env: str, instance_id: str, hostname: str, current_config: str) -> bool:
|
202
484
|
"""Add settings to a specific host entry.
|
203
485
|
|
@@ -211,25 +493,8 @@ class CloudXSetup:
|
|
211
493
|
bool: True if settings were added successfully
|
212
494
|
"""
|
213
495
|
try:
|
214
|
-
#
|
215
|
-
|
216
|
-
if self.profile != "vscode":
|
217
|
-
proxy_command += f" --profile {self.profile}"
|
218
|
-
if self.aws_env:
|
219
|
-
proxy_command += f" --aws-env {self.aws_env}"
|
220
|
-
if self.ssh_key != "vscode":
|
221
|
-
proxy_command += f" --ssh-key {self.ssh_key}"
|
222
|
-
|
223
|
-
host_entry = f"""
|
224
|
-
Host cloudx-{cloudx_env}-{hostname}
|
225
|
-
HostName {instance_id}
|
226
|
-
User ec2-user
|
227
|
-
"""
|
228
|
-
host_entry += f""" IdentityFile {self.ssh_key_file}
|
229
|
-
IdentitiesOnly yes
|
230
|
-
"""
|
231
|
-
host_entry += f""" ProxyCommand {proxy_command}
|
232
|
-
"""
|
496
|
+
# Generate the host entry using the consolidated helper method
|
497
|
+
host_entry = self._build_host_config(cloudx_env, hostname, instance_id)
|
233
498
|
|
234
499
|
# Append host entry
|
235
500
|
with open(self.ssh_config_file, 'a') as f:
|
@@ -330,8 +595,10 @@ Host cloudx-{cloudx_env}-{hostname}
|
|
330
595
|
if f"Host cloudx-{cloudx_env}-*" in current_config:
|
331
596
|
self.print_status(f"Found existing config for cloudx-{cloudx_env}-*", True, 2)
|
332
597
|
choice = self.prompt(
|
333
|
-
"Would you like to \n
|
334
|
-
"
|
598
|
+
"Would you like to \n"
|
599
|
+
" 1: override the existing config\n"
|
600
|
+
" 2: add settings to the specific host entry?\n"
|
601
|
+
"Select an option",
|
335
602
|
"1"
|
336
603
|
)
|
337
604
|
if choice == "2":
|
@@ -357,29 +624,18 @@ Host cloudx-{cloudx_env}-{hostname}
|
|
357
624
|
|
358
625
|
# Create base config
|
359
626
|
self.print_status(f"Creating new config for cloudx-{cloudx_env}-*", None, 2)
|
360
|
-
|
361
|
-
proxy_command = "uvx cloudx-proxy connect %h %p"
|
362
|
-
if self.profile != "vscode":
|
363
|
-
proxy_command += f" --profile {self.profile}"
|
364
|
-
if self.aws_env:
|
365
|
-
proxy_command += f" --aws-env {self.aws_env}"
|
366
|
-
if self.ssh_key != "vscode":
|
367
|
-
proxy_command += f" --ssh-key {self.ssh_key}"
|
368
|
-
|
627
|
+
|
369
628
|
# Ensure control directory exists with proper permissions
|
370
629
|
if not self._ensure_control_dir():
|
371
630
|
return False
|
372
631
|
|
373
|
-
# Build base configuration
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
IdentitiesOnly yes
|
381
|
-
|
382
|
-
"""
|
632
|
+
# Build base configuration with wildcard hostname pattern
|
633
|
+
# Start with a header comment
|
634
|
+
base_config = "# cloudx-proxy SSH Configuration\n"
|
635
|
+
|
636
|
+
# Add base host pattern with wildcard
|
637
|
+
base_config += self._build_host_config(cloudx_env, None, None, include_proxy=True)
|
638
|
+
|
383
639
|
# Add SSH multiplexing configuration
|
384
640
|
control_path = "~/.ssh/control/%r@%h:%p"
|
385
641
|
if platform.system() == 'Windows':
|
@@ -391,9 +647,6 @@ Host cloudx-{cloudx_env}-*
|
|
391
647
|
ControlPath {control_path}
|
392
648
|
ControlPersist 4h
|
393
649
|
|
394
|
-
"""
|
395
|
-
# Add ProxyCommand
|
396
|
-
base_config += f""" ProxyCommand {proxy_command}
|
397
650
|
"""
|
398
651
|
|
399
652
|
# If file exists, append the new config, otherwise create it
|
@@ -410,17 +663,16 @@ Host cloudx-{cloudx_env}-*
|
|
410
663
|
self.ssh_config_file.chmod(stat.S_IRUSR | stat.S_IWUSR) # 600 permissions (owner read/write)
|
411
664
|
self.print_status("Set config file permissions to 600", True, 2)
|
412
665
|
|
413
|
-
# Add specific host entry
|
666
|
+
# Add specific host entry using the consolidated helper method
|
414
667
|
self.print_status(f"Adding host entry for cloudx-{cloudx_env}-{hostname}", None, 2)
|
415
|
-
host_entry =
|
416
|
-
Host cloudx-{cloudx_env}-{hostname}
|
417
|
-
HostName {instance_id}
|
418
|
-
"""
|
668
|
+
host_entry = self._build_host_config(cloudx_env, hostname, instance_id, include_proxy=False)
|
419
669
|
with open(self.ssh_config_file, 'a') as f:
|
420
670
|
f.write(host_entry)
|
421
671
|
self.print_status("Host entry added", True, 2)
|
422
672
|
|
423
|
-
#
|
673
|
+
# Handle system SSH config integration
|
674
|
+
system_config_path = Path(self.home_dir) / ".ssh" / "config"
|
675
|
+
|
424
676
|
# Ensure ~/.ssh directory has proper permissions
|
425
677
|
ssh_parent_dir = Path(self.home_dir) / ".ssh"
|
426
678
|
if not ssh_parent_dir.exists():
|
@@ -428,36 +680,41 @@ Host cloudx-{cloudx_env}-{hostname}
|
|
428
680
|
self.print_status(f"Created SSH directory: {ssh_parent_dir}", True, 2)
|
429
681
|
self._set_directory_permissions(ssh_parent_dir)
|
430
682
|
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
if main_config.exists():
|
435
|
-
content = main_config.read_text()
|
436
|
-
if include_line not in content:
|
437
|
-
with open(main_config, 'a') as f:
|
438
|
-
f.write(f"\n{include_line}")
|
439
|
-
self.print_status("Added include line to main SSH config", True, 2)
|
440
|
-
else:
|
441
|
-
self.print_status("Main SSH config already includes our config", True, 2)
|
442
|
-
|
443
|
-
# Set correct permissions on main config file
|
444
|
-
if platform.system() != 'Windows':
|
445
|
-
import stat
|
446
|
-
main_config.chmod(stat.S_IRUSR | stat.S_IWUSR) # 600 permissions
|
447
|
-
self.print_status("Set main config file permissions to 600", True, 2)
|
683
|
+
# If our config file is the system config, we're done
|
684
|
+
if self.ssh_config_file.samefile(system_config_path) if self.ssh_config_file.exists() and system_config_path.exists() else str(self.ssh_config_file) == str(system_config_path):
|
685
|
+
self.print_status("Using system SSH config directly, no Include needed", True, 2)
|
448
686
|
else:
|
449
|
-
|
450
|
-
|
687
|
+
# Otherwise, make sure the system config includes our config file
|
688
|
+
include_line = f"Include {self.ssh_config_file}\n"
|
451
689
|
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
690
|
+
if system_config_path.exists():
|
691
|
+
content = system_config_path.read_text()
|
692
|
+
if include_line not in content:
|
693
|
+
with open(system_config_path, 'a') as f:
|
694
|
+
f.write(f"\n{include_line}")
|
695
|
+
self.print_status("Added include line to system SSH config", True, 2)
|
696
|
+
else:
|
697
|
+
self.print_status("System SSH config already includes our config", True, 2)
|
698
|
+
|
699
|
+
# Set correct permissions on system config file
|
700
|
+
if platform.system() != 'Windows':
|
701
|
+
import stat
|
702
|
+
system_config_path.chmod(stat.S_IRUSR | stat.S_IWUSR) # 600 permissions
|
703
|
+
self.print_status("Set system config file permissions to 600", True, 2)
|
704
|
+
else:
|
705
|
+
system_config_path.write_text(include_line)
|
706
|
+
self.print_status("Created system SSH config with include line", True, 2)
|
707
|
+
|
708
|
+
# Set correct permissions on newly created system config file
|
709
|
+
if platform.system() != 'Windows':
|
710
|
+
import stat
|
711
|
+
system_config_path.chmod(stat.S_IRUSR | stat.S_IWUSR) # 600 permissions
|
712
|
+
self.print_status("Set system config file permissions to 600", True, 2)
|
457
713
|
|
458
714
|
self.print_status("SSH configuration summary:", None)
|
459
|
-
self.print_status(f"
|
715
|
+
self.print_status(f"System config: {system_config_path}", None, 2)
|
460
716
|
self.print_status(f"cloudx-proxy config: {self.ssh_config_file}", None, 2)
|
717
|
+
self.print_status(f"SSH key directory: {self.ssh_dir}", None, 2)
|
461
718
|
self.print_status(f"Connect using: ssh cloudx-{cloudx_env}-{hostname}", None, 2)
|
462
719
|
|
463
720
|
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
|