cloudx-proxy 0.1.1__tar.gz → 0.3.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.0/CHANGELOG.md +46 -0
- {cloudx_proxy-0.1.1 → cloudx_proxy-0.3.0}/PKG-INFO +1 -1
- {cloudx_proxy-0.1.1 → cloudx_proxy-0.3.0}/cloudx_proxy/_version.py +2 -2
- {cloudx_proxy-0.1.1 → cloudx_proxy-0.3.0}/cloudx_proxy/cli.py +10 -8
- cloudx_proxy-0.3.0/cloudx_proxy/setup.py +524 -0
- {cloudx_proxy-0.1.1 → cloudx_proxy-0.3.0}/cloudx_proxy.egg-info/PKG-INFO +1 -1
- cloudx_proxy-0.1.1/CHANGELOG.md +0 -21
- cloudx_proxy-0.1.1/cloudx_proxy/setup.py +0 -336
- {cloudx_proxy-0.1.1 → cloudx_proxy-0.3.0}/.github/workflows/release.yml +0 -0
- {cloudx_proxy-0.1.1 → cloudx_proxy-0.3.0}/.gitignore +0 -0
- {cloudx_proxy-0.1.1 → cloudx_proxy-0.3.0}/.releaserc +0 -0
- {cloudx_proxy-0.1.1 → cloudx_proxy-0.3.0}/CONTRIBUTING.md +0 -0
- {cloudx_proxy-0.1.1 → cloudx_proxy-0.3.0}/LICENSE +0 -0
- {cloudx_proxy-0.1.1 → cloudx_proxy-0.3.0}/README.md +0 -0
- {cloudx_proxy-0.1.1 → cloudx_proxy-0.3.0}/cloudx_proxy/__init__.py +0 -0
- {cloudx_proxy-0.1.1 → cloudx_proxy-0.3.0}/cloudx_proxy/core.py +0 -0
- {cloudx_proxy-0.1.1 → cloudx_proxy-0.3.0}/cloudx_proxy.egg-info/SOURCES.txt +0 -0
- {cloudx_proxy-0.1.1 → cloudx_proxy-0.3.0}/cloudx_proxy.egg-info/dependency_links.txt +0 -0
- {cloudx_proxy-0.1.1 → cloudx_proxy-0.3.0}/cloudx_proxy.egg-info/entry_points.txt +0 -0
- {cloudx_proxy-0.1.1 → cloudx_proxy-0.3.0}/cloudx_proxy.egg-info/requires.txt +0 -0
- {cloudx_proxy-0.1.1 → cloudx_proxy-0.3.0}/cloudx_proxy.egg-info/top_level.txt +0 -0
- {cloudx_proxy-0.1.1 → cloudx_proxy-0.3.0}/package.json +0 -0
- {cloudx_proxy-0.1.1 → cloudx_proxy-0.3.0}/pyproject.toml +0 -0
- {cloudx_proxy-0.1.1 → cloudx_proxy-0.3.0}/setup.cfg +0 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
# [0.3.0](https://github.com/easytocloud/cloudX-proxy/compare/v0.2.0...v0.3.0) (2025-02-09)
|
2
|
+
|
3
|
+
|
4
|
+
### Bug Fixes
|
5
|
+
|
6
|
+
* improve SSH key message and config header ([94dd9b6](https://github.com/easytocloud/cloudX-proxy/commit/94dd9b6bd42b2b23e2e732470adf0096aa98e0fb))
|
7
|
+
* improve UI formatting and progress tracking ([570a0de](https://github.com/easytocloud/cloudX-proxy/commit/570a0deab309f42ee8c961062596189f8d5d6a91))
|
8
|
+
* only include non-default parameters in ProxyCommand ([e1ecae9](https://github.com/easytocloud/cloudX-proxy/commit/e1ecae9fd91ae1bbed92d60ed384a0e405269a35))
|
9
|
+
* simplify setup UI and improve error handling ([613cba3](https://github.com/easytocloud/cloudX-proxy/commit/613cba3596c5631d7125c814f0f829c7171ff529))
|
10
|
+
* update branding to cloudx-proxy ([b354d84](https://github.com/easytocloud/cloudX-proxy/commit/b354d84d99005d11f51212ce70d40c0d36ea47dd))
|
11
|
+
|
12
|
+
|
13
|
+
### Features
|
14
|
+
|
15
|
+
* add status indicators to instance setup check ([dfb3624](https://github.com/easytocloud/cloudX-proxy/commit/dfb36240583b46a54742306f9eae24e592d65fbe))
|
16
|
+
* enhance setup UI with progress bar, colors, and summary ([f72efd1](https://github.com/easytocloud/cloudX-proxy/commit/f72efd175c7805cfd41605f33d5056e714911972))
|
17
|
+
* extract default env from IAM user and improve SSH config handling ([25fa9c9](https://github.com/easytocloud/cloudX-proxy/commit/25fa9c976d4ae992e5217680405cd407e613eac3))
|
18
|
+
|
19
|
+
# [0.2.0](https://github.com/easytocloud/cloudX-proxy/compare/v0.1.1...v0.2.0) (2025-02-09)
|
20
|
+
|
21
|
+
|
22
|
+
### Features
|
23
|
+
|
24
|
+
* add setup checklist and make all steps optional ([46016b8](https://github.com/easytocloud/cloudX-proxy/commit/46016b8fd7f1a1ae42fb34a7ff35365279883ab0))
|
25
|
+
|
26
|
+
## [0.1.1](https://github.com/easytocloud/cloudX-proxy/compare/v0.1.0...v0.1.1) (2025-02-09)
|
27
|
+
|
28
|
+
# Changelog
|
29
|
+
|
30
|
+
All notable changes to this project will be documented in this file.
|
31
|
+
|
32
|
+
## [0.1.0](https://github.com/easytocloud/cloudX-proxy/releases/tag/v0.1.0) (2025-02-09)
|
33
|
+
|
34
|
+
Initial release with core functionality:
|
35
|
+
|
36
|
+
### Features
|
37
|
+
|
38
|
+
* SSH proxy command for connecting VSCode to EC2 instances via SSM
|
39
|
+
* AWS profile configuration with cloudX-{env}-{user} format
|
40
|
+
* SSH key management with 1Password integration
|
41
|
+
* Environment-specific SSH config generation
|
42
|
+
* Instance setup status verification
|
43
|
+
* Cross-platform support (Windows, macOS, Linux)
|
44
|
+
* Automatic instance startup if stopped
|
45
|
+
* SSH key distribution via EC2 Instance Connect
|
46
|
+
* SSH tunneling through AWS Systems Manager
|
@@ -16,9 +16,9 @@ def cli():
|
|
16
16
|
@click.argument('port', type=int, default=22)
|
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
|
-
@click.option('--key
|
19
|
+
@click.option('--ssh-key', default='vscode', help='SSH key name to use (default: vscode)')
|
20
20
|
@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,
|
21
|
+
def connect(instance_id: str, port: int, profile: str, region: str, ssh_key: str, aws_env: str):
|
22
22
|
"""Connect to an EC2 instance via SSM.
|
23
23
|
|
24
24
|
INSTANCE_ID is the EC2 instance ID to connect to (e.g., i-0123456789abcdef0)
|
@@ -34,7 +34,7 @@ def connect(instance_id: str, port: int, profile: str, region: str, key_path: st
|
|
34
34
|
port=port,
|
35
35
|
profile=profile,
|
36
36
|
region=region,
|
37
|
-
|
37
|
+
ssh_key=ssh_key,
|
38
38
|
aws_env=aws_env
|
39
39
|
)
|
40
40
|
|
@@ -65,6 +65,8 @@ def setup(profile: str, ssh_key: str, aws_env: str):
|
|
65
65
|
try:
|
66
66
|
setup = CloudXSetup(profile=profile, ssh_key=ssh_key, aws_env=aws_env)
|
67
67
|
|
68
|
+
print("\n\033[1;95m=== cloudx-proxy Setup ===\033[0m\n")
|
69
|
+
|
68
70
|
# Set up AWS profile
|
69
71
|
if not setup.setup_aws_profile():
|
70
72
|
sys.exit(1)
|
@@ -73,10 +75,10 @@ def setup(profile: str, ssh_key: str, aws_env: str):
|
|
73
75
|
if not setup.setup_ssh_key():
|
74
76
|
sys.exit(1)
|
75
77
|
|
76
|
-
# Get
|
77
|
-
cloudx_env =
|
78
|
-
instance_id =
|
79
|
-
hostname =
|
78
|
+
# Get environment and instance details
|
79
|
+
cloudx_env = setup.prompt("Enter environment", getattr(setup, 'default_env', None))
|
80
|
+
instance_id = setup.prompt("Enter EC2 instance ID (e.g., i-0123456789abcdef0)")
|
81
|
+
hostname = setup.prompt("Enter hostname for the instance")
|
80
82
|
|
81
83
|
# Set up SSH config
|
82
84
|
if not setup.setup_ssh_config(cloudx_env, instance_id, hostname):
|
@@ -86,7 +88,7 @@ def setup(profile: str, ssh_key: str, aws_env: str):
|
|
86
88
|
setup.wait_for_setup_completion(instance_id)
|
87
89
|
|
88
90
|
except Exception as e:
|
89
|
-
print(f"
|
91
|
+
print(f"\n\033[91mError: {str(e)}\033[0m", file=sys.stderr)
|
90
92
|
sys.exit(1)
|
91
93
|
|
92
94
|
if __name__ == '__main__':
|
@@ -0,0 +1,524 @@
|
|
1
|
+
import os
|
2
|
+
import time
|
3
|
+
import json
|
4
|
+
import subprocess
|
5
|
+
from pathlib import Path
|
6
|
+
from typing import Optional, Tuple
|
7
|
+
import boto3
|
8
|
+
from botocore.exceptions import ClientError
|
9
|
+
|
10
|
+
class CloudXSetup:
|
11
|
+
def __init__(self, profile: str = "vscode", ssh_key: str = "vscode", aws_env: str = None):
|
12
|
+
"""Initialize cloudx-proxy setup.
|
13
|
+
|
14
|
+
Args:
|
15
|
+
profile: AWS profile name (default: "vscode")
|
16
|
+
ssh_key: SSH key name (default: "vscode")
|
17
|
+
aws_env: AWS environment directory (default: None)
|
18
|
+
"""
|
19
|
+
self.profile = profile
|
20
|
+
self.ssh_key = ssh_key
|
21
|
+
self.aws_env = aws_env
|
22
|
+
self.home_dir = str(Path.home())
|
23
|
+
self.ssh_dir = Path(self.home_dir) / ".ssh" / "vscode"
|
24
|
+
self.ssh_config_file = self.ssh_dir / "config"
|
25
|
+
self.ssh_key_file = self.ssh_dir / f"{ssh_key}"
|
26
|
+
self.using_1password = False
|
27
|
+
self.default_env = None
|
28
|
+
|
29
|
+
def print_header(self, text: str) -> None:
|
30
|
+
"""Print a section header.
|
31
|
+
|
32
|
+
Args:
|
33
|
+
text: The header text
|
34
|
+
"""
|
35
|
+
print(f"\n\n\033[1;94m=== {text} ===\033[0m")
|
36
|
+
|
37
|
+
def print_status(self, message: str, status: bool = None, indent: int = 0) -> None:
|
38
|
+
"""Print a status message with optional checkmark/cross.
|
39
|
+
|
40
|
+
Args:
|
41
|
+
message: The message to print
|
42
|
+
status: True for success (✓), False for failure (✗), None for no symbol
|
43
|
+
indent: Number of spaces to indent
|
44
|
+
"""
|
45
|
+
prefix = " " * indent
|
46
|
+
if status is not None:
|
47
|
+
symbol = "✓" if status else "✗"
|
48
|
+
color = "\033[92m" if status else "\033[91m" # Green for success, red for failure
|
49
|
+
reset = "\033[0m"
|
50
|
+
print(f"{prefix}{color}{symbol}{reset} {message}")
|
51
|
+
else:
|
52
|
+
print(f"{prefix}○ {message}")
|
53
|
+
|
54
|
+
def prompt(self, message: str, default: str = None) -> str:
|
55
|
+
"""Display a colored prompt for user input.
|
56
|
+
|
57
|
+
Args:
|
58
|
+
message: The prompt message
|
59
|
+
default: Default value (shown in brackets)
|
60
|
+
|
61
|
+
Returns:
|
62
|
+
str: User's input or default value
|
63
|
+
"""
|
64
|
+
if default:
|
65
|
+
prompt_text = f"\033[93m{message} [{default}]: \033[0m"
|
66
|
+
else:
|
67
|
+
prompt_text = f"\033[93m{message}: \033[0m"
|
68
|
+
response = input(prompt_text)
|
69
|
+
return response if response else default
|
70
|
+
|
71
|
+
def setup_aws_profile(self) -> bool:
|
72
|
+
"""Set up AWS profile using aws configure command.
|
73
|
+
|
74
|
+
Returns:
|
75
|
+
bool: True if profile was set up successfully or user chose to continue
|
76
|
+
"""
|
77
|
+
self.print_status("Checking AWS profile configuration...")
|
78
|
+
|
79
|
+
try:
|
80
|
+
# Configure AWS environment if specified
|
81
|
+
if self.aws_env:
|
82
|
+
aws_env_dir = os.path.expanduser(f"~/.aws/aws-envs/{self.aws_env}")
|
83
|
+
os.environ["AWS_CONFIG_FILE"] = os.path.join(aws_env_dir, "config")
|
84
|
+
os.environ["AWS_SHARED_CREDENTIALS_FILE"] = os.path.join(aws_env_dir, "credentials")
|
85
|
+
|
86
|
+
# Try to create session with profile
|
87
|
+
try:
|
88
|
+
session = boto3.Session(profile_name=self.profile)
|
89
|
+
except:
|
90
|
+
# Profile doesn't exist, create it
|
91
|
+
self.print_status(f"AWS profile '{self.profile}' not found", False, 2)
|
92
|
+
self.print_status("Setting up AWS profile...", None, 2)
|
93
|
+
print("\033[96mPlease enter your AWS credentials:\033[0m")
|
94
|
+
|
95
|
+
# Use aws configure command
|
96
|
+
subprocess.run([
|
97
|
+
'aws', 'configure',
|
98
|
+
'--profile', self.profile
|
99
|
+
], check=True)
|
100
|
+
|
101
|
+
# Create new session with configured profile
|
102
|
+
session = boto3.Session(profile_name=self.profile)
|
103
|
+
|
104
|
+
# Verify the profile works
|
105
|
+
try:
|
106
|
+
identity = session.client('sts').get_caller_identity()
|
107
|
+
user_arn = identity['Arn']
|
108
|
+
|
109
|
+
# Extract environment from IAM user name
|
110
|
+
user_parts = [part for part in user_arn.split('/') if part.startswith('cloudX-')]
|
111
|
+
if user_parts:
|
112
|
+
self.default_env = user_parts[0].split('-')[1] # Extract env from cloudX-{env}-{user}
|
113
|
+
self.print_status(f"AWS profile '{self.profile}' exists and matches cloudX format", True, 2)
|
114
|
+
return True
|
115
|
+
else:
|
116
|
+
self.print_status(f"AWS profile exists but doesn't match cloudX-{{env}}-{{user}} format", False, 2)
|
117
|
+
self.print_status("Please ensure your IAM user follows the format: cloudX-{env}-{username}", None, 2)
|
118
|
+
return False
|
119
|
+
except ClientError:
|
120
|
+
self.print_status("Invalid AWS credentials", False, 2)
|
121
|
+
return False
|
122
|
+
|
123
|
+
except Exception as e:
|
124
|
+
self.print_status(f"\033[1;91mError:\033[0m {str(e)}", False, 2)
|
125
|
+
return False
|
126
|
+
|
127
|
+
def setup_ssh_key(self) -> bool:
|
128
|
+
"""Set up SSH key pair.
|
129
|
+
|
130
|
+
Returns:
|
131
|
+
bool: True if key was set up successfully
|
132
|
+
"""
|
133
|
+
self.print_header("SSH Key Configuration")
|
134
|
+
self.print_status(f"Checking SSH key '{self.ssh_key}' configuration...")
|
135
|
+
|
136
|
+
try:
|
137
|
+
# Create .ssh/vscode directory if it doesn't exist
|
138
|
+
self.ssh_dir.mkdir(parents=True, exist_ok=True)
|
139
|
+
self.print_status("SSH directory exists", True, 2)
|
140
|
+
|
141
|
+
key_exists = self.ssh_key_file.exists() and (self.ssh_key_file.with_suffix('.pub')).exists()
|
142
|
+
|
143
|
+
if key_exists:
|
144
|
+
self.print_status(f"SSH key '{self.ssh_key}' exists", True, 2)
|
145
|
+
self.using_1password = self.prompt("Would you like to use 1Password SSH agent?", "N").lower() == 'y'
|
146
|
+
if self.using_1password:
|
147
|
+
self.print_status("Using 1Password SSH agent", True, 2)
|
148
|
+
else:
|
149
|
+
store_in_1password = self.prompt("Would you like to store the private key in 1Password?", "N").lower() == 'y'
|
150
|
+
if store_in_1password:
|
151
|
+
if self._store_key_in_1password():
|
152
|
+
self.print_status("Private key stored in 1Password", True, 2)
|
153
|
+
else:
|
154
|
+
self.print_status("Failed to store private key in 1Password", False, 2)
|
155
|
+
else:
|
156
|
+
self.print_status(f"Generating new SSH key '{self.ssh_key}'...", None, 2)
|
157
|
+
subprocess.run([
|
158
|
+
'ssh-keygen',
|
159
|
+
'-t', 'ed25519',
|
160
|
+
'-f', str(self.ssh_key_file),
|
161
|
+
'-N', '' # Empty passphrase
|
162
|
+
], check=True)
|
163
|
+
self.print_status("SSH key generated", True, 2)
|
164
|
+
|
165
|
+
self.using_1password = self.prompt("Would you like to use 1Password SSH agent?", "N").lower() == 'y'
|
166
|
+
if self.using_1password:
|
167
|
+
self.print_status("Using 1Password SSH agent", True, 2)
|
168
|
+
else:
|
169
|
+
store_in_1password = self.prompt("Would you like to store the private key in 1Password?", "N").lower() == 'y'
|
170
|
+
if store_in_1password:
|
171
|
+
if self._store_key_in_1password():
|
172
|
+
self.print_status("Private key stored in 1Password", True, 2)
|
173
|
+
else:
|
174
|
+
self.print_status("Failed to store private key in 1Password", False, 2)
|
175
|
+
|
176
|
+
return True
|
177
|
+
|
178
|
+
except Exception as e:
|
179
|
+
self.print_status(f"Error: {str(e)}", False, 2)
|
180
|
+
continue_setup = self.prompt("Would you like to continue anyway?", "Y").lower() != 'n'
|
181
|
+
if continue_setup:
|
182
|
+
self.print_status("Continuing setup despite SSH key issues", None, 2)
|
183
|
+
return True
|
184
|
+
return False
|
185
|
+
|
186
|
+
def _store_key_in_1password(self) -> bool:
|
187
|
+
"""Store SSH private key in 1Password.
|
188
|
+
|
189
|
+
Returns:
|
190
|
+
bool: True if key was stored successfully
|
191
|
+
"""
|
192
|
+
try:
|
193
|
+
subprocess.run(['op', '--version'], check=True, capture_output=True)
|
194
|
+
print("Storing private key in 1Password...")
|
195
|
+
subprocess.run([
|
196
|
+
'op', 'document', 'create',
|
197
|
+
str(self.ssh_key_file),
|
198
|
+
'--title', f'cloudx-proxy SSH Key - {self.ssh_key}'
|
199
|
+
], check=True)
|
200
|
+
return True
|
201
|
+
except subprocess.CalledProcessError:
|
202
|
+
print("Error: 1Password CLI not installed or not signed in.")
|
203
|
+
return False
|
204
|
+
|
205
|
+
def _add_host_entry(self, cloudx_env: str, instance_id: str, hostname: str, current_config: str) -> bool:
|
206
|
+
"""Add settings to a specific host entry.
|
207
|
+
|
208
|
+
Args:
|
209
|
+
cloudx_env: CloudX environment
|
210
|
+
instance_id: EC2 instance ID
|
211
|
+
hostname: Hostname for the instance
|
212
|
+
current_config: Current SSH config content
|
213
|
+
|
214
|
+
Returns:
|
215
|
+
bool: True if settings were added successfully
|
216
|
+
"""
|
217
|
+
try:
|
218
|
+
# Build host entry with all settings
|
219
|
+
proxy_command = "uvx cloudx-proxy connect %h %p"
|
220
|
+
if self.profile != "vscode":
|
221
|
+
proxy_command += f" --profile {self.profile}"
|
222
|
+
if self.aws_env:
|
223
|
+
proxy_command += f" --aws-env {self.aws_env}"
|
224
|
+
if self.ssh_key != "vscode":
|
225
|
+
proxy_command += f" --ssh-key {self.ssh_key}"
|
226
|
+
|
227
|
+
host_entry = f"""
|
228
|
+
Host cloudx-{cloudx_env}-{hostname}
|
229
|
+
HostName {instance_id}
|
230
|
+
User ec2-user
|
231
|
+
"""
|
232
|
+
if self.using_1password:
|
233
|
+
host_entry += f""" IdentityAgent ~/.1password/agent.sock
|
234
|
+
IdentityFile {self.ssh_key_file}.pub
|
235
|
+
IdentitiesOnly yes
|
236
|
+
"""
|
237
|
+
else:
|
238
|
+
host_entry += f""" IdentityFile {self.ssh_key_file}
|
239
|
+
"""
|
240
|
+
host_entry += f""" ProxyCommand {proxy_command}
|
241
|
+
"""
|
242
|
+
|
243
|
+
# Append host entry
|
244
|
+
with open(self.ssh_config_file, 'a') as f:
|
245
|
+
f.write(host_entry)
|
246
|
+
self.print_status("Host entry added with settings", True, 2)
|
247
|
+
return True
|
248
|
+
|
249
|
+
except Exception as e:
|
250
|
+
self.print_status(f"\033[1;91mError:\033[0m {str(e)}", False, 2)
|
251
|
+
continue_setup = self.prompt("Would you like to continue anyway?", "Y").lower() != 'n'
|
252
|
+
if continue_setup:
|
253
|
+
self.print_status("Continuing setup despite SSH config issues", None, 2)
|
254
|
+
return True
|
255
|
+
return False
|
256
|
+
|
257
|
+
def setup_ssh_config(self, cloudx_env: str, instance_id: str, hostname: str) -> bool:
|
258
|
+
"""Set up SSH config for the instance.
|
259
|
+
|
260
|
+
This method manages the SSH configuration in ~/.ssh/vscode/config, with the following behavior:
|
261
|
+
1. For a new environment (if cloudx-{env}-* doesn't exist):
|
262
|
+
Creates a base config with:
|
263
|
+
- User and key configuration
|
264
|
+
- 1Password SSH agent integration if selected
|
265
|
+
- ProxyCommand using uvx cloudx-proxy with proper parameters
|
266
|
+
|
267
|
+
2. For an existing environment:
|
268
|
+
- Skips creating duplicate environment config
|
269
|
+
- Only adds the new host entry
|
270
|
+
|
271
|
+
Example config structure:
|
272
|
+
```
|
273
|
+
# Base environment config (created only once per environment)
|
274
|
+
Host cloudx-{env}-*
|
275
|
+
User ec2-user
|
276
|
+
IdentityAgent ~/.1password/agent.sock # If using 1Password
|
277
|
+
IdentityFile ~/.ssh/vscode/key.pub # .pub for 1Password, no .pub otherwise
|
278
|
+
IdentitiesOnly yes # If using 1Password
|
279
|
+
ProxyCommand uvx cloudx-proxy connect %h %p --profile profile --aws-env env
|
280
|
+
|
281
|
+
# Host entries (added for each instance)
|
282
|
+
Host cloudx-{env}-hostname
|
283
|
+
HostName i-1234567890
|
284
|
+
```
|
285
|
+
|
286
|
+
Args:
|
287
|
+
cloudx_env: CloudX environment (e.g., dev, prod)
|
288
|
+
instance_id: EC2 instance ID
|
289
|
+
hostname: Hostname for the instance
|
290
|
+
|
291
|
+
Returns:
|
292
|
+
bool: True if config was set up successfully
|
293
|
+
"""
|
294
|
+
self.print_header("SSH Configuration")
|
295
|
+
self.print_status("Setting up SSH configuration...")
|
296
|
+
|
297
|
+
try:
|
298
|
+
# Check existing configuration
|
299
|
+
if self.ssh_config_file.exists():
|
300
|
+
current_config = self.ssh_config_file.read_text()
|
301
|
+
# Check if configuration for this environment already exists
|
302
|
+
if f"Host cloudx-{cloudx_env}-*" in current_config:
|
303
|
+
self.print_status(f"Found existing config for cloudx-{cloudx_env}-*", True, 2)
|
304
|
+
choice = self.prompt(
|
305
|
+
"Would you like to (1) override the existing config or "
|
306
|
+
"(2) add settings to the specific host entry?",
|
307
|
+
"1"
|
308
|
+
)
|
309
|
+
if choice == "2":
|
310
|
+
# Add settings to specific host entry
|
311
|
+
self.print_status("Adding settings to specific host entry", None, 2)
|
312
|
+
return self._add_host_entry(cloudx_env, instance_id, hostname, current_config)
|
313
|
+
else:
|
314
|
+
# Remove existing config for this environment
|
315
|
+
self.print_status("Removing existing configuration", None, 2)
|
316
|
+
lines = current_config.splitlines()
|
317
|
+
new_lines = []
|
318
|
+
skip = False
|
319
|
+
for line in lines:
|
320
|
+
if line.strip() == f"Host cloudx-{cloudx_env}-*":
|
321
|
+
skip = True
|
322
|
+
elif skip and line.startswith("Host "):
|
323
|
+
skip = False
|
324
|
+
if not skip:
|
325
|
+
new_lines.append(line)
|
326
|
+
current_config = "\n".join(new_lines)
|
327
|
+
with open(self.ssh_config_file, 'w') as f:
|
328
|
+
f.write(current_config)
|
329
|
+
|
330
|
+
# Create base config
|
331
|
+
self.print_status(f"Creating new config for cloudx-{cloudx_env}-*", None, 2)
|
332
|
+
# Build ProxyCommand with only non-default parameters
|
333
|
+
proxy_command = "uvx cloudx-proxy connect %h %p"
|
334
|
+
if self.profile != "vscode":
|
335
|
+
proxy_command += f" --profile {self.profile}"
|
336
|
+
if self.aws_env:
|
337
|
+
proxy_command += f" --aws-env {self.aws_env}"
|
338
|
+
if self.ssh_key != "vscode":
|
339
|
+
proxy_command += f" --ssh-key {self.ssh_key}"
|
340
|
+
|
341
|
+
# Build base configuration
|
342
|
+
base_config = f"""# cloudx-proxy SSH Configuration
|
343
|
+
Host cloudx-{cloudx_env}-*
|
344
|
+
User ec2-user
|
345
|
+
"""
|
346
|
+
# Add 1Password or standard key configuration
|
347
|
+
if self.using_1password:
|
348
|
+
base_config += f""" IdentityAgent ~/.1password/agent.sock
|
349
|
+
IdentityFile {self.ssh_key_file}.pub
|
350
|
+
IdentitiesOnly yes
|
351
|
+
"""
|
352
|
+
else:
|
353
|
+
base_config += f""" IdentityFile {self.ssh_key_file}
|
354
|
+
"""
|
355
|
+
# Add ProxyCommand
|
356
|
+
base_config += f""" ProxyCommand {proxy_command}
|
357
|
+
"""
|
358
|
+
|
359
|
+
# If file exists, append the new config, otherwise create it
|
360
|
+
if self.ssh_config_file.exists():
|
361
|
+
with open(self.ssh_config_file, 'a') as f:
|
362
|
+
f.write("\n" + base_config)
|
363
|
+
else:
|
364
|
+
self.ssh_config_file.write_text(base_config)
|
365
|
+
self.print_status("Base configuration created", True, 2)
|
366
|
+
|
367
|
+
# Add specific host entry
|
368
|
+
self.print_status(f"Adding host entry for cloudx-{cloudx_env}-{hostname}", None, 2)
|
369
|
+
host_entry = f"""
|
370
|
+
Host cloudx-{cloudx_env}-{hostname}
|
371
|
+
HostName {instance_id}
|
372
|
+
"""
|
373
|
+
with open(self.ssh_config_file, 'a') as f:
|
374
|
+
f.write(host_entry)
|
375
|
+
self.print_status("Host entry added", True, 2)
|
376
|
+
|
377
|
+
# Ensure main SSH config includes our config
|
378
|
+
main_config = Path(self.home_dir) / ".ssh" / "config"
|
379
|
+
include_line = f"Include {self.ssh_config_file}\n"
|
380
|
+
|
381
|
+
if main_config.exists():
|
382
|
+
content = main_config.read_text()
|
383
|
+
if include_line not in content:
|
384
|
+
with open(main_config, 'a') as f:
|
385
|
+
f.write(f"\n{include_line}")
|
386
|
+
self.print_status("Added include line to main SSH config", True, 2)
|
387
|
+
else:
|
388
|
+
self.print_status("Main SSH config already includes our config", True, 2)
|
389
|
+
else:
|
390
|
+
main_config.write_text(include_line)
|
391
|
+
self.print_status("Created main SSH config with include line", True, 2)
|
392
|
+
|
393
|
+
self.print_status("\nSSH configuration summary:", None)
|
394
|
+
self.print_status(f"Main config: {main_config}", None, 2)
|
395
|
+
self.print_status(f"cloudx-proxy config: {self.ssh_config_file}", None, 2)
|
396
|
+
self.print_status(f"Connect using: ssh cloudx-{cloudx_env}-{hostname}", None, 2)
|
397
|
+
|
398
|
+
return True
|
399
|
+
|
400
|
+
except Exception as e:
|
401
|
+
self.print_status(f"\033[1;91mError:\033[0m {str(e)}", False, 2)
|
402
|
+
continue_setup = self.prompt("Would you like to continue anyway?", "Y").lower() != 'n'
|
403
|
+
if continue_setup:
|
404
|
+
self.print_status("Continuing setup despite SSH config issues", None, 2)
|
405
|
+
return True
|
406
|
+
return False
|
407
|
+
|
408
|
+
def check_instance_setup(self, instance_id: str) -> Tuple[bool, bool]:
|
409
|
+
"""Check if instance setup is complete.
|
410
|
+
|
411
|
+
Args:
|
412
|
+
instance_id: EC2 instance ID
|
413
|
+
|
414
|
+
Returns:
|
415
|
+
Tuple[bool, bool]: (is_running, is_setup_complete)
|
416
|
+
"""
|
417
|
+
try:
|
418
|
+
session = boto3.Session(profile_name=self.profile)
|
419
|
+
ssm = session.client('ssm')
|
420
|
+
|
421
|
+
# Check if instance is online in SSM
|
422
|
+
self.print_status("Checking instance status in SSM...", None, 4)
|
423
|
+
response = ssm.describe_instance_information(
|
424
|
+
Filters=[{'Key': 'InstanceIds', 'Values': [instance_id]}]
|
425
|
+
)
|
426
|
+
is_running = bool(response['InstanceInformationList'])
|
427
|
+
|
428
|
+
if not is_running:
|
429
|
+
self.print_status("Instance is not accessible via SSM", False, 4)
|
430
|
+
return False, False
|
431
|
+
|
432
|
+
self.print_status("Instance is accessible via SSM", True, 4)
|
433
|
+
|
434
|
+
# Check setup status using SSM command
|
435
|
+
self.print_status("Checking setup status...", None, 4)
|
436
|
+
response = ssm.send_command(
|
437
|
+
InstanceIds=[instance_id],
|
438
|
+
DocumentName='AWS-RunShellScript',
|
439
|
+
Parameters={
|
440
|
+
'commands': [
|
441
|
+
'test -f /home/ec2-user/.install-done && echo "DONE" || '
|
442
|
+
'test -f /home/ec2-user/.install-running && echo "RUNNING" || '
|
443
|
+
'echo "NOT_STARTED"'
|
444
|
+
]
|
445
|
+
}
|
446
|
+
)
|
447
|
+
|
448
|
+
command_id = response['Command']['CommandId']
|
449
|
+
|
450
|
+
# Wait for command completion
|
451
|
+
for _ in range(10): # 10 second timeout
|
452
|
+
time.sleep(1)
|
453
|
+
result = ssm.get_command_invocation(
|
454
|
+
CommandId=command_id,
|
455
|
+
InstanceId=instance_id
|
456
|
+
)
|
457
|
+
if result['Status'] in ['Success', 'Failed']:
|
458
|
+
break
|
459
|
+
|
460
|
+
is_setup_complete = result['Status'] == 'Success' and result['StandardOutputContent'].strip() == 'DONE'
|
461
|
+
|
462
|
+
if is_setup_complete:
|
463
|
+
self.print_status("Setup is complete", True, 4)
|
464
|
+
else:
|
465
|
+
status = result['StandardOutputContent'].strip()
|
466
|
+
self.print_status(f"Setup status: {status}", None, 4)
|
467
|
+
|
468
|
+
return True, is_setup_complete
|
469
|
+
|
470
|
+
except Exception as e:
|
471
|
+
self.print_status(f"\033[1;91mError:\033[0m {str(e)}", False, 4)
|
472
|
+
return False, False
|
473
|
+
|
474
|
+
def wait_for_setup_completion(self, instance_id: str) -> bool:
|
475
|
+
"""Wait for instance setup to complete.
|
476
|
+
|
477
|
+
Args:
|
478
|
+
instance_id: EC2 instance ID
|
479
|
+
|
480
|
+
Returns:
|
481
|
+
bool: True if setup completed successfully
|
482
|
+
"""
|
483
|
+
self.print_header("Instance Setup Check")
|
484
|
+
self.print_status(f"Checking instance {instance_id} setup status...")
|
485
|
+
|
486
|
+
is_running, is_complete = self.check_instance_setup(instance_id)
|
487
|
+
|
488
|
+
if not is_running:
|
489
|
+
self.print_status("Instance is not running or not accessible via SSM", False, 2)
|
490
|
+
continue_setup = self.prompt("Would you like to continue anyway?", "Y").lower() != 'n'
|
491
|
+
if continue_setup:
|
492
|
+
self.print_status("Continuing setup despite instance access issues", None, 2)
|
493
|
+
return True
|
494
|
+
return False
|
495
|
+
|
496
|
+
if is_complete:
|
497
|
+
self.print_status("Instance setup is complete", True, 2)
|
498
|
+
return True
|
499
|
+
|
500
|
+
wait = self.prompt("Instance setup is not complete. Would you like to wait?", "Y").lower() != 'n'
|
501
|
+
if not wait:
|
502
|
+
self.print_status("Skipping instance setup check", None, 2)
|
503
|
+
return True
|
504
|
+
|
505
|
+
self.print_status("Waiting for setup to complete...", None, 2)
|
506
|
+
dots = 0
|
507
|
+
while True:
|
508
|
+
is_running, is_complete = self.check_instance_setup(instance_id)
|
509
|
+
|
510
|
+
if not is_running:
|
511
|
+
self.print_status("Instance is no longer running or accessible", False, 2)
|
512
|
+
continue_setup = self.prompt("Would you like to continue anyway?", "Y").lower() != 'n'
|
513
|
+
if continue_setup:
|
514
|
+
self.print_status("Continuing setup despite instance issues", None, 2)
|
515
|
+
return True
|
516
|
+
return False
|
517
|
+
|
518
|
+
if is_complete:
|
519
|
+
self.print_status("Instance setup completed successfully", True, 2)
|
520
|
+
return True
|
521
|
+
|
522
|
+
dots = (dots + 1) % 4
|
523
|
+
print(f"\r {'.' * dots}{' ' * (3 - dots)}", end='', flush=True)
|
524
|
+
time.sleep(10)
|
cloudx_proxy-0.1.1/CHANGELOG.md
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
## [0.1.1](https://github.com/easytocloud/cloudX-proxy/compare/v0.1.0...v0.1.1) (2025-02-09)
|
2
|
-
|
3
|
-
# Changelog
|
4
|
-
|
5
|
-
All notable changes to this project will be documented in this file.
|
6
|
-
|
7
|
-
## [0.1.0](https://github.com/easytocloud/cloudX-proxy/releases/tag/v0.1.0) (2025-02-09)
|
8
|
-
|
9
|
-
Initial release with core functionality:
|
10
|
-
|
11
|
-
### Features
|
12
|
-
|
13
|
-
* SSH proxy command for connecting VSCode to EC2 instances via SSM
|
14
|
-
* AWS profile configuration with cloudX-{env}-{user} format
|
15
|
-
* SSH key management with 1Password integration
|
16
|
-
* Environment-specific SSH config generation
|
17
|
-
* Instance setup status verification
|
18
|
-
* Cross-platform support (Windows, macOS, Linux)
|
19
|
-
* Automatic instance startup if stopped
|
20
|
-
* SSH key distribution via EC2 Instance Connect
|
21
|
-
* SSH tunneling through AWS Systems Manager
|
@@ -1,336 +0,0 @@
|
|
1
|
-
import os
|
2
|
-
import time
|
3
|
-
import json
|
4
|
-
import subprocess
|
5
|
-
from pathlib import Path
|
6
|
-
from typing import Optional, Tuple
|
7
|
-
import boto3
|
8
|
-
from botocore.exceptions import ClientError
|
9
|
-
|
10
|
-
class CloudXSetup:
|
11
|
-
def __init__(self, profile: str = "vscode", ssh_key: str = "vscode", aws_env: str = None):
|
12
|
-
"""Initialize CloudX setup.
|
13
|
-
|
14
|
-
Args:
|
15
|
-
profile: AWS profile name (default: "vscode")
|
16
|
-
ssh_key: SSH key name (default: "vscode")
|
17
|
-
aws_env: AWS environment directory (default: None)
|
18
|
-
"""
|
19
|
-
self.profile = profile
|
20
|
-
self.ssh_key = ssh_key
|
21
|
-
self.aws_env = aws_env
|
22
|
-
self.home_dir = str(Path.home())
|
23
|
-
self.ssh_dir = Path(self.home_dir) / ".ssh" / "vscode"
|
24
|
-
self.ssh_config_file = self.ssh_dir / "config"
|
25
|
-
self.ssh_key_file = self.ssh_dir / f"{ssh_key}"
|
26
|
-
self.using_1password = False
|
27
|
-
|
28
|
-
def setup_aws_profile(self) -> bool:
|
29
|
-
"""Set up AWS profile using aws configure command.
|
30
|
-
|
31
|
-
Returns:
|
32
|
-
bool: True if profile was set up successfully
|
33
|
-
"""
|
34
|
-
try:
|
35
|
-
# Configure AWS environment if specified
|
36
|
-
if self.aws_env:
|
37
|
-
aws_env_dir = os.path.expanduser(f"~/.aws/aws-envs/{self.aws_env}")
|
38
|
-
os.environ["AWS_CONFIG_FILE"] = os.path.join(aws_env_dir, "config")
|
39
|
-
os.environ["AWS_SHARED_CREDENTIALS_FILE"] = os.path.join(aws_env_dir, "credentials")
|
40
|
-
|
41
|
-
# Check if profile exists
|
42
|
-
session = boto3.Session(profile_name=self.profile)
|
43
|
-
try:
|
44
|
-
session.client('sts').get_caller_identity()
|
45
|
-
print(f"AWS profile '{self.profile}' already exists and is valid.")
|
46
|
-
return True
|
47
|
-
except ClientError:
|
48
|
-
pass
|
49
|
-
|
50
|
-
# Profile doesn't exist or is invalid, set it up
|
51
|
-
print(f"Setting up AWS profile '{self.profile}'...")
|
52
|
-
print("Please enter your AWS credentials:")
|
53
|
-
|
54
|
-
# Use aws configure command
|
55
|
-
subprocess.run([
|
56
|
-
'aws', 'configure',
|
57
|
-
'--profile', self.profile
|
58
|
-
], check=True)
|
59
|
-
|
60
|
-
# Verify the profile works
|
61
|
-
session = boto3.Session(profile_name=self.profile)
|
62
|
-
identity = session.client('sts').get_caller_identity()
|
63
|
-
user_arn = identity['Arn']
|
64
|
-
|
65
|
-
if not any(part.startswith('cloudX-') for part in user_arn.split('/')):
|
66
|
-
print(f"Warning: User ARN '{user_arn}' does not match expected format cloudX-{{env}}-{{user}}")
|
67
|
-
|
68
|
-
return True
|
69
|
-
|
70
|
-
except Exception as e:
|
71
|
-
print(f"Error setting up AWS profile: {e}")
|
72
|
-
return False
|
73
|
-
|
74
|
-
def setup_ssh_key(self) -> bool:
|
75
|
-
"""Set up SSH key pair.
|
76
|
-
|
77
|
-
Returns:
|
78
|
-
bool: True if key was set up successfully
|
79
|
-
"""
|
80
|
-
try:
|
81
|
-
# Create .ssh/vscode directory if it doesn't exist
|
82
|
-
self.ssh_dir.mkdir(parents=True, exist_ok=True)
|
83
|
-
|
84
|
-
key_exists = self.ssh_key_file.exists() and (self.ssh_key_file.with_suffix('.pub')).exists()
|
85
|
-
|
86
|
-
if key_exists:
|
87
|
-
print(f"SSH key '{self.ssh_key}' already exists.")
|
88
|
-
self.using_1password = input("Would you like to use 1Password SSH agent? (y/N): ").lower() == 'y'
|
89
|
-
if not self.using_1password:
|
90
|
-
store_in_1password = input("Would you like to store the private key in 1Password? (y/N): ").lower() == 'y'
|
91
|
-
if store_in_1password:
|
92
|
-
self._store_key_in_1password()
|
93
|
-
else:
|
94
|
-
print(f"Generating new SSH key '{self.ssh_key}'...")
|
95
|
-
subprocess.run([
|
96
|
-
'ssh-keygen',
|
97
|
-
'-t', 'ed25519',
|
98
|
-
'-f', str(self.ssh_key_file),
|
99
|
-
'-N', '' # Empty passphrase
|
100
|
-
], check=True)
|
101
|
-
|
102
|
-
self.using_1password = input("Would you like to use 1Password SSH agent? (y/N): ").lower() == 'y'
|
103
|
-
if not self.using_1password:
|
104
|
-
store_in_1password = input("Would you like to store the private key in 1Password? (y/N): ").lower() == 'y'
|
105
|
-
if store_in_1password:
|
106
|
-
self._store_key_in_1password()
|
107
|
-
|
108
|
-
return True
|
109
|
-
|
110
|
-
except Exception as e:
|
111
|
-
print(f"Error setting up SSH key: {e}")
|
112
|
-
return False
|
113
|
-
|
114
|
-
def _store_key_in_1password(self) -> bool:
|
115
|
-
"""Store SSH private key in 1Password.
|
116
|
-
|
117
|
-
Returns:
|
118
|
-
bool: True if key was stored successfully
|
119
|
-
"""
|
120
|
-
try:
|
121
|
-
subprocess.run(['op', '--version'], check=True, capture_output=True)
|
122
|
-
print("Storing private key in 1Password...")
|
123
|
-
subprocess.run([
|
124
|
-
'op', 'document', 'create',
|
125
|
-
str(self.ssh_key_file),
|
126
|
-
'--title', f'CloudX SSH Key - {self.ssh_key}'
|
127
|
-
], check=True)
|
128
|
-
return True
|
129
|
-
except subprocess.CalledProcessError:
|
130
|
-
print("Error: 1Password CLI not installed or not signed in.")
|
131
|
-
return False
|
132
|
-
|
133
|
-
def setup_ssh_config(self, cloudx_env: str, instance_id: str, hostname: str) -> bool:
|
134
|
-
"""Set up SSH config for the instance.
|
135
|
-
|
136
|
-
This method manages the SSH configuration in ~/.ssh/vscode/config, with the following behavior:
|
137
|
-
1. For a new environment (if cloudx-{env}-* doesn't exist):
|
138
|
-
Creates a base config with:
|
139
|
-
- User and key configuration
|
140
|
-
- 1Password SSH agent integration if selected
|
141
|
-
- ProxyCommand using uvx cloudx-proxy with proper parameters
|
142
|
-
|
143
|
-
2. For an existing environment:
|
144
|
-
- Skips creating duplicate environment config
|
145
|
-
- Only adds the new host entry
|
146
|
-
|
147
|
-
Example config structure:
|
148
|
-
```
|
149
|
-
# Base environment config (created only once per environment)
|
150
|
-
Host cloudx-{env}-*
|
151
|
-
User ec2-user
|
152
|
-
IdentityAgent ~/.1password/agent.sock # If using 1Password
|
153
|
-
IdentityFile ~/.ssh/vscode/key.pub # .pub for 1Password, no .pub otherwise
|
154
|
-
IdentitiesOnly yes # If using 1Password
|
155
|
-
ProxyCommand uvx cloudx-proxy connect %h %p --profile profile --aws-env env
|
156
|
-
|
157
|
-
# Host entries (added for each instance)
|
158
|
-
Host cloudx-{env}-hostname
|
159
|
-
HostName i-1234567890
|
160
|
-
```
|
161
|
-
|
162
|
-
Args:
|
163
|
-
cloudx_env: CloudX environment (e.g., dev, prod)
|
164
|
-
instance_id: EC2 instance ID
|
165
|
-
hostname: Hostname for the instance
|
166
|
-
|
167
|
-
Returns:
|
168
|
-
bool: True if config was set up successfully
|
169
|
-
"""
|
170
|
-
try:
|
171
|
-
# Check if we need to create base config
|
172
|
-
need_base_config = True
|
173
|
-
if self.ssh_config_file.exists():
|
174
|
-
current_config = self.ssh_config_file.read_text()
|
175
|
-
# Check if configuration for this environment already exists
|
176
|
-
if f"Host cloudx-{cloudx_env}-*" in current_config:
|
177
|
-
need_base_config = False
|
178
|
-
|
179
|
-
if need_base_config:
|
180
|
-
# Build ProxyCommand with all necessary parameters
|
181
|
-
proxy_command = f"uvx cloudx-proxy connect %h %p --profile {self.profile}"
|
182
|
-
if self.aws_env:
|
183
|
-
proxy_command += f" --aws-env {self.aws_env}"
|
184
|
-
if self.ssh_key != "vscode":
|
185
|
-
proxy_command += f" --key-path {self.ssh_key_file}.pub"
|
186
|
-
|
187
|
-
# Build base configuration
|
188
|
-
base_config = f"""# CloudX SSH Configuration
|
189
|
-
Host cloudx-{cloudx_env}-*
|
190
|
-
User ec2-user
|
191
|
-
"""
|
192
|
-
# Add 1Password or standard key configuration
|
193
|
-
if self.using_1password:
|
194
|
-
base_config += f""" IdentityAgent ~/.1password/agent.sock
|
195
|
-
IdentityFile {self.ssh_key_file}.pub
|
196
|
-
IdentitiesOnly yes
|
197
|
-
"""
|
198
|
-
else:
|
199
|
-
base_config += f""" IdentityFile {self.ssh_key_file}
|
200
|
-
"""
|
201
|
-
# Add ProxyCommand
|
202
|
-
base_config += f""" ProxyCommand {proxy_command}
|
203
|
-
"""
|
204
|
-
|
205
|
-
# If file exists, append the new config, otherwise create it
|
206
|
-
if self.ssh_config_file.exists():
|
207
|
-
with open(self.ssh_config_file, 'a') as f:
|
208
|
-
f.write("\n" + base_config)
|
209
|
-
else:
|
210
|
-
self.ssh_config_file.write_text(base_config)
|
211
|
-
|
212
|
-
# Add specific host entry
|
213
|
-
host_entry = f"""
|
214
|
-
Host cloudx-{cloudx_env}-{hostname}
|
215
|
-
HostName {instance_id}
|
216
|
-
"""
|
217
|
-
with open(self.ssh_config_file, 'a') as f:
|
218
|
-
f.write(host_entry)
|
219
|
-
|
220
|
-
# Ensure main SSH config includes our config
|
221
|
-
main_config = Path(self.home_dir) / ".ssh" / "config"
|
222
|
-
include_line = f"Include {self.ssh_config_file}\n"
|
223
|
-
|
224
|
-
if main_config.exists():
|
225
|
-
content = main_config.read_text()
|
226
|
-
if include_line not in content:
|
227
|
-
with open(main_config, 'a') as f:
|
228
|
-
f.write(f"\n{include_line}")
|
229
|
-
else:
|
230
|
-
main_config.write_text(include_line)
|
231
|
-
|
232
|
-
print(f"\nSSH configuration has been set up:")
|
233
|
-
print(f"- Main config file: {main_config}")
|
234
|
-
print(f"- CloudX config file: {self.ssh_config_file}")
|
235
|
-
print(f"\nYou can now connect using: ssh cloudx-{cloudx_env}-{hostname}")
|
236
|
-
|
237
|
-
return True
|
238
|
-
|
239
|
-
except Exception as e:
|
240
|
-
print(f"Error setting up SSH config: {e}")
|
241
|
-
return False
|
242
|
-
|
243
|
-
def check_instance_setup(self, instance_id: str) -> Tuple[bool, bool]:
|
244
|
-
"""Check if instance setup is complete.
|
245
|
-
|
246
|
-
Args:
|
247
|
-
instance_id: EC2 instance ID
|
248
|
-
|
249
|
-
Returns:
|
250
|
-
Tuple[bool, bool]: (is_running, is_setup_complete)
|
251
|
-
"""
|
252
|
-
try:
|
253
|
-
session = boto3.Session(profile_name=self.profile)
|
254
|
-
ssm = session.client('ssm')
|
255
|
-
|
256
|
-
# Check if instance is online in SSM
|
257
|
-
response = ssm.describe_instance_information(
|
258
|
-
Filters=[{'Key': 'InstanceIds', 'Values': [instance_id]}]
|
259
|
-
)
|
260
|
-
is_running = bool(response['InstanceInformationList'])
|
261
|
-
|
262
|
-
if not is_running:
|
263
|
-
return False, False
|
264
|
-
|
265
|
-
# Check setup status using SSM command
|
266
|
-
response = ssm.send_command(
|
267
|
-
InstanceIds=[instance_id],
|
268
|
-
DocumentName='AWS-RunShellScript',
|
269
|
-
Parameters={
|
270
|
-
'commands': [
|
271
|
-
'test -f /home/ec2-user/.install-done && echo "DONE" || '
|
272
|
-
'test -f /home/ec2-user/.install-running && echo "RUNNING" || '
|
273
|
-
'echo "NOT_STARTED"'
|
274
|
-
]
|
275
|
-
}
|
276
|
-
)
|
277
|
-
|
278
|
-
command_id = response['Command']['CommandId']
|
279
|
-
|
280
|
-
# Wait for command completion
|
281
|
-
for _ in range(10): # 10 second timeout
|
282
|
-
time.sleep(1)
|
283
|
-
result = ssm.get_command_invocation(
|
284
|
-
CommandId=command_id,
|
285
|
-
InstanceId=instance_id
|
286
|
-
)
|
287
|
-
if result['Status'] in ['Success', 'Failed']:
|
288
|
-
break
|
289
|
-
|
290
|
-
is_setup_complete = result['Status'] == 'Success' and result['StandardOutputContent'].strip() == 'DONE'
|
291
|
-
|
292
|
-
return True, is_setup_complete
|
293
|
-
|
294
|
-
except Exception as e:
|
295
|
-
print(f"Error checking instance setup: {e}")
|
296
|
-
return False, False
|
297
|
-
|
298
|
-
def wait_for_setup_completion(self, instance_id: str) -> bool:
|
299
|
-
"""Wait for instance setup to complete.
|
300
|
-
|
301
|
-
Args:
|
302
|
-
instance_id: EC2 instance ID
|
303
|
-
|
304
|
-
Returns:
|
305
|
-
bool: True if setup completed successfully
|
306
|
-
"""
|
307
|
-
print(f"Checking setup status for instance {instance_id}...")
|
308
|
-
|
309
|
-
is_running, is_complete = self.check_instance_setup(instance_id)
|
310
|
-
|
311
|
-
if not is_running:
|
312
|
-
print("Error: Instance is not running or not accessible via SSM")
|
313
|
-
return False
|
314
|
-
|
315
|
-
if is_complete:
|
316
|
-
print("Instance setup is already complete")
|
317
|
-
return True
|
318
|
-
|
319
|
-
wait = input("Instance setup is not complete. Would you like to wait? (Y/n): ").lower() != 'n'
|
320
|
-
if not wait:
|
321
|
-
return False
|
322
|
-
|
323
|
-
print("Waiting for setup to complete", end='', flush=True)
|
324
|
-
while True:
|
325
|
-
is_running, is_complete = self.check_instance_setup(instance_id)
|
326
|
-
|
327
|
-
if not is_running:
|
328
|
-
print("\nError: Instance is no longer running or accessible")
|
329
|
-
return False
|
330
|
-
|
331
|
-
if is_complete:
|
332
|
-
print("\nInstance setup completed successfully")
|
333
|
-
return True
|
334
|
-
|
335
|
-
print(".", end='', flush=True)
|
336
|
-
time.sleep(10)
|
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
|