py2ls 0.1.10.12__py3-none-any.whl → 0.2.7.10__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.
Potentially problematic release.
This version of py2ls might be problematic. Click here for more details.
- py2ls/.DS_Store +0 -0
- py2ls/.git/.DS_Store +0 -0
- py2ls/.git/index +0 -0
- py2ls/.git/logs/refs/remotes/origin/HEAD +1 -0
- py2ls/.git/objects/.DS_Store +0 -0
- py2ls/.git/refs/.DS_Store +0 -0
- py2ls/ImageLoader.py +621 -0
- py2ls/__init__.py +7 -5
- py2ls/apptainer2ls.py +3940 -0
- py2ls/batman.py +164 -42
- py2ls/bio.py +2595 -0
- py2ls/cell_image_clf.py +1632 -0
- py2ls/container2ls.py +4635 -0
- py2ls/corr.py +475 -0
- py2ls/data/.DS_Store +0 -0
- py2ls/data/email/email_html_template.html +88 -0
- py2ls/data/hyper_param_autogluon_zeroshot2024.json +2383 -0
- py2ls/data/hyper_param_tabrepo_2024.py +1753 -0
- py2ls/data/mygenes_fields_241022.txt +355 -0
- py2ls/data/re_common_pattern.json +173 -0
- py2ls/data/sns_info.json +74 -0
- py2ls/data/styles/.DS_Store +0 -0
- py2ls/data/styles/example/.DS_Store +0 -0
- py2ls/data/styles/stylelib/.DS_Store +0 -0
- py2ls/data/styles/stylelib/grid.mplstyle +15 -0
- py2ls/data/styles/stylelib/high-contrast.mplstyle +6 -0
- py2ls/data/styles/stylelib/high-vis.mplstyle +4 -0
- py2ls/data/styles/stylelib/ieee.mplstyle +15 -0
- py2ls/data/styles/stylelib/light.mplstyl +6 -0
- py2ls/data/styles/stylelib/muted.mplstyle +6 -0
- py2ls/data/styles/stylelib/nature-reviews-latex.mplstyle +616 -0
- py2ls/data/styles/stylelib/nature-reviews.mplstyle +616 -0
- py2ls/data/styles/stylelib/nature.mplstyle +31 -0
- py2ls/data/styles/stylelib/no-latex.mplstyle +10 -0
- py2ls/data/styles/stylelib/notebook.mplstyle +36 -0
- py2ls/data/styles/stylelib/paper.mplstyle +290 -0
- py2ls/data/styles/stylelib/paper2.mplstyle +305 -0
- py2ls/data/styles/stylelib/retro.mplstyle +4 -0
- py2ls/data/styles/stylelib/sans.mplstyle +10 -0
- py2ls/data/styles/stylelib/scatter.mplstyle +7 -0
- py2ls/data/styles/stylelib/science.mplstyle +48 -0
- py2ls/data/styles/stylelib/std-colors.mplstyle +4 -0
- py2ls/data/styles/stylelib/vibrant.mplstyle +6 -0
- py2ls/data/tiles.csv +146 -0
- py2ls/data/usages_pd.json +1417 -0
- py2ls/data/usages_sns.json +31 -0
- py2ls/docker2ls.py +5446 -0
- py2ls/ec2ls.py +61 -0
- py2ls/fetch_update.py +145 -0
- py2ls/ich2ls.py +1955 -296
- py2ls/im2.py +8242 -0
- py2ls/image_ml2ls.py +2100 -0
- py2ls/ips.py +33909 -3418
- py2ls/ml2ls.py +7700 -0
- py2ls/mol.py +289 -0
- py2ls/mount2ls.py +1307 -0
- py2ls/netfinder.py +873 -351
- py2ls/nl2ls.py +283 -0
- py2ls/ocr.py +1581 -458
- py2ls/plot.py +10394 -314
- py2ls/rna2ls.py +311 -0
- py2ls/ssh2ls.md +456 -0
- py2ls/ssh2ls.py +5933 -0
- py2ls/ssh2ls_v01.py +2204 -0
- py2ls/stats.py +66 -172
- py2ls/temp20251124.py +509 -0
- py2ls/translator.py +2 -0
- py2ls/utils/decorators.py +3564 -0
- py2ls/utils_bio.py +3453 -0
- {py2ls-0.1.10.12.dist-info → py2ls-0.2.7.10.dist-info}/METADATA +113 -224
- {py2ls-0.1.10.12.dist-info → py2ls-0.2.7.10.dist-info}/RECORD +72 -16
- {py2ls-0.1.10.12.dist-info → py2ls-0.2.7.10.dist-info}/WHEEL +0 -0
py2ls/ssh2ls_v01.py
ADDED
|
@@ -0,0 +1,2204 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
SSH2LS - Ultimate SSH Multi-Hop Connection Manager
|
|
4
|
+
Handles complex jump host setups, key management, agent forwarding, and secure file transfers.
|
|
5
|
+
usage:
|
|
6
|
+
# 1. Interactive Wizard Mode:
|
|
7
|
+
# Start the interactive wizard
|
|
8
|
+
python ssh2ls.py wizard
|
|
9
|
+
|
|
10
|
+
# Or just (defaults to wizard)
|
|
11
|
+
python ssh2ls.py
|
|
12
|
+
# 2. Quick deNBI Setup:
|
|
13
|
+
# Run the automated deNBI setup
|
|
14
|
+
python denbi_setup.py
|
|
15
|
+
|
|
16
|
+
3. Command Line Usage:
|
|
17
|
+
# Setup a connection (like your deNBI setup)
|
|
18
|
+
python ssh2ls.py setup denbi \
|
|
19
|
+
--jumphost 193.196.20.189 \
|
|
20
|
+
--jumphost-user ubuntu \
|
|
21
|
+
--target 192.168.54.219 \
|
|
22
|
+
--key ~/.ssh/denbi
|
|
23
|
+
|
|
24
|
+
# Connect to target via jumphost
|
|
25
|
+
python ssh2ls.py connect denbi --hop target
|
|
26
|
+
|
|
27
|
+
# Upload files
|
|
28
|
+
python ssh2ls.py transfer upload denbi ./local_data /home/ubuntu/data/
|
|
29
|
+
|
|
30
|
+
# Setup remote environment (bioinformatics)
|
|
31
|
+
python ssh2ls.py setup-env denbi --template bioinfo
|
|
32
|
+
|
|
33
|
+
# Create SSH tunnel for web access
|
|
34
|
+
python ssh2ls.py tunnel create denbi 8080 localhost 80
|
|
35
|
+
|
|
36
|
+
# Show quick commands
|
|
37
|
+
python ssh2ls.py quick denbi
|
|
38
|
+
|
|
39
|
+
# Test connection
|
|
40
|
+
python ssh2ls.py test denbi
|
|
41
|
+
|
|
42
|
+
# List all connections
|
|
43
|
+
python ssh2ls.py list
|
|
44
|
+
|
|
45
|
+
4. Quick Aliases Created:
|
|
46
|
+
# Add to your shell
|
|
47
|
+
source ~/denbi_aliases.sh
|
|
48
|
+
|
|
49
|
+
# Then use:
|
|
50
|
+
denbi-jump # Connect to jumphost
|
|
51
|
+
denbi-vm # Connect to target VM
|
|
52
|
+
denbi-upload # Upload files
|
|
53
|
+
denbi-download # Download files
|
|
54
|
+
denbi-test # Test connection
|
|
55
|
+
denbi-copy-data /path/to/data # Copy data to VM
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
import os
|
|
59
|
+
import sys
|
|
60
|
+
import json
|
|
61
|
+
import yaml
|
|
62
|
+
import argparse
|
|
63
|
+
import getpass
|
|
64
|
+
import subprocess
|
|
65
|
+
import textwrap
|
|
66
|
+
import platform
|
|
67
|
+
import socket
|
|
68
|
+
import stat
|
|
69
|
+
import time
|
|
70
|
+
from pathlib import Path
|
|
71
|
+
from typing import Dict, List, Any, Optional, Tuple, Union
|
|
72
|
+
from dataclasses import dataclass, field, asdict
|
|
73
|
+
from datetime import datetime
|
|
74
|
+
import shutil
|
|
75
|
+
import paramiko
|
|
76
|
+
from cryptography.fernet import Fernet
|
|
77
|
+
import base64
|
|
78
|
+
import tempfile
|
|
79
|
+
import threading
|
|
80
|
+
import queue
|
|
81
|
+
import select
|
|
82
|
+
|
|
83
|
+
class SSHKeyManager:
|
|
84
|
+
"""Manages SSH keys including generation, loading, and agent operations"""
|
|
85
|
+
|
|
86
|
+
def __init__(self, ssh_dir: str = "~/.ssh"):
|
|
87
|
+
self.ssh_dir = Path(ssh_dir).expanduser()
|
|
88
|
+
self.ssh_dir.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
self.agent_socket = os.environ.get('SSH_AUTH_SOCK')
|
|
90
|
+
self.key_cache = {}
|
|
91
|
+
|
|
92
|
+
def generate_key_pair(self, key_name: str = None, key_type: str = "ed25519",
|
|
93
|
+
key_size: int = 4096, passphrase: str = None,
|
|
94
|
+
comment: str = None) -> Tuple[Path, Path]:
|
|
95
|
+
"""
|
|
96
|
+
Generate SSH key pair with specified parameters
|
|
97
|
+
Returns: (private_key_path, public_key_path)
|
|
98
|
+
"""
|
|
99
|
+
if not key_name:
|
|
100
|
+
key_name = f"id_{key_type}_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
|
101
|
+
|
|
102
|
+
private_key_path = self.ssh_dir / key_name
|
|
103
|
+
public_key_path = private_key_path.with_suffix('.pub')
|
|
104
|
+
|
|
105
|
+
if private_key_path.exists():
|
|
106
|
+
raise FileExistsError(f"Key {private_key_path} already exists")
|
|
107
|
+
|
|
108
|
+
# Generate key using paramiko
|
|
109
|
+
if key_type == "rsa":
|
|
110
|
+
key = paramiko.RSAKey.generate(bits=key_size)
|
|
111
|
+
elif key_type == "ed25519":
|
|
112
|
+
key = paramiko.Ed25519Key.generate()
|
|
113
|
+
elif key_type == "ecdsa":
|
|
114
|
+
key = paramiko.ECDSAKey.generate()
|
|
115
|
+
else:
|
|
116
|
+
raise ValueError(f"Unsupported key type: {key_type}")
|
|
117
|
+
|
|
118
|
+
# Save private key
|
|
119
|
+
if passphrase:
|
|
120
|
+
key.write_private_key_file(str(private_key_path), password=passphrase)
|
|
121
|
+
else:
|
|
122
|
+
key.write_private_key_file(str(private_key_path))
|
|
123
|
+
|
|
124
|
+
# Save public key
|
|
125
|
+
if not comment:
|
|
126
|
+
comment = f"{getpass.getuser()}@{socket.gethostname()}-{datetime.now().strftime('%Y%m%d')}"
|
|
127
|
+
|
|
128
|
+
with open(public_key_path, 'w') as f:
|
|
129
|
+
f.write(f"{key.get_name()} {key.get_base64()} {comment}")
|
|
130
|
+
|
|
131
|
+
# Set proper permissions
|
|
132
|
+
private_key_path.chmod(0o600)
|
|
133
|
+
public_key_path.chmod(0o644)
|
|
134
|
+
|
|
135
|
+
print(f"✓ Generated key pair:")
|
|
136
|
+
print(f" Private: {private_key_path}")
|
|
137
|
+
print(f" Public: {public_key_path}")
|
|
138
|
+
print(f" Comment: {comment}")
|
|
139
|
+
|
|
140
|
+
# Show public key for easy copying
|
|
141
|
+
print(f"\nPublic key (copy this to share):")
|
|
142
|
+
print("-" * 80)
|
|
143
|
+
with open(public_key_path, 'r') as f:
|
|
144
|
+
print(f.read().strip())
|
|
145
|
+
print("-" * 80)
|
|
146
|
+
|
|
147
|
+
return private_key_path, public_key_path
|
|
148
|
+
|
|
149
|
+
def add_key_to_agent(self, key_path: Path, passphrase: str = None) -> bool:
|
|
150
|
+
"""Add SSH key to SSH agent"""
|
|
151
|
+
if not self.agent_socket:
|
|
152
|
+
print("⚠ SSH agent not running. Start with: eval $(ssh-agent)")
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
# Try using ssh-add command
|
|
157
|
+
cmd = ['ssh-add', str(key_path)]
|
|
158
|
+
if passphrase:
|
|
159
|
+
# For passphrase-protected keys, we need to handle it interactively
|
|
160
|
+
result = subprocess.run(cmd, capture_output=True, text=True, input=passphrase)
|
|
161
|
+
else:
|
|
162
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
163
|
+
|
|
164
|
+
if result.returncode == 0:
|
|
165
|
+
print(f"✓ Key added to SSH agent: {key_path.name}")
|
|
166
|
+
return True
|
|
167
|
+
else:
|
|
168
|
+
print(f"✗ Failed to add key to agent: {result.stderr}")
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
except Exception as e:
|
|
172
|
+
print(f"✗ Error adding key to agent: {e}")
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
def list_keys_in_agent(self) -> List[str]:
|
|
176
|
+
"""List all keys currently loaded in SSH agent"""
|
|
177
|
+
if not self.agent_socket:
|
|
178
|
+
return []
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
result = subprocess.run(['ssh-add', '-l'], capture_output=True, text=True)
|
|
182
|
+
if result.returncode == 0:
|
|
183
|
+
return result.stdout.strip().split('\n')
|
|
184
|
+
except Exception:
|
|
185
|
+
pass
|
|
186
|
+
return []
|
|
187
|
+
|
|
188
|
+
def remove_key_from_agent(self, key_path: Path) -> bool:
|
|
189
|
+
"""Remove specific key from SSH agent"""
|
|
190
|
+
if not self.agent_socket:
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
result = subprocess.run(['ssh-add', '-d', str(key_path)],
|
|
195
|
+
capture_output=True, text=True)
|
|
196
|
+
return result.returncode == 0
|
|
197
|
+
except Exception:
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
def check_key_permissions(self, key_path: Path) -> bool:
|
|
201
|
+
"""Check if SSH key has correct permissions (600)"""
|
|
202
|
+
try:
|
|
203
|
+
mode = key_path.stat().st_mode
|
|
204
|
+
return stat.S_IMODE(mode) == 0o600
|
|
205
|
+
except Exception:
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
def fix_key_permissions(self, key_path: Path):
|
|
209
|
+
"""Fix SSH key permissions to 600"""
|
|
210
|
+
key_path.chmod(0o600)
|
|
211
|
+
print(f"✓ Fixed permissions for: {key_path}")
|
|
212
|
+
|
|
213
|
+
def find_available_keys(self) -> List[Path]:
|
|
214
|
+
"""Find all SSH keys in .ssh directory"""
|
|
215
|
+
key_patterns = ['id_rsa', 'id_ed25519', 'id_ecdsa', 'id_dsa']
|
|
216
|
+
keys = []
|
|
217
|
+
|
|
218
|
+
for pattern in key_patterns:
|
|
219
|
+
for key_file in self.ssh_dir.glob(f"{pattern}*"):
|
|
220
|
+
if not key_file.name.endswith('.pub'):
|
|
221
|
+
keys.append(key_file)
|
|
222
|
+
|
|
223
|
+
return keys
|
|
224
|
+
|
|
225
|
+
def get_public_key_from_private(self, private_key_path: Path) -> Optional[str]:
|
|
226
|
+
"""Extract public key from private key"""
|
|
227
|
+
try:
|
|
228
|
+
for key_class in [paramiko.RSAKey, paramiko.Ed25519Key, paramiko.ECDSAKey, paramiko.DSSKey]:
|
|
229
|
+
try:
|
|
230
|
+
key = key_class(filename=str(private_key_path))
|
|
231
|
+
return f"{key.get_name()} {key.get_base64()} {getpass.getuser()}@{socket.gethostname()}"
|
|
232
|
+
except paramiko.SSHException:
|
|
233
|
+
continue
|
|
234
|
+
except Exception as e:
|
|
235
|
+
print(f"✗ Error extracting public key: {e}")
|
|
236
|
+
|
|
237
|
+
return None
|
|
238
|
+
|
|
239
|
+
class MultiHopSSHManager:
|
|
240
|
+
"""Manages complex multi-hop SSH connections with agent forwarding"""
|
|
241
|
+
|
|
242
|
+
def __init__(self, config_dir: str = "~/.ssh/ssh2ls"):
|
|
243
|
+
self.config_dir = Path(config_dir).expanduser()
|
|
244
|
+
self.config_dir.mkdir(parents=True, exist_ok=True)
|
|
245
|
+
self.sessions_file = self.config_dir / "sessions.json"
|
|
246
|
+
self.tunnels_file = self.config_dir / "tunnels.json"
|
|
247
|
+
self.sessions = self.load_sessions()
|
|
248
|
+
self.tunnels = self.load_tunnels()
|
|
249
|
+
self.key_manager = SSHKeyManager()
|
|
250
|
+
|
|
251
|
+
def load_sessions(self) -> Dict:
|
|
252
|
+
"""Load saved SSH sessions"""
|
|
253
|
+
if self.sessions_file.exists():
|
|
254
|
+
with open(self.sessions_file, 'r') as f:
|
|
255
|
+
return json.load(f)
|
|
256
|
+
return {}
|
|
257
|
+
|
|
258
|
+
def save_sessions(self):
|
|
259
|
+
"""Save SSH sessions to file"""
|
|
260
|
+
with open(self.sessions_file, 'w') as f:
|
|
261
|
+
json.dump(self.sessions, f, indent=2)
|
|
262
|
+
|
|
263
|
+
def load_tunnels(self) -> Dict:
|
|
264
|
+
"""Load saved tunnel configurations"""
|
|
265
|
+
if self.tunnels_file.exists():
|
|
266
|
+
with open(self.tunnels_file, 'r') as f:
|
|
267
|
+
return json.load(f)
|
|
268
|
+
return {}
|
|
269
|
+
|
|
270
|
+
def save_tunnels(self):
|
|
271
|
+
"""Save tunnel configurations"""
|
|
272
|
+
with open(self.tunnels_file, 'w') as f:
|
|
273
|
+
json.dump(self.tunnels, f, indent=2)
|
|
274
|
+
|
|
275
|
+
def create_jumphost_config(self, name: str, jumphost_ip: str, jumphost_user: str,
|
|
276
|
+
private_key_path: str, target_ip: str, target_user: str = None,
|
|
277
|
+
description: str = "") -> Dict:
|
|
278
|
+
"""Create a jumphost configuration like in your notes"""
|
|
279
|
+
|
|
280
|
+
if not target_user:
|
|
281
|
+
target_user = jumphost_user
|
|
282
|
+
|
|
283
|
+
config = {
|
|
284
|
+
"name": name,
|
|
285
|
+
"jumphost": {
|
|
286
|
+
"ip": jumphost_ip,
|
|
287
|
+
"user": jumphost_user,
|
|
288
|
+
"private_key": private_key_path,
|
|
289
|
+
"description": f"Jumphost for accessing {target_ip}"
|
|
290
|
+
},
|
|
291
|
+
"target": {
|
|
292
|
+
"ip": target_ip,
|
|
293
|
+
"user": target_user,
|
|
294
|
+
"description": description or f"Target VM behind {jumphost_ip}"
|
|
295
|
+
},
|
|
296
|
+
"agent_forwarding": True,
|
|
297
|
+
"created": datetime.now().isoformat(),
|
|
298
|
+
"last_used": None,
|
|
299
|
+
"usage_count": 0
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
# Add to sessions
|
|
303
|
+
self.sessions[name] = config
|
|
304
|
+
self.save_sessions()
|
|
305
|
+
|
|
306
|
+
print(f"✓ Created jumphost configuration '{name}':")
|
|
307
|
+
print(f" Jumphost: {jumphost_user}@{jumphost_ip}")
|
|
308
|
+
print(f" Target: {target_user}@{target_ip}")
|
|
309
|
+
print(f" Key: {private_key_path}")
|
|
310
|
+
|
|
311
|
+
return config
|
|
312
|
+
|
|
313
|
+
def generate_ssh_config(self, session_name: str) -> str:
|
|
314
|
+
"""Generate SSH config for a session"""
|
|
315
|
+
if session_name not in self.sessions:
|
|
316
|
+
raise KeyError(f"Session '{session_name}' not found")
|
|
317
|
+
|
|
318
|
+
session = self.sessions[session_name]
|
|
319
|
+
jh = session["jumphost"]
|
|
320
|
+
target = session["target"]
|
|
321
|
+
|
|
322
|
+
config = f"""
|
|
323
|
+
# SSH Configuration for {session_name}
|
|
324
|
+
# Generated by ssh2ls on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
|
325
|
+
# Description: {session.get('description', '')}
|
|
326
|
+
|
|
327
|
+
Host {session_name}_jumphost
|
|
328
|
+
HostName {jh['ip']}
|
|
329
|
+
User {jh['user']}
|
|
330
|
+
IdentityFile {jh['private_key']}
|
|
331
|
+
ForwardAgent {"yes" if session.get('agent_forwarding', True) else "no"}
|
|
332
|
+
ServerAliveInterval 30
|
|
333
|
+
ServerAliveCountMax 3
|
|
334
|
+
# Add any custom options here
|
|
335
|
+
|
|
336
|
+
Host {session_name}
|
|
337
|
+
HostName {target['ip']}
|
|
338
|
+
User {target['user']}
|
|
339
|
+
ProxyJump {session_name}_jumphost
|
|
340
|
+
# Connection through jumphost
|
|
341
|
+
|
|
342
|
+
# Direct access to target (requires being on the same network)
|
|
343
|
+
Host {session_name}_direct
|
|
344
|
+
HostName {target['ip']}
|
|
345
|
+
User {target['user']}
|
|
346
|
+
# Only use this if you have direct network access
|
|
347
|
+
|
|
348
|
+
# Quick commands
|
|
349
|
+
# ssh {session_name}_jumphost # Connect to jumphost
|
|
350
|
+
# ssh {session_name} # Connect to target via jumphost
|
|
351
|
+
"""
|
|
352
|
+
return config.strip()
|
|
353
|
+
|
|
354
|
+
def test_connection(self, session_name: str, max_hops: int = 2) -> Dict[str, bool]:
|
|
355
|
+
"""Test connection through all hops"""
|
|
356
|
+
if session_name not in self.sessions:
|
|
357
|
+
raise KeyError(f"Session '{session_name}' not found")
|
|
358
|
+
|
|
359
|
+
session = self.sessions[session_name]
|
|
360
|
+
results = {}
|
|
361
|
+
|
|
362
|
+
print(f"\n🔍 Testing connection for '{session_name}'...")
|
|
363
|
+
|
|
364
|
+
# Test jumphost connection
|
|
365
|
+
jh = session["jumphost"]
|
|
366
|
+
print(f"\n1. Testing jumphost: {jh['user']}@{jh['ip']}")
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
client = paramiko.SSHClient()
|
|
370
|
+
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
371
|
+
|
|
372
|
+
# Load private key
|
|
373
|
+
key = self._load_private_key(jh['private_key'])
|
|
374
|
+
|
|
375
|
+
client.connect(
|
|
376
|
+
hostname=jh['ip'],
|
|
377
|
+
username=jh['user'],
|
|
378
|
+
pkey=key,
|
|
379
|
+
timeout=10
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
# Test basic command
|
|
383
|
+
stdin, stdout, stderr = client.exec_command('echo "Jumphost connection successful"')
|
|
384
|
+
output = stdout.read().decode().strip()
|
|
385
|
+
|
|
386
|
+
if "successful" in output:
|
|
387
|
+
print(f" ✓ Jumphost connection successful")
|
|
388
|
+
results['jumphost'] = True
|
|
389
|
+
|
|
390
|
+
# Test target connection via jumphost
|
|
391
|
+
if max_hops > 1:
|
|
392
|
+
print(f"\n2. Testing target via jumphost: {session['target']['user']}@{session['target']['ip']}")
|
|
393
|
+
|
|
394
|
+
# Use agent forwarding if configured
|
|
395
|
+
if session.get('agent_forwarding', True):
|
|
396
|
+
print(" Using agent forwarding...")
|
|
397
|
+
|
|
398
|
+
# Try to execute command on target via jumphost
|
|
399
|
+
# This is simplified - in reality you'd need to tunnel through
|
|
400
|
+
cmd = f"ssh -o ConnectTimeout=5 {session['target']['user']}@{session['target']['ip']} 'echo Target-connection-successful'"
|
|
401
|
+
stdin, stdout, stderr = client.exec_command(cmd)
|
|
402
|
+
|
|
403
|
+
stdout_text = stdout.read().decode().strip()
|
|
404
|
+
stderr_text = stderr.read().decode().strip()
|
|
405
|
+
|
|
406
|
+
if "Target-connection-successful" in stdout_text:
|
|
407
|
+
print(f" ✓ Target connection via jumphost successful")
|
|
408
|
+
results['target_via_jumphost'] = True
|
|
409
|
+
else:
|
|
410
|
+
print(f" ✗ Target connection failed: {stderr_text}")
|
|
411
|
+
results['target_via_jumphost'] = False
|
|
412
|
+
else:
|
|
413
|
+
print(f" ✗ Jumphost connection failed")
|
|
414
|
+
results['jumphost'] = False
|
|
415
|
+
|
|
416
|
+
client.close()
|
|
417
|
+
|
|
418
|
+
except Exception as e:
|
|
419
|
+
print(f" ✗ Connection error: {e}")
|
|
420
|
+
results['jumphost'] = False
|
|
421
|
+
|
|
422
|
+
# Update usage stats
|
|
423
|
+
session['last_used'] = datetime.now().isoformat()
|
|
424
|
+
session['usage_count'] = session.get('usage_count', 0) + 1
|
|
425
|
+
self.save_sessions()
|
|
426
|
+
|
|
427
|
+
return results
|
|
428
|
+
|
|
429
|
+
def _load_private_key(self, key_path: str) -> Optional[paramiko.PKey]:
|
|
430
|
+
"""Load private key from file"""
|
|
431
|
+
try:
|
|
432
|
+
key_path = Path(key_path).expanduser()
|
|
433
|
+
|
|
434
|
+
# Try different key types
|
|
435
|
+
for key_class in [paramiko.RSAKey, paramiko.Ed25519Key, paramiko.ECDSAKey, paramiko.DSSKey]:
|
|
436
|
+
try:
|
|
437
|
+
return key_class.from_private_key_file(str(key_path))
|
|
438
|
+
except paramiko.SSHException:
|
|
439
|
+
continue
|
|
440
|
+
|
|
441
|
+
print(f"✗ Could not load private key: {key_path}")
|
|
442
|
+
return None
|
|
443
|
+
|
|
444
|
+
except Exception as e:
|
|
445
|
+
print(f"✗ Error loading key: {e}")
|
|
446
|
+
return None
|
|
447
|
+
|
|
448
|
+
def setup_agent_forwarding(self, session_name: str) -> bool:
|
|
449
|
+
"""Setup and test SSH agent forwarding"""
|
|
450
|
+
if session_name not in self.sessions:
|
|
451
|
+
raise KeyError(f"Session '{session_name}' not found")
|
|
452
|
+
|
|
453
|
+
session = self.sessions[session_name]
|
|
454
|
+
|
|
455
|
+
print(f"\n🔧 Setting up agent forwarding for '{session_name}'...")
|
|
456
|
+
|
|
457
|
+
# Check if SSH agent is running
|
|
458
|
+
agent_socket = os.environ.get('SSH_AUTH_SOCK')
|
|
459
|
+
if not agent_socket:
|
|
460
|
+
print("⚠ SSH agent is not running. Starting SSH agent...")
|
|
461
|
+
subprocess.run(['ssh-agent'], shell=True)
|
|
462
|
+
print("⚠ Please run: eval $(ssh-agent)")
|
|
463
|
+
return False
|
|
464
|
+
|
|
465
|
+
# Add key to agent if needed
|
|
466
|
+
key_path = session['jumphost']['private_key']
|
|
467
|
+
if self.key_manager.add_key_to_agent(Path(key_path).expanduser()):
|
|
468
|
+
print("✓ Key added to SSH agent")
|
|
469
|
+
else:
|
|
470
|
+
print("⚠ Could not add key to agent")
|
|
471
|
+
|
|
472
|
+
# Test agent forwarding
|
|
473
|
+
print("\nTesting agent forwarding...")
|
|
474
|
+
cmd = ['ssh', '-A',
|
|
475
|
+
f"{session['jumphost']['user']}@{session['jumphost']['ip']}",
|
|
476
|
+
'-i', key_path,
|
|
477
|
+
'ssh-add -l && echo "Agent-forwarding-successful"']
|
|
478
|
+
|
|
479
|
+
try:
|
|
480
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
|
481
|
+
|
|
482
|
+
if "Agent-forwarding-successful" in result.stdout:
|
|
483
|
+
print("✓ SSH agent forwarding is working correctly")
|
|
484
|
+
print(f" Keys available on jumphost: {result.stdout.strip()}")
|
|
485
|
+
|
|
486
|
+
# Update session
|
|
487
|
+
session['agent_forwarding'] = True
|
|
488
|
+
session['agent_forwarding_tested'] = datetime.now().isoformat()
|
|
489
|
+
self.save_sessions()
|
|
490
|
+
|
|
491
|
+
return True
|
|
492
|
+
else:
|
|
493
|
+
print("✗ Agent forwarding test failed")
|
|
494
|
+
print(f" Output: {result.stderr}")
|
|
495
|
+
return False
|
|
496
|
+
|
|
497
|
+
except subprocess.TimeoutExpired:
|
|
498
|
+
print("✗ Connection timeout")
|
|
499
|
+
return False
|
|
500
|
+
except Exception as e:
|
|
501
|
+
print(f"✗ Error: {e}")
|
|
502
|
+
return False
|
|
503
|
+
|
|
504
|
+
def create_tunnel(self, session_name: str, local_port: int,
|
|
505
|
+
remote_host: str, remote_port: int,
|
|
506
|
+
tunnel_name: str = None) -> Dict:
|
|
507
|
+
"""Create SSH tunnel configuration"""
|
|
508
|
+
if session_name not in self.sessions:
|
|
509
|
+
raise KeyError(f"Session '{session_name}' not found")
|
|
510
|
+
|
|
511
|
+
session = self.sessions[session_name]
|
|
512
|
+
|
|
513
|
+
if not tunnel_name:
|
|
514
|
+
tunnel_name = f"{session_name}_tunnel_{local_port}_{remote_port}"
|
|
515
|
+
|
|
516
|
+
tunnel_config = {
|
|
517
|
+
"name": tunnel_name,
|
|
518
|
+
"session": session_name,
|
|
519
|
+
"local_port": local_port,
|
|
520
|
+
"remote_host": remote_host,
|
|
521
|
+
"remote_port": remote_port,
|
|
522
|
+
"type": "local_forward",
|
|
523
|
+
"command": self._generate_tunnel_command(session, local_port, remote_host, remote_port),
|
|
524
|
+
"status": "stopped",
|
|
525
|
+
"created": datetime.now().isoformat(),
|
|
526
|
+
"pid": None
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
self.tunnels[tunnel_name] = tunnel_config
|
|
530
|
+
self.save_tunnels()
|
|
531
|
+
|
|
532
|
+
print(f"✓ Created tunnel '{tunnel_name}':")
|
|
533
|
+
print(f" Local: localhost:{local_port}")
|
|
534
|
+
print(f" Remote: {remote_host}:{remote_port}")
|
|
535
|
+
print(f" Command: {tunnel_config['command']}")
|
|
536
|
+
|
|
537
|
+
return tunnel_config
|
|
538
|
+
|
|
539
|
+
def _generate_tunnel_command(self, session: Dict, local_port: int,
|
|
540
|
+
remote_host: str, remote_port: int) -> str:
|
|
541
|
+
"""Generate SSH tunnel command"""
|
|
542
|
+
jh = session["jumphost"]
|
|
543
|
+
|
|
544
|
+
cmd = [
|
|
545
|
+
"ssh", "-N", "-L",
|
|
546
|
+
f"{local_port}:{remote_host}:{remote_port}",
|
|
547
|
+
"-i", jh['private_key'],
|
|
548
|
+
f"{jh['user']}@{jh['ip']}"
|
|
549
|
+
]
|
|
550
|
+
|
|
551
|
+
if session.get('agent_forwarding', True):
|
|
552
|
+
cmd.insert(2, "-A")
|
|
553
|
+
|
|
554
|
+
return " ".join(cmd)
|
|
555
|
+
|
|
556
|
+
def start_tunnel(self, tunnel_name: str, background: bool = True) -> bool:
|
|
557
|
+
"""Start SSH tunnel"""
|
|
558
|
+
if tunnel_name not in self.tunnels:
|
|
559
|
+
raise KeyError(f"Tunnel '{tunnel_name}' not found")
|
|
560
|
+
|
|
561
|
+
tunnel = self.tunnels[tunnel_name]
|
|
562
|
+
|
|
563
|
+
print(f"\nStarting tunnel '{tunnel_name}'...")
|
|
564
|
+
print(f"Command: {tunnel['command']}")
|
|
565
|
+
|
|
566
|
+
try:
|
|
567
|
+
if background:
|
|
568
|
+
# Start in background
|
|
569
|
+
process = subprocess.Popen(
|
|
570
|
+
tunnel['command'].split(),
|
|
571
|
+
stdout=subprocess.DEVNULL,
|
|
572
|
+
stderr=subprocess.PIPE,
|
|
573
|
+
start_new_session=True
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
# Give it a moment to start
|
|
577
|
+
time.sleep(2)
|
|
578
|
+
|
|
579
|
+
if process.poll() is None:
|
|
580
|
+
tunnel['pid'] = process.pid
|
|
581
|
+
tunnel['status'] = 'running'
|
|
582
|
+
tunnel['started'] = datetime.now().isoformat()
|
|
583
|
+
self.save_tunnels()
|
|
584
|
+
|
|
585
|
+
print(f"✓ Tunnel started in background (PID: {process.pid})")
|
|
586
|
+
print(f" Check status with: ssh2ls tunnel status {tunnel_name}")
|
|
587
|
+
return True
|
|
588
|
+
else:
|
|
589
|
+
stderr = process.stderr.read().decode() if process.stderr else ""
|
|
590
|
+
print(f"✗ Tunnel failed to start: {stderr}")
|
|
591
|
+
return False
|
|
592
|
+
else:
|
|
593
|
+
# Run in foreground
|
|
594
|
+
print("\nRunning tunnel in foreground (Ctrl+C to stop)...")
|
|
595
|
+
subprocess.run(tunnel['command'].split())
|
|
596
|
+
return True
|
|
597
|
+
|
|
598
|
+
except Exception as e:
|
|
599
|
+
print(f"✗ Error starting tunnel: {e}")
|
|
600
|
+
return False
|
|
601
|
+
|
|
602
|
+
def stop_tunnel(self, tunnel_name: str) -> bool:
|
|
603
|
+
"""Stop SSH tunnel"""
|
|
604
|
+
if tunnel_name not in self.tunnels:
|
|
605
|
+
raise KeyError(f"Tunnel '{tunnel_name}' not found")
|
|
606
|
+
|
|
607
|
+
tunnel = self.tunnels[tunnel_name]
|
|
608
|
+
|
|
609
|
+
if tunnel['status'] != 'running' or not tunnel.get('pid'):
|
|
610
|
+
print(f"Tunnel '{tunnel_name}' is not running")
|
|
611
|
+
return False
|
|
612
|
+
|
|
613
|
+
try:
|
|
614
|
+
print(f"\n🛑 Stopping tunnel '{tunnel_name}' (PID: {tunnel['pid']})...")
|
|
615
|
+
|
|
616
|
+
# Try graceful termination
|
|
617
|
+
try:
|
|
618
|
+
os.kill(tunnel['pid'], 15) # SIGTERM
|
|
619
|
+
time.sleep(1)
|
|
620
|
+
|
|
621
|
+
# Check if still running
|
|
622
|
+
try:
|
|
623
|
+
os.kill(tunnel['pid'], 0)
|
|
624
|
+
# Still running, force kill
|
|
625
|
+
os.kill(tunnel['pid'], 9) # SIGKILL
|
|
626
|
+
print("Force killed tunnel")
|
|
627
|
+
except OSError:
|
|
628
|
+
# Process terminated
|
|
629
|
+
pass
|
|
630
|
+
|
|
631
|
+
except ProcessLookupError:
|
|
632
|
+
# Process already dead
|
|
633
|
+
pass
|
|
634
|
+
|
|
635
|
+
tunnel['status'] = 'stopped'
|
|
636
|
+
tunnel['stopped'] = datetime.now().isoformat()
|
|
637
|
+
tunnel['pid'] = None
|
|
638
|
+
self.save_tunnels()
|
|
639
|
+
|
|
640
|
+
print(f"✓ Tunnel stopped")
|
|
641
|
+
return True
|
|
642
|
+
|
|
643
|
+
except Exception as e:
|
|
644
|
+
print(f"✗ Error stopping tunnel: {e}")
|
|
645
|
+
return False
|
|
646
|
+
|
|
647
|
+
def transfer_files(self, session_name: str, local_path: str,
|
|
648
|
+
remote_path: str, direction: str = "upload") -> bool:
|
|
649
|
+
"""
|
|
650
|
+
Transfer files through jumphost
|
|
651
|
+
direction: 'upload' (local→remote) or 'download' (remote→local)
|
|
652
|
+
"""
|
|
653
|
+
if session_name not in self.sessions:
|
|
654
|
+
raise KeyError(f"Session '{session_name}' not found")
|
|
655
|
+
|
|
656
|
+
session = self.sessions[session_name]
|
|
657
|
+
jh = session["jumphost"]
|
|
658
|
+
target = session["target"]
|
|
659
|
+
|
|
660
|
+
print(f"\nTransferring files for '{session_name}'...")
|
|
661
|
+
print(f"Direction: {direction}")
|
|
662
|
+
print(f"Local: {local_path}")
|
|
663
|
+
print(f"Remote: {remote_path}")
|
|
664
|
+
|
|
665
|
+
# Build SCP command with ProxyJump
|
|
666
|
+
if direction == "upload":
|
|
667
|
+
src = local_path
|
|
668
|
+
dst = f"{target['user']}@{target['ip']}:{remote_path}"
|
|
669
|
+
else: # download
|
|
670
|
+
src = f"{target['user']}@{target['ip']}:{remote_path}"
|
|
671
|
+
dst = local_path
|
|
672
|
+
|
|
673
|
+
cmd = [
|
|
674
|
+
"scp", "-r",
|
|
675
|
+
"-o", f"ProxyJump={jh['user']}@{jh['ip']}",
|
|
676
|
+
"-i", jh['private_key']
|
|
677
|
+
]
|
|
678
|
+
|
|
679
|
+
if session.get('agent_forwarding', True):
|
|
680
|
+
cmd.extend(["-o", "ForwardAgent=yes"])
|
|
681
|
+
|
|
682
|
+
cmd.extend([src, dst])
|
|
683
|
+
|
|
684
|
+
print(f"\nCommand: {' '.join(cmd)}")
|
|
685
|
+
|
|
686
|
+
try:
|
|
687
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
688
|
+
|
|
689
|
+
if result.returncode == 0:
|
|
690
|
+
print(f"✓ File transfer successful")
|
|
691
|
+
return True
|
|
692
|
+
else:
|
|
693
|
+
print(f"✗ File transfer failed:")
|
|
694
|
+
print(f" Error: {result.stderr}")
|
|
695
|
+
return False
|
|
696
|
+
|
|
697
|
+
except Exception as e:
|
|
698
|
+
print(f"✗ Error during file transfer: {e}")
|
|
699
|
+
return False
|
|
700
|
+
|
|
701
|
+
def interactive_shell(self, session_name: str, hop: str = "target") -> bool:
|
|
702
|
+
"""
|
|
703
|
+
Start interactive SSH shell
|
|
704
|
+
hop: 'jumphost' or 'target'
|
|
705
|
+
"""
|
|
706
|
+
if session_name not in self.sessions:
|
|
707
|
+
raise KeyError(f"Session '{session_name}' not found")
|
|
708
|
+
|
|
709
|
+
session = self.sessions[session_name]
|
|
710
|
+
|
|
711
|
+
if hop == "jumphost":
|
|
712
|
+
host = session["jumphost"]
|
|
713
|
+
ssh_cmd = [
|
|
714
|
+
"ssh", "-i", host["private_key"],
|
|
715
|
+
f"{host['user']}@{host['ip']}"
|
|
716
|
+
]
|
|
717
|
+
|
|
718
|
+
if session.get('agent_forwarding', True):
|
|
719
|
+
ssh_cmd.insert(1, "-A")
|
|
720
|
+
|
|
721
|
+
else: # target via jumphost
|
|
722
|
+
host = session["target"]
|
|
723
|
+
jh = session["jumphost"]
|
|
724
|
+
|
|
725
|
+
ssh_cmd = [
|
|
726
|
+
"ssh", "-J", f"{jh['user']}@{jh['ip']}",
|
|
727
|
+
"-i", jh["private_key"],
|
|
728
|
+
f"{host['user']}@{host['ip']}"
|
|
729
|
+
]
|
|
730
|
+
|
|
731
|
+
if session.get('agent_forwarding', True):
|
|
732
|
+
ssh_cmd.insert(1, "-A")
|
|
733
|
+
|
|
734
|
+
print(f"\nStarting interactive SSH session to {hop}...")
|
|
735
|
+
print(f"Command: {' '.join(ssh_cmd)}")
|
|
736
|
+
print("(Press Ctrl+D or type 'exit' to disconnect)")
|
|
737
|
+
|
|
738
|
+
try:
|
|
739
|
+
# Update usage stats
|
|
740
|
+
session['last_used'] = datetime.now().isoformat()
|
|
741
|
+
session['usage_count'] = session.get('usage_count', 0) + 1
|
|
742
|
+
self.save_sessions()
|
|
743
|
+
|
|
744
|
+
# Start interactive session
|
|
745
|
+
subprocess.run(ssh_cmd)
|
|
746
|
+
return True
|
|
747
|
+
|
|
748
|
+
except KeyboardInterrupt:
|
|
749
|
+
print("\nSession interrupted")
|
|
750
|
+
return False
|
|
751
|
+
except Exception as e:
|
|
752
|
+
print(f"✗ Error starting session: {e}")
|
|
753
|
+
return False
|
|
754
|
+
|
|
755
|
+
def batch_setup_environment(self, session_name: str, commands: List[str]) -> bool:
|
|
756
|
+
"""
|
|
757
|
+
Execute batch commands on target through jumphost
|
|
758
|
+
Useful for setting up environments (like your nf-core setup)
|
|
759
|
+
"""
|
|
760
|
+
if session_name not in self.sessions:
|
|
761
|
+
raise KeyError(f"Session '{session_name}' not found")
|
|
762
|
+
|
|
763
|
+
session = self.sessions[session_name]
|
|
764
|
+
jh = session["jumphost"]
|
|
765
|
+
target = session["target"]
|
|
766
|
+
|
|
767
|
+
print(f"\n⚙️ Setting up environment on target...")
|
|
768
|
+
|
|
769
|
+
# Create a temporary script with all commands
|
|
770
|
+
script_content = "#!/bin/bash\nset -e\n\n"
|
|
771
|
+
script_content += "# Environment setup script\n"
|
|
772
|
+
script_content += "# Generated by ssh2ls\n\n"
|
|
773
|
+
|
|
774
|
+
for i, cmd in enumerate(commands, 1):
|
|
775
|
+
script_content += f"echo 'Step {i}: {cmd}'\n"
|
|
776
|
+
script_content += f"{cmd}\n"
|
|
777
|
+
script_content += "echo ''\n"
|
|
778
|
+
|
|
779
|
+
script_content += "echo 'Environment setup complete!'\n"
|
|
780
|
+
|
|
781
|
+
# Save script locally
|
|
782
|
+
temp_script = tempfile.NamedTemporaryFile(mode='w', suffix='.sh', delete=False)
|
|
783
|
+
temp_script.write(script_content)
|
|
784
|
+
temp_script.close()
|
|
785
|
+
|
|
786
|
+
# Make executable
|
|
787
|
+
os.chmod(temp_script.name, 0o755)
|
|
788
|
+
|
|
789
|
+
try:
|
|
790
|
+
# Upload script
|
|
791
|
+
print("Uploading setup script...")
|
|
792
|
+
upload_success = self.transfer_files(
|
|
793
|
+
session_name=session_name,
|
|
794
|
+
local_path=temp_script.name,
|
|
795
|
+
remote_path="~/setup_env.sh",
|
|
796
|
+
direction="upload"
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
if not upload_success:
|
|
800
|
+
return False
|
|
801
|
+
|
|
802
|
+
# Execute script on target
|
|
803
|
+
print("\nExecuting setup script...")
|
|
804
|
+
|
|
805
|
+
# Build SSH command to execute script
|
|
806
|
+
ssh_cmd = [
|
|
807
|
+
"ssh", "-J", f"{jh['user']}@{jh['ip']}",
|
|
808
|
+
"-i", jh["private_key"],
|
|
809
|
+
f"{target['user']}@{target['ip']}",
|
|
810
|
+
"bash ~/setup_env.sh"
|
|
811
|
+
]
|
|
812
|
+
|
|
813
|
+
if session.get('agent_forwarding', True):
|
|
814
|
+
ssh_cmd.insert(1, "-A")
|
|
815
|
+
|
|
816
|
+
result = subprocess.run(ssh_cmd, capture_output=True, text=True)
|
|
817
|
+
|
|
818
|
+
# Clean up temporary script
|
|
819
|
+
os.unlink(temp_script.name)
|
|
820
|
+
|
|
821
|
+
if result.returncode == 0:
|
|
822
|
+
print("✓ Environment setup completed successfully")
|
|
823
|
+
print("\nOutput:")
|
|
824
|
+
print("-" * 60)
|
|
825
|
+
print(result.stdout)
|
|
826
|
+
print("-" * 60)
|
|
827
|
+
return True
|
|
828
|
+
else:
|
|
829
|
+
print("✗ Environment setup failed:")
|
|
830
|
+
print(f"Error: {result.stderr}")
|
|
831
|
+
return False
|
|
832
|
+
|
|
833
|
+
except Exception as e:
|
|
834
|
+
print(f"✗ Error during environment setup: {e}")
|
|
835
|
+
return False
|
|
836
|
+
|
|
837
|
+
def get_quick_commands(self, session_name: str) -> Dict[str, str]:
|
|
838
|
+
"""Get quick commands for common operations"""
|
|
839
|
+
if session_name not in self.sessions:
|
|
840
|
+
raise KeyError(f"Session '{session_name}' not found")
|
|
841
|
+
|
|
842
|
+
session = self.sessions[session_name]
|
|
843
|
+
jh = session["jumphost"]
|
|
844
|
+
target = session["target"]
|
|
845
|
+
|
|
846
|
+
return {
|
|
847
|
+
"connect_jumphost": f"ssh -A -i {jh['private_key']} {jh['user']}@{jh['ip']}",
|
|
848
|
+
"connect_target": f"ssh -A -i {jh['private_key']} -J {jh['user']}@{jh['ip']} {target['user']}@{target['ip']}",
|
|
849
|
+
"copy_to_target": f"scp -r -o ProxyJump={jh['user']}@{jh['ip']} -i {jh['private_key']} LOCAL_FILE {target['user']}@{target['ip']}:REMOTE_PATH",
|
|
850
|
+
"copy_from_target": f"scp -r -o ProxyJump={jh['user']}@{jh['ip']} -i {jh['private_key']} {target['user']}@{target['ip']}:REMOTE_FILE LOCAL_PATH",
|
|
851
|
+
"check_agent": "ssh-add -l",
|
|
852
|
+
"add_key": f"ssh-add {jh['private_key']}"
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
class SSH2LS:
|
|
856
|
+
"""Main SSH2LS application"""
|
|
857
|
+
|
|
858
|
+
def __init__(self):
|
|
859
|
+
self.key_manager = SSHKeyManager()
|
|
860
|
+
self.ssh_manager = MultiHopSSHManager()
|
|
861
|
+
self.config_dir = Path("~/.ssh/ssh2ls").expanduser()
|
|
862
|
+
self.config_dir.mkdir(parents=True, exist_ok=True)
|
|
863
|
+
self.instructions = InstructionDisplay()
|
|
864
|
+
def interactive_wizard(self):
|
|
865
|
+
"""Interactive wizard for setting up SSH connections"""
|
|
866
|
+
print("╔══════════════════════════════════════════════════════════════╗")
|
|
867
|
+
print("║ SSH2LS - Connection Wizard ║")
|
|
868
|
+
print("╚══════════════════════════════════════════════════════════════╝")
|
|
869
|
+
print("\nTo setup complex SSH connections.")
|
|
870
|
+
print(" Local → Jumphost (xxx.xxx.xx.xxx) → Remote VM (xxx.xxx.xx.xxx)")
|
|
871
|
+
print()
|
|
872
|
+
|
|
873
|
+
while True:
|
|
874
|
+
print("\n" + "="*70)
|
|
875
|
+
print("MAIN MENU")
|
|
876
|
+
print("="*70)
|
|
877
|
+
print("1. SSH Key Management")
|
|
878
|
+
print("2. Setup Multi-Hop Connection (like deNBI)")
|
|
879
|
+
print("3. Manage Existing Connections")
|
|
880
|
+
print("4. Transfer Files")
|
|
881
|
+
print("5. Setup Remote Environment")
|
|
882
|
+
print("6. Create SSH Tunnels")
|
|
883
|
+
print("7. Show Quick Commands")
|
|
884
|
+
print("8. Test Connections")
|
|
885
|
+
print("9. Export Configuration")
|
|
886
|
+
print("0. Exit")
|
|
887
|
+
|
|
888
|
+
choice = input("\nSelect option (0-9): ").strip()
|
|
889
|
+
|
|
890
|
+
if choice == "1":
|
|
891
|
+
self.key_management_menu()
|
|
892
|
+
elif choice == "2":
|
|
893
|
+
self.setup_multi_hop_menu()
|
|
894
|
+
elif choice == "3":
|
|
895
|
+
self.manage_connections_menu()
|
|
896
|
+
elif choice == "4":
|
|
897
|
+
self.file_transfer_menu()
|
|
898
|
+
elif choice == "5":
|
|
899
|
+
self.environment_setup_menu()
|
|
900
|
+
elif choice == "6":
|
|
901
|
+
self.tunnel_management_menu()
|
|
902
|
+
elif choice == "7":
|
|
903
|
+
self.show_quick_commands()
|
|
904
|
+
elif choice == "8":
|
|
905
|
+
self.test_connections_menu()
|
|
906
|
+
elif choice == "9":
|
|
907
|
+
self.export_configuration()
|
|
908
|
+
elif choice == "0":
|
|
909
|
+
print("\nbye!")
|
|
910
|
+
break
|
|
911
|
+
else:
|
|
912
|
+
print("Invalid option. Please try again.")
|
|
913
|
+
|
|
914
|
+
def key_management_menu(self):
|
|
915
|
+
"""SSH key management menu"""
|
|
916
|
+
print("\n" + "─"*70)
|
|
917
|
+
print("SSH KEY MANAGEMENT")
|
|
918
|
+
print("─"*70)
|
|
919
|
+
|
|
920
|
+
while True:
|
|
921
|
+
print("\nOptions:")
|
|
922
|
+
print("1. Generate new SSH key pair")
|
|
923
|
+
print("2. List keys in SSH agent")
|
|
924
|
+
print("3. Add key to SSH agent")
|
|
925
|
+
print("4. Remove key from SSH agent")
|
|
926
|
+
print("5. Find available keys")
|
|
927
|
+
print("6. Fix key permissions")
|
|
928
|
+
print("7. Extract public key from private")
|
|
929
|
+
print("8. Back to main menu")
|
|
930
|
+
|
|
931
|
+
choice = input("\nSelect option (1-8): ").strip()
|
|
932
|
+
|
|
933
|
+
if choice == "1":
|
|
934
|
+
self.generate_key_interactive()
|
|
935
|
+
elif choice == "2":
|
|
936
|
+
self.list_keys_in_agent()
|
|
937
|
+
elif choice == "3":
|
|
938
|
+
self.add_key_to_agent_interactive()
|
|
939
|
+
elif choice == "4":
|
|
940
|
+
self.remove_key_from_agent_interactive()
|
|
941
|
+
elif choice == "5":
|
|
942
|
+
self.find_available_keys()
|
|
943
|
+
elif choice == "6":
|
|
944
|
+
self.fix_key_permissions_interactive()
|
|
945
|
+
elif choice == "7":
|
|
946
|
+
self.extract_public_key()
|
|
947
|
+
elif choice == "8":
|
|
948
|
+
break
|
|
949
|
+
else:
|
|
950
|
+
print("Invalid option.")
|
|
951
|
+
|
|
952
|
+
def generate_key_interactive(self):
|
|
953
|
+
"""Interactive SSH key generation"""
|
|
954
|
+
print("\nGenerating SSH key pair...")
|
|
955
|
+
|
|
956
|
+
key_name = input("Key name [id_ed25519_denbi]: ").strip() or "id_ed25519_denbi"
|
|
957
|
+
key_type = input("Key type (rsa/ed25519/ecdsa) [ed25519]: ").strip() or "ed25519"
|
|
958
|
+
|
|
959
|
+
if key_type == "rsa":
|
|
960
|
+
key_size = input("Key size (2048/3072/4096) [4096]: ").strip() or "4096"
|
|
961
|
+
key_size = int(key_size)
|
|
962
|
+
else:
|
|
963
|
+
key_size = None
|
|
964
|
+
|
|
965
|
+
use_passphrase = input("Use passphrase? (y/N): ").strip().lower() == 'y'
|
|
966
|
+
passphrase = None
|
|
967
|
+
if use_passphrase:
|
|
968
|
+
passphrase = getpass.getpass("Passphrase: ")
|
|
969
|
+
passphrase_confirm = getpass.getpass("Confirm passphrase: ")
|
|
970
|
+
if passphrase != passphrase_confirm:
|
|
971
|
+
print("✗ Passphrases do not match!")
|
|
972
|
+
return
|
|
973
|
+
|
|
974
|
+
comment = input(f"Comment [{getpass.getuser()}@denbi]: ").strip() or f"{getpass.getuser()}@denbi"
|
|
975
|
+
|
|
976
|
+
try:
|
|
977
|
+
private_key, public_key = self.key_manager.generate_key_pair(
|
|
978
|
+
key_name=key_name,
|
|
979
|
+
key_type=key_type,
|
|
980
|
+
key_size=key_size,
|
|
981
|
+
passphrase=passphrase,
|
|
982
|
+
comment=comment
|
|
983
|
+
)
|
|
984
|
+
|
|
985
|
+
# Optionally add to agent
|
|
986
|
+
add_to_agent = input("\nAdd key to SSH agent now? (y/N): ").strip().lower() == 'y'
|
|
987
|
+
if add_to_agent:
|
|
988
|
+
self.key_manager.add_key_to_agent(private_key, passphrase)
|
|
989
|
+
|
|
990
|
+
except Exception as e:
|
|
991
|
+
print(f"✗ Error generating key: {e}")
|
|
992
|
+
|
|
993
|
+
def setup_multi_hop_menu(self):
|
|
994
|
+
"""Setup multi-hop connection like deNBI"""
|
|
995
|
+
print("\n" + "─"*70)
|
|
996
|
+
print("SETUP MULTI-HOP CONNECTION")
|
|
997
|
+
print("─"*70)
|
|
998
|
+
print("\nThis will setup a connection like your deNBI setup:")
|
|
999
|
+
print(" Your Mac → Jumphost → Target VM")
|
|
1000
|
+
print()
|
|
1001
|
+
|
|
1002
|
+
session_name = input("Connection name (e.g., 'denbi', 'production'): ").strip()
|
|
1003
|
+
if not session_name:
|
|
1004
|
+
print("✗ Connection name is required")
|
|
1005
|
+
return
|
|
1006
|
+
|
|
1007
|
+
print("\n--- Jumphost Configuration ---")
|
|
1008
|
+
jumphost_ip = input("Jumphost IP/Hostname (e.g., 193.196.20.189): ").strip()
|
|
1009
|
+
jumphost_user = input(f"Jumphost username [ubuntu]: ").strip() or "ubuntu"
|
|
1010
|
+
|
|
1011
|
+
print("\n--- SSH Key for Jumphost ---")
|
|
1012
|
+
print("1. Use existing key")
|
|
1013
|
+
print("2. Generate new key")
|
|
1014
|
+
print("3. Skip for now")
|
|
1015
|
+
|
|
1016
|
+
key_choice = input("\nSelect option (1-3): ").strip()
|
|
1017
|
+
|
|
1018
|
+
if key_choice == "1":
|
|
1019
|
+
# List available keys
|
|
1020
|
+
keys = self.key_manager.find_available_keys()
|
|
1021
|
+
if keys:
|
|
1022
|
+
print("\nAvailable keys:")
|
|
1023
|
+
for i, key in enumerate(keys, 1):
|
|
1024
|
+
print(f" {i}. {key.name}")
|
|
1025
|
+
|
|
1026
|
+
key_idx = input(f"\nSelect key (1-{len(keys)}) or enter path: ").strip()
|
|
1027
|
+
if key_idx.isdigit() and 1 <= int(key_idx) <= len(keys):
|
|
1028
|
+
private_key_path = str(keys[int(key_idx) - 1])
|
|
1029
|
+
else:
|
|
1030
|
+
private_key_path = key_idx
|
|
1031
|
+
else:
|
|
1032
|
+
print("No keys found in ~/.ssh")
|
|
1033
|
+
private_key_path = input("Path to private key: ").strip()
|
|
1034
|
+
|
|
1035
|
+
elif key_choice == "2":
|
|
1036
|
+
print("\nGenerating key for jumphost...")
|
|
1037
|
+
private_key, public_key = self.key_manager.generate_key_pair(
|
|
1038
|
+
key_name=f"id_ed25519_{session_name}",
|
|
1039
|
+
key_type="ed25519",
|
|
1040
|
+
comment=f"{getpass.getuser()}@{session_name}"
|
|
1041
|
+
)
|
|
1042
|
+
private_key_path = str(private_key)
|
|
1043
|
+
|
|
1044
|
+
print("\n⚠ IMPORTANT: Share this public key with your admin:")
|
|
1045
|
+
print("-" * 80)
|
|
1046
|
+
with open(public_key, 'r') as f:
|
|
1047
|
+
print(f.read().strip())
|
|
1048
|
+
print("-" * 80)
|
|
1049
|
+
print("\nAsk them to add it to the jumphost's authorized_keys")
|
|
1050
|
+
input("\nPress Enter when the key has been added to the jumphost...")
|
|
1051
|
+
|
|
1052
|
+
else:
|
|
1053
|
+
private_key_path = input("Path to private key: ").strip()
|
|
1054
|
+
|
|
1055
|
+
print("\n--- Target VM Configuration ---")
|
|
1056
|
+
target_ip = input("Target VM IP (e.g., 192.168.54.219): ").strip()
|
|
1057
|
+
target_user = input(f"Target username [{jumphost_user}]: ").strip() or jumphost_user
|
|
1058
|
+
|
|
1059
|
+
description = input("Description (optional): ").strip()
|
|
1060
|
+
|
|
1061
|
+
# Create the configuration
|
|
1062
|
+
try:
|
|
1063
|
+
config = self.ssh_manager.create_jumphost_config(
|
|
1064
|
+
name=session_name,
|
|
1065
|
+
jumphost_ip=jumphost_ip,
|
|
1066
|
+
jumphost_user=jumphost_user,
|
|
1067
|
+
private_key_path=private_key_path,
|
|
1068
|
+
target_ip=target_ip,
|
|
1069
|
+
target_user=target_user,
|
|
1070
|
+
description=description
|
|
1071
|
+
)
|
|
1072
|
+
|
|
1073
|
+
# Generate SSH config
|
|
1074
|
+
ssh_config = self.ssh_manager.generate_ssh_config(session_name)
|
|
1075
|
+
|
|
1076
|
+
print(f"\n✅ Configuration created!")
|
|
1077
|
+
print(f"\nSSH Configuration:")
|
|
1078
|
+
print("-" * 60)
|
|
1079
|
+
print(ssh_config)
|
|
1080
|
+
print("-" * 60)
|
|
1081
|
+
|
|
1082
|
+
# Save to SSH config file
|
|
1083
|
+
save_to_ssh = input("\nAdd to ~/.ssh/config? (y/N): ").strip().lower() == 'y'
|
|
1084
|
+
if save_to_ssh:
|
|
1085
|
+
self._append_to_ssh_config(ssh_config)
|
|
1086
|
+
print("✓ Added to ~/.ssh/config")
|
|
1087
|
+
|
|
1088
|
+
# Setup agent forwarding
|
|
1089
|
+
setup_agent = input("\nSetup SSH agent forwarding? (y/N): ").strip().lower() == 'y'
|
|
1090
|
+
if setup_agent:
|
|
1091
|
+
self.ssh_manager.setup_agent_forwarding(session_name)
|
|
1092
|
+
|
|
1093
|
+
# Test connection
|
|
1094
|
+
test_now = input("\nTest connection now? (y/N): ").strip().lower() == 'y'
|
|
1095
|
+
if test_now:
|
|
1096
|
+
self.ssh_manager.test_connection(session_name)
|
|
1097
|
+
|
|
1098
|
+
except Exception as e:
|
|
1099
|
+
print(f"✗ Error creating configuration: {e}")
|
|
1100
|
+
|
|
1101
|
+
def manage_connections_menu(self):
|
|
1102
|
+
"""Manage existing connections"""
|
|
1103
|
+
sessions = self.ssh_manager.sessions
|
|
1104
|
+
|
|
1105
|
+
if not sessions:
|
|
1106
|
+
print("No connections configured.")
|
|
1107
|
+
return
|
|
1108
|
+
|
|
1109
|
+
print("\n" + "─"*70)
|
|
1110
|
+
print("MANAGE CONNECTIONS")
|
|
1111
|
+
print("─"*70)
|
|
1112
|
+
|
|
1113
|
+
while True:
|
|
1114
|
+
print("\nConfigured connections:")
|
|
1115
|
+
for i, (name, session) in enumerate(sessions.items(), 1):
|
|
1116
|
+
jh = session["jumphost"]
|
|
1117
|
+
target = session["target"]
|
|
1118
|
+
last_used = session.get("last_used", "Never")
|
|
1119
|
+
print(f"{i:2}. {name}: {jh['user']}@{jh['ip']} → {target['user']}@{target['ip']}")
|
|
1120
|
+
print(f" Last used: {last_used}")
|
|
1121
|
+
|
|
1122
|
+
print("\nOptions:")
|
|
1123
|
+
print("1. Connect to jumphost")
|
|
1124
|
+
print("2. Connect to target via jumphost")
|
|
1125
|
+
print("3. Test connection")
|
|
1126
|
+
print("4. Edit connection")
|
|
1127
|
+
print("5. Delete connection")
|
|
1128
|
+
print("6. Show SSH config")
|
|
1129
|
+
print("7. Back to main menu")
|
|
1130
|
+
|
|
1131
|
+
choice = input("\nSelect option (1-7): ").strip()
|
|
1132
|
+
|
|
1133
|
+
if choice == "1":
|
|
1134
|
+
self._connect_to_host("jumphost")
|
|
1135
|
+
elif choice == "2":
|
|
1136
|
+
self._connect_to_host("target")
|
|
1137
|
+
elif choice == "3":
|
|
1138
|
+
self._test_specific_connection()
|
|
1139
|
+
elif choice == "4":
|
|
1140
|
+
self._edit_connection()
|
|
1141
|
+
elif choice == "5":
|
|
1142
|
+
self._delete_connection()
|
|
1143
|
+
elif choice == "6":
|
|
1144
|
+
self._show_ssh_config()
|
|
1145
|
+
elif choice == "7":
|
|
1146
|
+
break
|
|
1147
|
+
else:
|
|
1148
|
+
print("Invalid option.")
|
|
1149
|
+
|
|
1150
|
+
def _connect_to_host(self, host_type: str):
|
|
1151
|
+
"""Connect to jumphost or target"""
|
|
1152
|
+
sessions = self.ssh_manager.sessions
|
|
1153
|
+
if not sessions:
|
|
1154
|
+
print("No connections configured.")
|
|
1155
|
+
return
|
|
1156
|
+
|
|
1157
|
+
print("\nSelect connection:")
|
|
1158
|
+
names = list(sessions.keys())
|
|
1159
|
+
for i, name in enumerate(names, 1):
|
|
1160
|
+
print(f"{i}. {name}")
|
|
1161
|
+
|
|
1162
|
+
choice = input(f"\nSelect connection (1-{len(names)}): ").strip()
|
|
1163
|
+
if choice.isdigit() and 1 <= int(choice) <= len(names):
|
|
1164
|
+
session_name = names[int(choice) - 1]
|
|
1165
|
+
self.ssh_manager.interactive_shell(session_name, host_type)
|
|
1166
|
+
else:
|
|
1167
|
+
print("Invalid selection.")
|
|
1168
|
+
|
|
1169
|
+
def file_transfer_menu(self):
|
|
1170
|
+
"""File transfer menu"""
|
|
1171
|
+
sessions = self.ssh_manager.sessions
|
|
1172
|
+
|
|
1173
|
+
if not sessions:
|
|
1174
|
+
print("No connections configured.")
|
|
1175
|
+
return
|
|
1176
|
+
|
|
1177
|
+
print("\n" + "─"*70)
|
|
1178
|
+
print("FILE TRANSFER")
|
|
1179
|
+
print("─"*70)
|
|
1180
|
+
|
|
1181
|
+
print("\nSelect connection:")
|
|
1182
|
+
names = list(sessions.keys())
|
|
1183
|
+
for i, name in enumerate(names, 1):
|
|
1184
|
+
print(f"{i}. {name}")
|
|
1185
|
+
|
|
1186
|
+
choice = input(f"\nSelect connection (1-{len(names)}): ").strip()
|
|
1187
|
+
if not (choice.isdigit() and 1 <= int(choice) <= len(names)):
|
|
1188
|
+
print("Invalid selection.")
|
|
1189
|
+
return
|
|
1190
|
+
|
|
1191
|
+
session_name = names[int(choice) - 1]
|
|
1192
|
+
|
|
1193
|
+
print("\nTransfer direction:")
|
|
1194
|
+
print("1. Upload (local → remote)")
|
|
1195
|
+
print("2. Download (remote → local)")
|
|
1196
|
+
|
|
1197
|
+
direction_choice = input("\nSelect direction (1-2): ").strip()
|
|
1198
|
+
if direction_choice == "1":
|
|
1199
|
+
direction = "upload"
|
|
1200
|
+
elif direction_choice == "2":
|
|
1201
|
+
direction = "download"
|
|
1202
|
+
else:
|
|
1203
|
+
print("Invalid selection.")
|
|
1204
|
+
return
|
|
1205
|
+
|
|
1206
|
+
if direction == "upload":
|
|
1207
|
+
local_path = input("Local file/folder path: ").strip()
|
|
1208
|
+
remote_path = input("Remote destination path: ").strip()
|
|
1209
|
+
else:
|
|
1210
|
+
remote_path = input("Remote file/folder path: ").strip()
|
|
1211
|
+
local_path = input("Local destination path: ").strip()
|
|
1212
|
+
|
|
1213
|
+
print(f"\nTransferring {direction}...")
|
|
1214
|
+
success = self.ssh_manager.transfer_files(
|
|
1215
|
+
session_name=session_name,
|
|
1216
|
+
local_path=local_path,
|
|
1217
|
+
remote_path=remote_path,
|
|
1218
|
+
direction=direction
|
|
1219
|
+
)
|
|
1220
|
+
|
|
1221
|
+
if success:
|
|
1222
|
+
print("✅ Transfer completed successfully!")
|
|
1223
|
+
else:
|
|
1224
|
+
print("❌ Transfer failed.")
|
|
1225
|
+
|
|
1226
|
+
def environment_setup_menu(self):
|
|
1227
|
+
"""Setup remote environment (like nf-core setup)"""
|
|
1228
|
+
sessions = self.ssh_manager.sessions
|
|
1229
|
+
|
|
1230
|
+
if not sessions:
|
|
1231
|
+
print("No connections configured.")
|
|
1232
|
+
return
|
|
1233
|
+
|
|
1234
|
+
print("\n" + "─"*70)
|
|
1235
|
+
print("REMOTE ENVIRONMENT SETUP")
|
|
1236
|
+
print("─"*70)
|
|
1237
|
+
print("\nThis will help you setup a remote environment like your nf-core setup.")
|
|
1238
|
+
|
|
1239
|
+
print("\nSelect connection:")
|
|
1240
|
+
names = list(sessions.keys())
|
|
1241
|
+
for i, name in enumerate(names, 1):
|
|
1242
|
+
print(f"{i}. {name}")
|
|
1243
|
+
|
|
1244
|
+
choice = input(f"\nSelect connection (1-{len(names)}): ").strip()
|
|
1245
|
+
if not (choice.isdigit() and 1 <= int(choice) <= len(names)):
|
|
1246
|
+
print("Invalid selection.")
|
|
1247
|
+
return
|
|
1248
|
+
|
|
1249
|
+
session_name = names[int(choice) - 1]
|
|
1250
|
+
|
|
1251
|
+
print("\nChoose setup template:")
|
|
1252
|
+
print("1. Basic Ubuntu setup (update, install tools)")
|
|
1253
|
+
print("2. Bioinformatics/nf-core setup")
|
|
1254
|
+
print("3. Docker setup")
|
|
1255
|
+
print("4. Python development")
|
|
1256
|
+
print("5. Custom commands")
|
|
1257
|
+
|
|
1258
|
+
template_choice = input("\nSelect template (1-5): ").strip()
|
|
1259
|
+
|
|
1260
|
+
commands = []
|
|
1261
|
+
|
|
1262
|
+
if template_choice == "1":
|
|
1263
|
+
# Basic Ubuntu setup
|
|
1264
|
+
commands = [
|
|
1265
|
+
"sudo apt update",
|
|
1266
|
+
"sudo apt upgrade -y",
|
|
1267
|
+
"sudo apt install -y git wget curl python3 python3-pip",
|
|
1268
|
+
"sudo apt install -y htop ncdu tmux",
|
|
1269
|
+
"echo 'Basic setup complete!'"
|
|
1270
|
+
]
|
|
1271
|
+
|
|
1272
|
+
elif template_choice == "2":
|
|
1273
|
+
# Bioinformatics/nf-core setup
|
|
1274
|
+
commands = [
|
|
1275
|
+
"sudo apt update && sudo apt upgrade -y",
|
|
1276
|
+
"sudo apt install -y git wget curl python3 python3-pip",
|
|
1277
|
+
"# Install Apptainer (Singularity)",
|
|
1278
|
+
"sudo apt install -y apptainer",
|
|
1279
|
+
"# Create data directory",
|
|
1280
|
+
"mkdir -p ~/chipseq_data",
|
|
1281
|
+
"# Download nf-core ChIP-seq pipeline",
|
|
1282
|
+
"git clone https://github.com/nf-core/chipseq.git",
|
|
1283
|
+
"cd chipseq && git checkout 2.1.0",
|
|
1284
|
+
"echo 'nf-core setup complete!'",
|
|
1285
|
+
"echo 'Next: Copy your data and run: apptainer pull docker://nfcore/chipseq'"
|
|
1286
|
+
]
|
|
1287
|
+
|
|
1288
|
+
elif template_choice == "3":
|
|
1289
|
+
# Docker setup
|
|
1290
|
+
commands = [
|
|
1291
|
+
"sudo apt update",
|
|
1292
|
+
"sudo apt install -y docker.io",
|
|
1293
|
+
"sudo systemctl start docker",
|
|
1294
|
+
"sudo systemctl enable docker",
|
|
1295
|
+
"sudo usermod -aG docker $USER",
|
|
1296
|
+
"echo 'Docker setup complete!'",
|
|
1297
|
+
"echo 'Log out and log back in for group changes to take effect'"
|
|
1298
|
+
]
|
|
1299
|
+
|
|
1300
|
+
elif template_choice == "4":
|
|
1301
|
+
# Python development
|
|
1302
|
+
commands = [
|
|
1303
|
+
"sudo apt update",
|
|
1304
|
+
"sudo apt install -y python3-pip python3-venv",
|
|
1305
|
+
"python3 -m pip install --upgrade pip",
|
|
1306
|
+
"mkdir -p ~/projects",
|
|
1307
|
+
"echo 'Python development setup complete!'"
|
|
1308
|
+
]
|
|
1309
|
+
|
|
1310
|
+
elif template_choice == "5":
|
|
1311
|
+
# Custom commands
|
|
1312
|
+
print("\nEnter commands (one per line, empty line to finish):")
|
|
1313
|
+
while True:
|
|
1314
|
+
cmd = input("> ").strip()
|
|
1315
|
+
if not cmd:
|
|
1316
|
+
break
|
|
1317
|
+
commands.append(cmd)
|
|
1318
|
+
|
|
1319
|
+
else:
|
|
1320
|
+
print("Invalid selection.")
|
|
1321
|
+
return
|
|
1322
|
+
|
|
1323
|
+
if not commands:
|
|
1324
|
+
print("No commands to execute.")
|
|
1325
|
+
return
|
|
1326
|
+
|
|
1327
|
+
print("\nCommands to execute:")
|
|
1328
|
+
for i, cmd in enumerate(commands, 1):
|
|
1329
|
+
print(f"{i:2}. {cmd}")
|
|
1330
|
+
|
|
1331
|
+
confirm = input("\nExecute these commands on the remote server? (y/N): ").strip().lower()
|
|
1332
|
+
if confirm != 'y':
|
|
1333
|
+
print("Cancelled.")
|
|
1334
|
+
return
|
|
1335
|
+
|
|
1336
|
+
print("\nStarting environment setup...")
|
|
1337
|
+
success = self.ssh_manager.batch_setup_environment(session_name, commands)
|
|
1338
|
+
|
|
1339
|
+
if success:
|
|
1340
|
+
print("✅ Environment setup completed!")
|
|
1341
|
+
else:
|
|
1342
|
+
print("❌ Environment setup failed.")
|
|
1343
|
+
|
|
1344
|
+
def tunnel_management_menu(self):
|
|
1345
|
+
"""SSH tunnel management menu"""
|
|
1346
|
+
print("\n" + "─"*70)
|
|
1347
|
+
print("SSH TUNNEL MANAGEMENT")
|
|
1348
|
+
print("─"*70)
|
|
1349
|
+
|
|
1350
|
+
while True:
|
|
1351
|
+
tunnels = self.ssh_manager.tunnels
|
|
1352
|
+
|
|
1353
|
+
if tunnels:
|
|
1354
|
+
print("\nActive tunnels:")
|
|
1355
|
+
for name, tunnel in tunnels.items():
|
|
1356
|
+
status = "🟢" if tunnel['status'] == 'running' else "🔴"
|
|
1357
|
+
print(f"{status} {name}: localhost:{tunnel['local_port']} → {tunnel['remote_host']}:{tunnel['remote_port']}")
|
|
1358
|
+
else:
|
|
1359
|
+
print("\nNo tunnels configured.")
|
|
1360
|
+
|
|
1361
|
+
print("\nOptions:")
|
|
1362
|
+
print("1. Create new tunnel")
|
|
1363
|
+
print("2. Start tunnel")
|
|
1364
|
+
print("3. Stop tunnel")
|
|
1365
|
+
print("4. View tunnel details")
|
|
1366
|
+
print("5. Delete tunnel")
|
|
1367
|
+
print("6. Back to main menu")
|
|
1368
|
+
|
|
1369
|
+
choice = input("\nSelect option (1-6): ").strip()
|
|
1370
|
+
|
|
1371
|
+
if choice == "1":
|
|
1372
|
+
self._create_tunnel_interactive()
|
|
1373
|
+
elif choice == "2":
|
|
1374
|
+
self._start_tunnel_interactive()
|
|
1375
|
+
elif choice == "3":
|
|
1376
|
+
self._stop_tunnel_interactive()
|
|
1377
|
+
elif choice == "4":
|
|
1378
|
+
self._view_tunnel_details()
|
|
1379
|
+
elif choice == "5":
|
|
1380
|
+
self._delete_tunnel()
|
|
1381
|
+
elif choice == "6":
|
|
1382
|
+
break
|
|
1383
|
+
else:
|
|
1384
|
+
print("Invalid option.")
|
|
1385
|
+
|
|
1386
|
+
def _create_tunnel_interactive(self):
|
|
1387
|
+
"""Create tunnel interactively"""
|
|
1388
|
+
sessions = self.ssh_manager.sessions
|
|
1389
|
+
|
|
1390
|
+
if not sessions:
|
|
1391
|
+
print("No connections configured. Create a connection first.")
|
|
1392
|
+
return
|
|
1393
|
+
|
|
1394
|
+
print("\nSelect connection:")
|
|
1395
|
+
names = list(sessions.keys())
|
|
1396
|
+
for i, name in enumerate(names, 1):
|
|
1397
|
+
print(f"{i}. {name}")
|
|
1398
|
+
|
|
1399
|
+
choice = input(f"\nSelect connection (1-{len(names)}): ").strip()
|
|
1400
|
+
if not (choice.isdigit() and 1 <= int(choice) <= len(names)):
|
|
1401
|
+
print("Invalid selection.")
|
|
1402
|
+
return
|
|
1403
|
+
|
|
1404
|
+
session_name = names[int(choice) - 1]
|
|
1405
|
+
|
|
1406
|
+
print("\nTunnel configuration:")
|
|
1407
|
+
local_port = input("Local port (e.g., 8080): ").strip()
|
|
1408
|
+
remote_host = input("Remote host (e.g., localhost or 192.168.1.100): ").strip()
|
|
1409
|
+
remote_port = input("Remote port (e.g., 80): ").strip()
|
|
1410
|
+
tunnel_name = input("Tunnel name (optional): ").strip()
|
|
1411
|
+
|
|
1412
|
+
try:
|
|
1413
|
+
local_port = int(local_port)
|
|
1414
|
+
remote_port = int(remote_port)
|
|
1415
|
+
|
|
1416
|
+
tunnel = self.ssh_manager.create_tunnel(
|
|
1417
|
+
session_name=session_name,
|
|
1418
|
+
local_port=local_port,
|
|
1419
|
+
remote_host=remote_host,
|
|
1420
|
+
remote_port=remote_port,
|
|
1421
|
+
tunnel_name=tunnel_name
|
|
1422
|
+
)
|
|
1423
|
+
|
|
1424
|
+
start_now = input("\nStart tunnel now? (y/N): ").strip().lower() == 'y'
|
|
1425
|
+
if start_now:
|
|
1426
|
+
background = input("Run in background? (y/N): ").strip().lower() == 'y'
|
|
1427
|
+
self.ssh_manager.start_tunnel(tunnel['name'], background)
|
|
1428
|
+
|
|
1429
|
+
except ValueError:
|
|
1430
|
+
print("✗ Ports must be numbers.")
|
|
1431
|
+
except Exception as e:
|
|
1432
|
+
print(f"✗ Error creating tunnel: {e}")
|
|
1433
|
+
|
|
1434
|
+
def show_quick_commands(self):
|
|
1435
|
+
"""Show quick commands for a connection"""
|
|
1436
|
+
sessions = self.ssh_manager.sessions
|
|
1437
|
+
|
|
1438
|
+
if not sessions:
|
|
1439
|
+
print("No connections configured.")
|
|
1440
|
+
return
|
|
1441
|
+
|
|
1442
|
+
print("\n" + "─"*70)
|
|
1443
|
+
print("QUICK COMMANDS")
|
|
1444
|
+
print("─"*70)
|
|
1445
|
+
|
|
1446
|
+
print("\nSelect connection:")
|
|
1447
|
+
names = list(sessions.keys())
|
|
1448
|
+
for i, name in enumerate(names, 1):
|
|
1449
|
+
print(f"{i}. {name}")
|
|
1450
|
+
|
|
1451
|
+
choice = input(f"\nSelect connection (1-{len(names)}): ").strip()
|
|
1452
|
+
if not (choice.isdigit() and 1 <= int(choice) <= len(names)):
|
|
1453
|
+
print("Invalid selection.")
|
|
1454
|
+
return
|
|
1455
|
+
|
|
1456
|
+
session_name = names[int(choice) - 1]
|
|
1457
|
+
|
|
1458
|
+
commands = self.ssh_manager.get_quick_commands(session_name)
|
|
1459
|
+
|
|
1460
|
+
print(f"\nQuick commands for '{session_name}':")
|
|
1461
|
+
print("-" * 70)
|
|
1462
|
+
for desc, cmd in commands.items():
|
|
1463
|
+
print(f"\n{desc.replace('_', ' ').title()}:")
|
|
1464
|
+
print(f" {cmd}")
|
|
1465
|
+
print("-" * 70)
|
|
1466
|
+
|
|
1467
|
+
# Copy to clipboard option
|
|
1468
|
+
if platform.system() == "Darwin": # macOS
|
|
1469
|
+
copy_all = input("\nCopy all commands to clipboard? (y/N): ").strip().lower()
|
|
1470
|
+
if copy_all == 'y':
|
|
1471
|
+
all_cmds = "\n".join([f"# {k}\n{v}\n" for k, v in commands.items()])
|
|
1472
|
+
subprocess.run(['pbcopy'], input=all_cmds.encode())
|
|
1473
|
+
print("✓ Commands copied to clipboard!")
|
|
1474
|
+
|
|
1475
|
+
def _append_to_ssh_config(self, config_text: str):
|
|
1476
|
+
"""Append configuration to ~/.ssh/config"""
|
|
1477
|
+
ssh_config_path = Path("~/.ssh/config").expanduser()
|
|
1478
|
+
ssh_config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1479
|
+
|
|
1480
|
+
# Backup existing config
|
|
1481
|
+
if ssh_config_path.exists():
|
|
1482
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
1483
|
+
backup_path = ssh_config_path.with_name(f"config.backup.{timestamp}")
|
|
1484
|
+
shutil.copy2(ssh_config_path, backup_path)
|
|
1485
|
+
print(f"✓ Backup created: {backup_path}")
|
|
1486
|
+
|
|
1487
|
+
# Append new config
|
|
1488
|
+
with open(ssh_config_path, 'a') as f:
|
|
1489
|
+
f.write(f"\n\n{config_text}\n")
|
|
1490
|
+
|
|
1491
|
+
def run_from_cli(self):
|
|
1492
|
+
"""Run from command line arguments"""
|
|
1493
|
+
parser = argparse.ArgumentParser(
|
|
1494
|
+
description="SSH2LS - Ultimate SSH Multi-Hop Manager",
|
|
1495
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
1496
|
+
epilog=textwrap.dedent("""
|
|
1497
|
+
Examples:
|
|
1498
|
+
%(prog)s wizard # Interactive wizard
|
|
1499
|
+
%(prog)s setup denbi # Setup deNBI-like connection
|
|
1500
|
+
%(prog)s connect denbi # Connect to target via jumphost
|
|
1501
|
+
%(prog)s tunnel create # Create SSH tunnel
|
|
1502
|
+
%(prog)s transfer upload denbi # Upload files
|
|
1503
|
+
%(prog)s setup-env denbi # Setup remote environment
|
|
1504
|
+
%(prog)s quick denbi # Show quick commands
|
|
1505
|
+
%(prog)s display # Display detailed instructions
|
|
1506
|
+
%(prog)s display --denbi # deNBI-specific instructions
|
|
1507
|
+
%(prog)s display --cheatsheet # Quick cheatsheet
|
|
1508
|
+
""")
|
|
1509
|
+
)
|
|
1510
|
+
|
|
1511
|
+
subparsers = parser.add_subparsers(dest='command', help='Command')
|
|
1512
|
+
|
|
1513
|
+
# Wizard command
|
|
1514
|
+
subparsers.add_parser('wizard', help='Interactive wizard')
|
|
1515
|
+
|
|
1516
|
+
# Setup command
|
|
1517
|
+
setup_parser = subparsers.add_parser('setup', help='Setup multi-hop connection')
|
|
1518
|
+
setup_parser.add_argument('name', help='Connection name')
|
|
1519
|
+
setup_parser.add_argument('--jumphost', required=True, help='Jumphost IP')
|
|
1520
|
+
setup_parser.add_argument('--jumphost-user', default='ubuntu', help='Jumphost username')
|
|
1521
|
+
setup_parser.add_argument('--target', required=True, help='Target IP')
|
|
1522
|
+
setup_parser.add_argument('--target-user', help='Target username')
|
|
1523
|
+
setup_parser.add_argument('--key', help='Private key path')
|
|
1524
|
+
|
|
1525
|
+
# Connect command
|
|
1526
|
+
connect_parser = subparsers.add_parser('connect', help='Connect to host')
|
|
1527
|
+
connect_parser.add_argument('name', help='Connection name')
|
|
1528
|
+
connect_parser.add_argument('--hop', choices=['jumphost', 'target'],
|
|
1529
|
+
default='target', help='Which hop to connect to')
|
|
1530
|
+
|
|
1531
|
+
# Transfer command
|
|
1532
|
+
transfer_parser = subparsers.add_parser('transfer', help='Transfer files')
|
|
1533
|
+
transfer_parser.add_argument('direction', choices=['upload', 'download'])
|
|
1534
|
+
transfer_parser.add_argument('name', help='Connection name')
|
|
1535
|
+
transfer_parser.add_argument('source', help='Source path')
|
|
1536
|
+
transfer_parser.add_argument('dest', help='Destination path')
|
|
1537
|
+
|
|
1538
|
+
# Tunnel command
|
|
1539
|
+
tunnel_parser = subparsers.add_parser('tunnel', help='Manage tunnels')
|
|
1540
|
+
tunnel_subparsers = tunnel_parser.add_subparsers(dest='tunnel_command')
|
|
1541
|
+
|
|
1542
|
+
tunnel_create = tunnel_subparsers.add_parser('create', help='Create tunnel')
|
|
1543
|
+
tunnel_create.add_argument('name', help='Connection name')
|
|
1544
|
+
tunnel_create.add_argument('local_port', type=int, help='Local port')
|
|
1545
|
+
tunnel_create.add_argument('remote_host', help='Remote host')
|
|
1546
|
+
tunnel_create.add_argument('remote_port', type=int, help='Remote port')
|
|
1547
|
+
tunnel_create.add_argument('--tunnel-name', help='Tunnel name')
|
|
1548
|
+
|
|
1549
|
+
tunnel_start = tunnel_subparsers.add_parser('start', help='Start tunnel')
|
|
1550
|
+
tunnel_start.add_argument('tunnel_name', help='Tunnel name')
|
|
1551
|
+
tunnel_start.add_argument('--background', action='store_true', help='Run in background')
|
|
1552
|
+
|
|
1553
|
+
tunnel_stop = tunnel_subparsers.add_parser('stop', help='Stop tunnel')
|
|
1554
|
+
tunnel_stop.add_argument('tunnel_name', help='Tunnel name')
|
|
1555
|
+
|
|
1556
|
+
# Setup environment command
|
|
1557
|
+
env_parser = subparsers.add_parser('setup-env', help='Setup remote environment')
|
|
1558
|
+
env_parser.add_argument('name', help='Connection name')
|
|
1559
|
+
env_parser.add_argument('--template', choices=['basic', 'bioinfo', 'docker', 'python'],
|
|
1560
|
+
default='basic', help='Environment template')
|
|
1561
|
+
|
|
1562
|
+
# Quick commands
|
|
1563
|
+
quick_parser = subparsers.add_parser('quick', help='Show quick commands')
|
|
1564
|
+
quick_parser.add_argument('name', help='Connection name')
|
|
1565
|
+
|
|
1566
|
+
# Test command
|
|
1567
|
+
test_parser = subparsers.add_parser('test', help='Test connection')
|
|
1568
|
+
test_parser.add_argument('name', help='Connection name')
|
|
1569
|
+
|
|
1570
|
+
# List command
|
|
1571
|
+
subparsers.add_parser('list', help='List all connections')
|
|
1572
|
+
|
|
1573
|
+
|
|
1574
|
+
# Display command
|
|
1575
|
+
display_parser = subparsers.add_parser('display', help='Display instructions')
|
|
1576
|
+
display_parser.add_argument('--denbi', action='store_true',
|
|
1577
|
+
help='Show deNBI-specific instructions')
|
|
1578
|
+
display_parser.add_argument('--cheatsheet', action='store_true',
|
|
1579
|
+
help='Show cheatsheet')
|
|
1580
|
+
|
|
1581
|
+
args = parser.parse_args()
|
|
1582
|
+
|
|
1583
|
+
if not args.command:
|
|
1584
|
+
parser.print_help()
|
|
1585
|
+
return
|
|
1586
|
+
|
|
1587
|
+
try:
|
|
1588
|
+
if args.command == 'wizard':
|
|
1589
|
+
self.interactive_wizard()
|
|
1590
|
+
|
|
1591
|
+
elif args.command == 'setup':
|
|
1592
|
+
self.ssh_manager.create_jumphost_config(
|
|
1593
|
+
name=args.name,
|
|
1594
|
+
jumphost_ip=args.jumphost,
|
|
1595
|
+
jumphost_user=args.jumphost_user,
|
|
1596
|
+
private_key_path=args.key or f"~/.ssh/id_ed25519_{args.name}",
|
|
1597
|
+
target_ip=args.target,
|
|
1598
|
+
target_user=args.target_user or args.jumphost_user
|
|
1599
|
+
)
|
|
1600
|
+
|
|
1601
|
+
elif args.command == 'connect':
|
|
1602
|
+
self.ssh_manager.interactive_shell(args.name, args.hop)
|
|
1603
|
+
|
|
1604
|
+
elif args.command == 'transfer':
|
|
1605
|
+
self.ssh_manager.transfer_files(
|
|
1606
|
+
session_name=args.name,
|
|
1607
|
+
local_path=args.source,
|
|
1608
|
+
remote_path=args.dest,
|
|
1609
|
+
direction=args.direction
|
|
1610
|
+
)
|
|
1611
|
+
|
|
1612
|
+
elif args.command == 'tunnel':
|
|
1613
|
+
if args.tunnel_command == 'create':
|
|
1614
|
+
self.ssh_manager.create_tunnel(
|
|
1615
|
+
session_name=args.name,
|
|
1616
|
+
local_port=args.local_port,
|
|
1617
|
+
remote_host=args.remote_host,
|
|
1618
|
+
remote_port=args.remote_port,
|
|
1619
|
+
tunnel_name=args.tunnel_name
|
|
1620
|
+
)
|
|
1621
|
+
elif args.tunnel_command == 'start':
|
|
1622
|
+
self.ssh_manager.start_tunnel(args.tunnel_name, args.background)
|
|
1623
|
+
elif args.tunnel_command == 'stop':
|
|
1624
|
+
self.ssh_manager.stop_tunnel(args.tunnel_name)
|
|
1625
|
+
|
|
1626
|
+
elif args.command == 'setup-env':
|
|
1627
|
+
templates = {
|
|
1628
|
+
'basic': [
|
|
1629
|
+
"sudo apt update",
|
|
1630
|
+
"sudo apt upgrade -y",
|
|
1631
|
+
"sudo apt install -y git wget curl python3 python3-pip"
|
|
1632
|
+
],
|
|
1633
|
+
'bioinfo': [
|
|
1634
|
+
"sudo apt update && sudo apt upgrade -y",
|
|
1635
|
+
"sudo apt install -y git wget curl python3 python3-pip",
|
|
1636
|
+
"sudo apt install -y apptainer",
|
|
1637
|
+
"mkdir -p ~/data"
|
|
1638
|
+
],
|
|
1639
|
+
'docker': [
|
|
1640
|
+
"sudo apt update",
|
|
1641
|
+
"sudo apt install -y docker.io",
|
|
1642
|
+
"sudo systemctl start docker",
|
|
1643
|
+
"sudo systemctl enable docker"
|
|
1644
|
+
],
|
|
1645
|
+
'python': [
|
|
1646
|
+
"sudo apt update",
|
|
1647
|
+
"sudo apt install -y python3-pip python3-venv",
|
|
1648
|
+
"python3 -m pip install --upgrade pip"
|
|
1649
|
+
]
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
self.ssh_manager.batch_setup_environment(args.name, templates[args.template])
|
|
1653
|
+
|
|
1654
|
+
elif args.command == 'quick':
|
|
1655
|
+
commands = self.ssh_manager.get_quick_commands(args.name)
|
|
1656
|
+
for desc, cmd in commands.items():
|
|
1657
|
+
print(f"{desc}: {cmd}")
|
|
1658
|
+
|
|
1659
|
+
elif args.command == 'test':
|
|
1660
|
+
self.ssh_manager.test_connection(args.name)
|
|
1661
|
+
|
|
1662
|
+
elif args.command == 'list':
|
|
1663
|
+
sessions = self.ssh_manager.sessions
|
|
1664
|
+
if sessions:
|
|
1665
|
+
for name, session in sessions.items():
|
|
1666
|
+
jh = session["jumphost"]
|
|
1667
|
+
target = session["target"]
|
|
1668
|
+
print(f"{name}: {jh['user']}@{jh['ip']} → {target['user']}@{target['ip']}")
|
|
1669
|
+
else:
|
|
1670
|
+
print("No connections configured.")
|
|
1671
|
+
|
|
1672
|
+
elif args.command == 'display':
|
|
1673
|
+
if args.denbi:
|
|
1674
|
+
self.instructions.display_deNBI_specific()
|
|
1675
|
+
elif args.cheatsheet:
|
|
1676
|
+
self.instructions.display_cheatsheet()
|
|
1677
|
+
else:
|
|
1678
|
+
self.instructions.display_full_instructions()
|
|
1679
|
+
|
|
1680
|
+
except Exception as e:
|
|
1681
|
+
print(f"Error: {e}")
|
|
1682
|
+
sys.exit(1)
|
|
1683
|
+
|
|
1684
|
+
|
|
1685
|
+
class InstructionDisplay:
|
|
1686
|
+
"""Display comprehensive instructions for SSH2LS"""
|
|
1687
|
+
|
|
1688
|
+
@staticmethod
|
|
1689
|
+
def display_full_instructions():
|
|
1690
|
+
"""Display comprehensive instructions"""
|
|
1691
|
+
print("\n" + "="*80)
|
|
1692
|
+
print("📚 SSH2LS - COMPLETE USER GUIDE")
|
|
1693
|
+
print("="*80)
|
|
1694
|
+
|
|
1695
|
+
print("\n🔹 OVERVIEW:")
|
|
1696
|
+
print("SSH2LS is a powerful tool for managing complex SSH connections, especially")
|
|
1697
|
+
print("multi-hop setups like your deNBI infrastructure. It automates everything from")
|
|
1698
|
+
print("key management to file transfers through jump hosts.")
|
|
1699
|
+
|
|
1700
|
+
print("\n" + "="*80)
|
|
1701
|
+
print("🚀 QUICK START")
|
|
1702
|
+
print("="*80)
|
|
1703
|
+
|
|
1704
|
+
print("\n1. Interactive Wizard (Recommended for beginners):")
|
|
1705
|
+
print(" python ssh2ls.py wizard")
|
|
1706
|
+
print(" python ssh2ls.py # Defaults to wizard mode")
|
|
1707
|
+
|
|
1708
|
+
print("\n2. Automated deNBI Setup (Your specific setup):")
|
|
1709
|
+
print(" python ssh2ls.py setup denbi \\")
|
|
1710
|
+
print(" --jumphost 193.196.20.189 \\")
|
|
1711
|
+
print(" --jumphost-user ubuntu \\")
|
|
1712
|
+
print(" --target 192.168.54.219 \\")
|
|
1713
|
+
print(" --key ~/.ssh/denbi")
|
|
1714
|
+
|
|
1715
|
+
print("\n3. Connect to your deNBI VM:")
|
|
1716
|
+
print(" python ssh2ls.py connect denbi --hop target")
|
|
1717
|
+
|
|
1718
|
+
print("\n" + "="*80)
|
|
1719
|
+
print("📋 COMMAND REFERENCE")
|
|
1720
|
+
print("="*80)
|
|
1721
|
+
|
|
1722
|
+
print("\n🔸 SETUP COMMANDS:")
|
|
1723
|
+
print(" wizard Interactive setup wizard")
|
|
1724
|
+
print(" setup <name> Setup new connection")
|
|
1725
|
+
print(" --jumphost IP Jumphost IP address")
|
|
1726
|
+
print(" --jumphost-user USER Jumphost username (default: ubuntu)")
|
|
1727
|
+
print(" --target IP Target VM IP")
|
|
1728
|
+
print(" --target-user USER Target username")
|
|
1729
|
+
print(" --key PATH Private key path")
|
|
1730
|
+
|
|
1731
|
+
print("\n🔸 CONNECTION COMMANDS:")
|
|
1732
|
+
print(" connect <name> Connect to host")
|
|
1733
|
+
print(" --hop [jumphost|target] Which host to connect to (default: target)")
|
|
1734
|
+
print(" test <name> Test connection")
|
|
1735
|
+
print(" list List all connections")
|
|
1736
|
+
|
|
1737
|
+
print("\n🔸 FILE TRANSFER COMMANDS:")
|
|
1738
|
+
print(" transfer upload <name> <source> <dest>")
|
|
1739
|
+
print(" transfer download <name> <source> <dest>")
|
|
1740
|
+
|
|
1741
|
+
print("\n🔸 ENVIRONMENT COMMANDS:")
|
|
1742
|
+
print(" setup-env <name> Setup remote environment")
|
|
1743
|
+
print(" --template [basic|bioinfo|docker|python]")
|
|
1744
|
+
|
|
1745
|
+
print("\n🔸 TUNNEL COMMANDS:")
|
|
1746
|
+
print(" tunnel create <name> <local> <remote_host> <remote_port>")
|
|
1747
|
+
print(" tunnel start <tunnel_name>")
|
|
1748
|
+
print(" tunnel stop <tunnel_name>")
|
|
1749
|
+
|
|
1750
|
+
print("\n🔸 UTILITY COMMANDS:")
|
|
1751
|
+
print(" quick <name> Show quick commands for connection")
|
|
1752
|
+
print(" display Show these instructions")
|
|
1753
|
+
print(" help Show help message")
|
|
1754
|
+
|
|
1755
|
+
print("\n" + "="*80)
|
|
1756
|
+
print("🎯 REAL-WORLD EXAMPLES (FROM YOUR NOTES)")
|
|
1757
|
+
print("="*80)
|
|
1758
|
+
|
|
1759
|
+
print("\n📝 EXAMPLE 1: Complete deNBI Setup Workflow")
|
|
1760
|
+
print("-" * 40)
|
|
1761
|
+
print("# 1. Generate SSH key (if not done already)")
|
|
1762
|
+
print("ssh-keygen -t ed25519 -f ~/.ssh/denbi -C 'your_email@example.com'")
|
|
1763
|
+
print()
|
|
1764
|
+
print("# 2. Share public key with admin (Mohamad)")
|
|
1765
|
+
print("cat ~/.ssh/denbi.pub")
|
|
1766
|
+
print("# Send this output to admin to add to jumphost")
|
|
1767
|
+
print()
|
|
1768
|
+
print("# 3. Setup connection with ssh2ls")
|
|
1769
|
+
print("python ssh2ls.py setup denbi \\")
|
|
1770
|
+
print(" --jumphost 193.196.20.189 \\")
|
|
1771
|
+
print(" --jumphost-user ubuntu \\")
|
|
1772
|
+
print(" --target 192.168.54.219 \\")
|
|
1773
|
+
print(" --key ~/.ssh/denbi")
|
|
1774
|
+
print()
|
|
1775
|
+
print("# 4. Add key to SSH agent")
|
|
1776
|
+
print("ssh-add ~/.ssh/denbi")
|
|
1777
|
+
print()
|
|
1778
|
+
print("# 5. Test the connection")
|
|
1779
|
+
print("python ssh2ls.py test denbi")
|
|
1780
|
+
print()
|
|
1781
|
+
print("# 6. Connect to target VM")
|
|
1782
|
+
print("python ssh2ls.py connect denbi --hop target")
|
|
1783
|
+
|
|
1784
|
+
print("\n📝 EXAMPLE 2: File Transfer Through Jumphost")
|
|
1785
|
+
print("-" * 40)
|
|
1786
|
+
print("# Upload local data to remote VM")
|
|
1787
|
+
print("python ssh2ls.py transfer upload denbi \\")
|
|
1788
|
+
print(" ./fastq_files/ \\")
|
|
1789
|
+
print(" /home/ubuntu/chipseq_data/")
|
|
1790
|
+
print()
|
|
1791
|
+
print("# Download results from VM")
|
|
1792
|
+
print("python ssh2ls.py transfer download denbi \\")
|
|
1793
|
+
print(" /home/ubuntu/chipseq_data/results/ \\")
|
|
1794
|
+
print(" ./local_results/")
|
|
1795
|
+
|
|
1796
|
+
print("\n📝 EXAMPLE 3: Setup Bioinformatics Environment")
|
|
1797
|
+
print("-" * 40)
|
|
1798
|
+
print("# Setup nf-core environment on remote VM")
|
|
1799
|
+
print("python ssh2ls.py setup-env denbi --template bioinfo")
|
|
1800
|
+
print()
|
|
1801
|
+
print("# Manual alternative (what this does):")
|
|
1802
|
+
print("ssh -A -i ~/.ssh/denbi -J ubuntu@193.196.20.189 ubuntu@192.168.54.219")
|
|
1803
|
+
print("# Then on the remote VM:")
|
|
1804
|
+
print("sudo apt update && sudo apt upgrade -y")
|
|
1805
|
+
print("sudo apt install -y git wget curl python3 python3-pip")
|
|
1806
|
+
print("sudo apt install -y apptainer")
|
|
1807
|
+
print("mkdir -p ~/chipseq_data")
|
|
1808
|
+
|
|
1809
|
+
print("\n📝 EXAMPLE 4: Create SSH Tunnel for Web Access")
|
|
1810
|
+
print("-" * 40)
|
|
1811
|
+
print("# Tunnel local port 8888 to remote port 80")
|
|
1812
|
+
print("python ssh2ls.py tunnel create denbi 8888 localhost 80")
|
|
1813
|
+
print("python ssh2ls.py tunnel start denbi_tunnel_8888_80")
|
|
1814
|
+
print()
|
|
1815
|
+
print("# Now access http://localhost:8888 in your browser")
|
|
1816
|
+
print("# This tunnels through jumphost to target VM")
|
|
1817
|
+
|
|
1818
|
+
print("\n" + "="*80)
|
|
1819
|
+
print("🔧 TROUBLESHOOTING COMMON ISSUES")
|
|
1820
|
+
print("="*80)
|
|
1821
|
+
|
|
1822
|
+
print("\n❌ Issue: Permission denied (publickey)")
|
|
1823
|
+
print("Solution:")
|
|
1824
|
+
print(" 1. Check key is added to agent: ssh-add -l")
|
|
1825
|
+
print(" 2. Add key: ssh-add ~/.ssh/your_key")
|
|
1826
|
+
print(" 3. Verify public key is on jumphost")
|
|
1827
|
+
print(" 4. Check key permissions: chmod 600 ~/.ssh/your_key")
|
|
1828
|
+
|
|
1829
|
+
print("\n❌ Issue: SSH agent not running")
|
|
1830
|
+
print("Solution:")
|
|
1831
|
+
print(" eval $(ssh-agent)")
|
|
1832
|
+
print(" ssh-add ~/.ssh/your_key")
|
|
1833
|
+
|
|
1834
|
+
print("\n❌ Issue: Connection timeout to jumphost")
|
|
1835
|
+
print("Solution:")
|
|
1836
|
+
print(" 1. Check network connectivity")
|
|
1837
|
+
print(" 2. Verify jumphost IP: 193.196.20.189")
|
|
1838
|
+
print(" 3. Check firewall rules")
|
|
1839
|
+
print(" 4. Test with: ping 193.196.20.189")
|
|
1840
|
+
|
|
1841
|
+
print("\n❌ Issue: Can't connect from jumphost to target")
|
|
1842
|
+
print("Solution:")
|
|
1843
|
+
print(" 1. Verify target IP: 192.168.54.219")
|
|
1844
|
+
print(" 2. Check if you're on jumphost: ssh ubuntu@193.196.20.189")
|
|
1845
|
+
print(" 3. From jumphost, test: ping 192.168.54.219")
|
|
1846
|
+
print(" 4. Check agent forwarding: ssh-add -l (on jumphost)")
|
|
1847
|
+
|
|
1848
|
+
print("\n" + "="*80)
|
|
1849
|
+
print("⚙️ ADVANCED CONFIGURATION")
|
|
1850
|
+
print("="*80)
|
|
1851
|
+
|
|
1852
|
+
print("\n📁 SSH Config File (Automatically Generated):")
|
|
1853
|
+
print("-" * 40)
|
|
1854
|
+
print("Host denbi_jumphost")
|
|
1855
|
+
print(" HostName 193.196.20.189")
|
|
1856
|
+
print(" User ubuntu")
|
|
1857
|
+
print(" IdentityFile ~/.ssh/denbi")
|
|
1858
|
+
print(" ForwardAgent yes")
|
|
1859
|
+
print("")
|
|
1860
|
+
print("Host denbi")
|
|
1861
|
+
print(" HostName 192.168.54.219")
|
|
1862
|
+
print(" User ubuntu")
|
|
1863
|
+
print(" ProxyJump denbi_jumphost")
|
|
1864
|
+
|
|
1865
|
+
print("\n🔐 SSH Agent Forwarding:")
|
|
1866
|
+
print("-" * 40)
|
|
1867
|
+
print("# Enable agent forwarding (automatic with -A flag)")
|
|
1868
|
+
print("ssh -A -i ~/.ssh/denbi ubuntu@193.196.20.189")
|
|
1869
|
+
print("# On jumphost, your keys are available:")
|
|
1870
|
+
print("ssh-add -l # Should show your keys")
|
|
1871
|
+
|
|
1872
|
+
print("\n📊 File Transfer Syntax:")
|
|
1873
|
+
print("-" * 40)
|
|
1874
|
+
print("# Manual SCP through jumphost:")
|
|
1875
|
+
print("scp -r -o ProxyJump=ubuntu@193.196.20.189 \\")
|
|
1876
|
+
print(" -i ~/.ssh/denbi \\")
|
|
1877
|
+
print(" local_file ubuntu@192.168.54.219:remote_path")
|
|
1878
|
+
|
|
1879
|
+
print("\n" + "="*80)
|
|
1880
|
+
print("🎮 INTERACTIVE WIZARD WALKTHROUGH")
|
|
1881
|
+
print("="*80)
|
|
1882
|
+
|
|
1883
|
+
print("\n1. Start wizard: python ssh2ls.py wizard")
|
|
1884
|
+
print("2. Choose 'Setup Multi-Hop Connection'")
|
|
1885
|
+
print("3. Enter connection details:")
|
|
1886
|
+
print(" - Name: denbi")
|
|
1887
|
+
print(" - Jumphost IP: 193.196.20.189")
|
|
1888
|
+
print(" - Jumphost user: ubuntu")
|
|
1889
|
+
print(" - Target IP: 192.168.54.219")
|
|
1890
|
+
print(" - Target user: ubuntu")
|
|
1891
|
+
print(" - Private key: ~/.ssh/denbi")
|
|
1892
|
+
print("4. Wizard will:")
|
|
1893
|
+
print(" - Generate SSH config")
|
|
1894
|
+
print(" - Setup agent forwarding")
|
|
1895
|
+
print(" - Test connection")
|
|
1896
|
+
print(" - Create useful aliases")
|
|
1897
|
+
|
|
1898
|
+
print("\n" + "="*80)
|
|
1899
|
+
print("📚 ADDITIONAL RESOURCES")
|
|
1900
|
+
print("="*80)
|
|
1901
|
+
|
|
1902
|
+
print("\n🔗 Useful SSH Documentation:")
|
|
1903
|
+
print(" • SSH Config Manual: man ssh_config")
|
|
1904
|
+
print(" • ProxyJump: https://en.wikibooks.org/wiki/OpenSSH/Cookbook/Proxies_and_Jump_Hosts")
|
|
1905
|
+
print(" • Agent Forwarding: https://en.wikibooks.org/wiki/OpenSSH/Cookbook/Agent_Forwarding")
|
|
1906
|
+
|
|
1907
|
+
print("\n🔗 nf-core Resources:")
|
|
1908
|
+
print(" • nf-core ChIP-seq: https://nf-co.re/chipseq")
|
|
1909
|
+
print(" • Pipeline documentation: https://nf-co.re/chipseq/docs")
|
|
1910
|
+
|
|
1911
|
+
print("\n🔗 deNBI Resources:")
|
|
1912
|
+
print(" • deNBI Cloud: https://cloud.denbi.de/")
|
|
1913
|
+
print(" • Documentation: https://docs.denbi.de/")
|
|
1914
|
+
|
|
1915
|
+
print("\n" + "="*80)
|
|
1916
|
+
print("💡 PRO TIPS")
|
|
1917
|
+
print("="*80)
|
|
1918
|
+
|
|
1919
|
+
print("\n1. Use SSH config aliases:")
|
|
1920
|
+
print(" ssh denbi # Connect to target via jumphost")
|
|
1921
|
+
print(" ssh denbi_jumphost # Connect to jumphost directly")
|
|
1922
|
+
|
|
1923
|
+
print("\n2. Persistent connections with ControlMaster:")
|
|
1924
|
+
print(" Add to ~/.ssh/config:")
|
|
1925
|
+
print(" Host *")
|
|
1926
|
+
print(" ControlMaster auto")
|
|
1927
|
+
print(" ControlPath ~/.ssh/controlmasters/%r@%h:%p")
|
|
1928
|
+
print(" ControlPersist 10m")
|
|
1929
|
+
|
|
1930
|
+
print("\n3. Monitor SSH connections:")
|
|
1931
|
+
print(" ssh -O check denbi # Check connection status")
|
|
1932
|
+
print(" ssh -O exit denbi # Close connection")
|
|
1933
|
+
|
|
1934
|
+
print("\n4. Verbose debugging:")
|
|
1935
|
+
print(" ssh -vvv denbi # Level 3 verbosity")
|
|
1936
|
+
print(" scp -v ... # Verbose SCP")
|
|
1937
|
+
|
|
1938
|
+
print("\n5. Transfer entire directories:")
|
|
1939
|
+
print(" tar czf - /local/dir | ssh denbi 'tar xzf - -C /remote/dir'")
|
|
1940
|
+
|
|
1941
|
+
print("\n" + "="*80)
|
|
1942
|
+
print("❓ NEED HELP?")
|
|
1943
|
+
print("="*80)
|
|
1944
|
+
|
|
1945
|
+
print("\nRun these commands for help:")
|
|
1946
|
+
print(" python ssh2ls.py --help # Command line help")
|
|
1947
|
+
print(" python ssh2ls.py display # Show these instructions")
|
|
1948
|
+
print(" man ssh # SSH manual")
|
|
1949
|
+
print(" man ssh_config # SSH config manual")
|
|
1950
|
+
|
|
1951
|
+
print("\nCommon problems and solutions are in the troubleshooting section above.")
|
|
1952
|
+
print("For specific issues with your deNBI setup, check the examples section.")
|
|
1953
|
+
|
|
1954
|
+
print("\n" + "="*80)
|
|
1955
|
+
print("✅ GETTING STARTED CHECKLIST")
|
|
1956
|
+
print("="*80)
|
|
1957
|
+
|
|
1958
|
+
checklist = [
|
|
1959
|
+
("Generate SSH key pair", "ssh-keygen -t ed25519 -f ~/.ssh/denbi"),
|
|
1960
|
+
("Share public key with admin", "cat ~/.ssh/denbi.pub"),
|
|
1961
|
+
("Add key to SSH agent", "ssh-add ~/.ssh/denbi"),
|
|
1962
|
+
("Test jumphost connection", "ssh -A -i ~/.ssh/denbi ubuntu@193.196.20.189"),
|
|
1963
|
+
("Setup ssh2ls connection", "python ssh2ls.py setup denbi [options]"),
|
|
1964
|
+
("Test full connection", "python ssh2ls.py test denbi"),
|
|
1965
|
+
("Transfer test file", "python ssh2ls.py transfer upload denbi test.txt ~/"),
|
|
1966
|
+
("Setup remote environment", "python ssh2ls.py setup-env denbi --template bioinfo"),
|
|
1967
|
+
]
|
|
1968
|
+
|
|
1969
|
+
for i, (item, cmd) in enumerate(checklist, 1):
|
|
1970
|
+
print(f"{i}. {item}")
|
|
1971
|
+
print(f" Command: {cmd}")
|
|
1972
|
+
|
|
1973
|
+
print("\n" + "="*80)
|
|
1974
|
+
print("🎉 You're ready to use SSH2LS! Start with: python ssh2ls.py wizard")
|
|
1975
|
+
print("="*80 + "\n")
|
|
1976
|
+
|
|
1977
|
+
@staticmethod
|
|
1978
|
+
def display_deNBI_specific():
|
|
1979
|
+
"""Display deNBI-specific instructions"""
|
|
1980
|
+
print("\n" + "="*80)
|
|
1981
|
+
print("🎯 deNBI SPECIFIC SETUP INSTRUCTIONS")
|
|
1982
|
+
print("="*80)
|
|
1983
|
+
|
|
1984
|
+
print("\n📋 YOUR SPECIFIC CONFIGURATION:")
|
|
1985
|
+
print("-" * 40)
|
|
1986
|
+
print("Jumphost: ubuntu@193.196.20.189")
|
|
1987
|
+
print("Target VM: ubuntu@192.168.54.219")
|
|
1988
|
+
print("Private key: ~/.ssh/denbi")
|
|
1989
|
+
print("Purpose: nf-core ChIP-seq analysis")
|
|
1990
|
+
|
|
1991
|
+
print("\n🚀 COMPLETE WORKFLOW:")
|
|
1992
|
+
print("-" * 40)
|
|
1993
|
+
print("1. KEY EXCHANGE:")
|
|
1994
|
+
print(" - Generate key: ssh-keygen -t ed25519 -f ~/.ssh/denbi")
|
|
1995
|
+
print(" - Share public key with Mohamad:")
|
|
1996
|
+
print(" cat ~/.ssh/denbi.pub")
|
|
1997
|
+
print(" - Wait for confirmation that key is added to jumphost")
|
|
1998
|
+
|
|
1999
|
+
print("\n2. INITIAL SETUP:")
|
|
2000
|
+
print(" # Add key to agent")
|
|
2001
|
+
print(" ssh-add ~/.ssh/denbi")
|
|
2002
|
+
print(" ")
|
|
2003
|
+
print(" # Test jumphost connection")
|
|
2004
|
+
print(" ssh -A -i ~/.ssh/denbi ubuntu@193.196.20.189")
|
|
2005
|
+
print(" # You should see Ubuntu welcome message")
|
|
2006
|
+
print(" ")
|
|
2007
|
+
print(" # From jumphost, test target connection")
|
|
2008
|
+
print(" ssh ubuntu@192.168.54.219")
|
|
2009
|
+
print(" # Should connect without password")
|
|
2010
|
+
|
|
2011
|
+
print("\n3. AUTOMATE WITH SSH2LS:")
|
|
2012
|
+
print(" # Run the setup wizard")
|
|
2013
|
+
print(" python ssh2ls.py wizard")
|
|
2014
|
+
print(" ")
|
|
2015
|
+
print(" # OR use quick setup")
|
|
2016
|
+
print(" python ssh2ls.py setup denbi \\")
|
|
2017
|
+
print(" --jumphost 193.196.20.189 \\")
|
|
2018
|
+
print(" --jumphost-user ubuntu \\")
|
|
2019
|
+
print(" --target 192.168.54.219 \\")
|
|
2020
|
+
print(" --key ~/.ssh/denbi")
|
|
2021
|
+
|
|
2022
|
+
print("\n4. TRANSFER YOUR DATA:")
|
|
2023
|
+
print(" # Create directory structure")
|
|
2024
|
+
print(" python ssh2ls.py setup-env denbi --template bioinfo")
|
|
2025
|
+
print(" ")
|
|
2026
|
+
print(" # Upload FASTQ files")
|
|
2027
|
+
print(" python ssh2ls.py transfer upload denbi \\")
|
|
2028
|
+
print(" /path/to/your/fastq/files/ \\")
|
|
2029
|
+
print(" /home/ubuntu/chipseq_data/")
|
|
2030
|
+
|
|
2031
|
+
print("\n5. RUN nf-core ChIP-seq:")
|
|
2032
|
+
print(" # Connect to VM")
|
|
2033
|
+
print(" python ssh2ls.py connect denbi --hop target")
|
|
2034
|
+
print(" ")
|
|
2035
|
+
print(" # On the VM:")
|
|
2036
|
+
print(" # Pull container")
|
|
2037
|
+
print(" apptainer pull docker://nfcore/chipseq")
|
|
2038
|
+
print(" ")
|
|
2039
|
+
print(" # Run pipeline")
|
|
2040
|
+
print(" apptainer exec nfcore_chipseq_latest.sif \\")
|
|
2041
|
+
print(" nextflow run nf-core/chipseq \\")
|
|
2042
|
+
print(" -r 2.1.0 \\")
|
|
2043
|
+
print(" -profile standard \\")
|
|
2044
|
+
print(" --input ~/chipseq_data/samplesheet.csv \\")
|
|
2045
|
+
print(" --genome GRCh38 \\")
|
|
2046
|
+
print(" --outdir ~/chipseq_results/")
|
|
2047
|
+
|
|
2048
|
+
print("\n" + "="*80)
|
|
2049
|
+
print("🔧 TROUBLESHOOTING deNBI SETUP")
|
|
2050
|
+
print("="*80)
|
|
2051
|
+
|
|
2052
|
+
print("\n❌ 'Permission denied' when connecting to jumphost")
|
|
2053
|
+
print("Solution:")
|
|
2054
|
+
print(" 1. Verify key was added to jumphost by admin")
|
|
2055
|
+
print(" 2. Check key is in agent: ssh-add -l")
|
|
2056
|
+
print(" 3. Test with: ssh -v -i ~/.ssh/denbi ubuntu@193.196.20.189")
|
|
2057
|
+
|
|
2058
|
+
print("\n❌ Can't connect from jumphost to target")
|
|
2059
|
+
print("Solution:")
|
|
2060
|
+
print(" 1. On jumphost, check if agent has keys: ssh-add -l")
|
|
2061
|
+
print(" 2. Test direct connection from jumphost:")
|
|
2062
|
+
print(" ssh -v ubuntu@192.168.54.219")
|
|
2063
|
+
print(" 3. Check network: ping 192.168.54.219")
|
|
2064
|
+
|
|
2065
|
+
print("\n❌ File transfer fails")
|
|
2066
|
+
print("Solution:")
|
|
2067
|
+
print(" 1. Check ProxyJump is working")
|
|
2068
|
+
print(" 2. Test with small file first")
|
|
2069
|
+
print(" 3. Use verbose mode: scp -v ...")
|
|
2070
|
+
|
|
2071
|
+
print("\n" + "="*80)
|
|
2072
|
+
print("📞 SUPPORT CONTACTS")
|
|
2073
|
+
print("="*80)
|
|
2074
|
+
|
|
2075
|
+
print("\nFor deNBI infrastructure issues:")
|
|
2076
|
+
print(" • Mohamad (Admin): Provided jumphost access")
|
|
2077
|
+
print(" • deNBI Support: https://cloud.denbi.de/support")
|
|
2078
|
+
|
|
2079
|
+
print("\nFor nf-core pipeline issues:")
|
|
2080
|
+
print(" • nf-core documentation: https://nf-co.re/chipseq/docs")
|
|
2081
|
+
print(" • GitHub issues: https://github.com/nf-core/chipseq/issues")
|
|
2082
|
+
|
|
2083
|
+
print("\nFor SSH2LS tool issues:")
|
|
2084
|
+
print(" • Run: python ssh2ls.py --help")
|
|
2085
|
+
print(" • Use: python ssh2ls.py display (these instructions)")
|
|
2086
|
+
|
|
2087
|
+
print("\n" + "="*80)
|
|
2088
|
+
print("✅ deNBI Setup Complete!")
|
|
2089
|
+
print("="*80 + "\n")
|
|
2090
|
+
|
|
2091
|
+
@staticmethod
|
|
2092
|
+
def display_cheatsheet():
|
|
2093
|
+
"""Display SSH2LS cheatsheet"""
|
|
2094
|
+
print("\n" + "="*80)
|
|
2095
|
+
print("📖 SSH2LS CHEATSHEET")
|
|
2096
|
+
print("="*80)
|
|
2097
|
+
|
|
2098
|
+
print("\n🔑 KEY MANAGEMENT:")
|
|
2099
|
+
print(" ssh-keygen -t ed25519 -f ~/.ssh/denbi")
|
|
2100
|
+
print(" ssh-add ~/.ssh/denbi")
|
|
2101
|
+
print(" ssh-add -l")
|
|
2102
|
+
print(" ssh-add -d ~/.ssh/denbi")
|
|
2103
|
+
|
|
2104
|
+
print("\n🔗 CONNECTION:")
|
|
2105
|
+
print(" # Direct methods")
|
|
2106
|
+
print(" ssh -A -i ~/.ssh/denbi ubuntu@193.196.20.189")
|
|
2107
|
+
print(" ssh -A -i ~/.ssh/denbi -J ubuntu@193.196.20.189 ubuntu@192.168.54.219")
|
|
2108
|
+
print(" ")
|
|
2109
|
+
print(" # Using ssh2ls")
|
|
2110
|
+
print(" python ssh2ls.py connect denbi --hop jumphost")
|
|
2111
|
+
print(" python ssh2ls.py connect denbi --hop target")
|
|
2112
|
+
|
|
2113
|
+
print("\n📁 FILE TRANSFER:")
|
|
2114
|
+
print(" # Manual")
|
|
2115
|
+
print(" scp -r -o ProxyJump=ubuntu@193.196.20.189 \\")
|
|
2116
|
+
print(" -i ~/.ssh/denbi local_file ubuntu@192.168.54.219:remote_path")
|
|
2117
|
+
print(" ")
|
|
2118
|
+
print(" # Using ssh2ls")
|
|
2119
|
+
print(" python ssh2ls.py transfer upload denbi local_path remote_path")
|
|
2120
|
+
print(" python ssh2ls.py transfer download denbi remote_path local_path")
|
|
2121
|
+
|
|
2122
|
+
print("\n🌐 TUNNELS:")
|
|
2123
|
+
print(" # Create web tunnel")
|
|
2124
|
+
print(" ssh -N -L 8080:localhost:80 \\")
|
|
2125
|
+
print(" -i ~/.ssh/denbi \\")
|
|
2126
|
+
print(" -J ubuntu@193.196.20.189 \\")
|
|
2127
|
+
print(" ubuntu@192.168.54.219")
|
|
2128
|
+
print(" ")
|
|
2129
|
+
print(" # Using ssh2ls")
|
|
2130
|
+
print(" python ssh2ls.py tunnel create denbi 8080 localhost 80")
|
|
2131
|
+
print(" python ssh2ls.py tunnel start denbi_tunnel_8080_80")
|
|
2132
|
+
|
|
2133
|
+
print("\n⚙️ ENVIRONMENT:")
|
|
2134
|
+
print(" python ssh2ls.py setup-env denbi --template bioinfo")
|
|
2135
|
+
print(" python ssh2ls.py setup-env denbi --template docker")
|
|
2136
|
+
print(" python ssh2ls.py setup-env denbi --template python")
|
|
2137
|
+
|
|
2138
|
+
print("\n🔍 DEBUGGING:")
|
|
2139
|
+
print(" ssh -vvv denbi # Verbose level 3")
|
|
2140
|
+
print(" ssh -O check denbi # Check connection")
|
|
2141
|
+
print(" python ssh2ls.py test denbi # Test with ssh2ls")
|
|
2142
|
+
print(" tail -f ~/.ssh/config # Monitor config")
|
|
2143
|
+
|
|
2144
|
+
print("\n📝 USEFUL ALIASES (add to ~/.bashrc or ~/.zshrc):")
|
|
2145
|
+
print(" alias denbi='ssh denbi'")
|
|
2146
|
+
print(" alias denbi-jump='ssh -A -i ~/.ssh/denbi ubuntu@193.196.20.189'")
|
|
2147
|
+
print(" alias denbi-vm='ssh -A -i ~/.ssh/denbi -J ubuntu@193.196.20.189 ubuntu@192.168.54.219'")
|
|
2148
|
+
print(" alias denbi-copy='scp -r -o ProxyJump=ubuntu@193.196.20.189 -i ~/.ssh/denbi'")
|
|
2149
|
+
|
|
2150
|
+
print("\n" + "="*80)
|
|
2151
|
+
print("💾 CONFIGURATION FILES")
|
|
2152
|
+
print("="*80)
|
|
2153
|
+
|
|
2154
|
+
print("\n~/.ssh/config (generated by ssh2ls):")
|
|
2155
|
+
print("-" * 40)
|
|
2156
|
+
print("Host denbi_jumphost")
|
|
2157
|
+
print(" HostName 193.196.20.189")
|
|
2158
|
+
print(" User ubuntu")
|
|
2159
|
+
print(" IdentityFile ~/.ssh/denbi")
|
|
2160
|
+
print(" ForwardAgent yes")
|
|
2161
|
+
print("")
|
|
2162
|
+
print("Host denbi")
|
|
2163
|
+
print(" HostName 192.168.54.219")
|
|
2164
|
+
print(" User ubuntu")
|
|
2165
|
+
print(" ProxyJump denbi_jumphost")
|
|
2166
|
+
|
|
2167
|
+
print("\n~/.ssh/ssh2ls/sessions.json (managed by ssh2ls):")
|
|
2168
|
+
print("-" * 40)
|
|
2169
|
+
print("Stores all your connection configurations")
|
|
2170
|
+
|
|
2171
|
+
print("\n" + "="*80)
|
|
2172
|
+
print("🚀 QUICK START RECAP")
|
|
2173
|
+
print("="*80)
|
|
2174
|
+
|
|
2175
|
+
print("\nFor your deNBI setup:")
|
|
2176
|
+
print(" 1. ssh-keygen -t ed25519 -f ~/.ssh/denbi")
|
|
2177
|
+
print(" 2. cat ~/.ssh/denbi.pub → share with admin")
|
|
2178
|
+
print(" 3. python ssh2ls.py setup denbi [options]")
|
|
2179
|
+
print(" 4. python ssh2ls.py connect denbi")
|
|
2180
|
+
print(" 5. python ssh2ls.py transfer upload denbi data/ ~/")
|
|
2181
|
+
|
|
2182
|
+
print("\n" + "="*80 + "\n")
|
|
2183
|
+
def main():
|
|
2184
|
+
"""Main entry point"""
|
|
2185
|
+
app = SSH2LS()
|
|
2186
|
+
|
|
2187
|
+
if len(sys.argv) > 1:
|
|
2188
|
+
app.run_from_cli()
|
|
2189
|
+
else:
|
|
2190
|
+
# Check dependencies
|
|
2191
|
+
try:
|
|
2192
|
+
import paramiko
|
|
2193
|
+
import cryptography
|
|
2194
|
+
print("✓ All dependencies found")
|
|
2195
|
+
except ImportError as e:
|
|
2196
|
+
print(f"⚠ Missing dependency: {e}")
|
|
2197
|
+
print("Install with: pip install paramiko cryptography pyyaml")
|
|
2198
|
+
sys.exit(1)
|
|
2199
|
+
|
|
2200
|
+
# Start interactive wizard
|
|
2201
|
+
app.interactive_wizard()
|
|
2202
|
+
|
|
2203
|
+
if __name__ == "__main__":
|
|
2204
|
+
main()
|