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.

Files changed (72) hide show
  1. py2ls/.DS_Store +0 -0
  2. py2ls/.git/.DS_Store +0 -0
  3. py2ls/.git/index +0 -0
  4. py2ls/.git/logs/refs/remotes/origin/HEAD +1 -0
  5. py2ls/.git/objects/.DS_Store +0 -0
  6. py2ls/.git/refs/.DS_Store +0 -0
  7. py2ls/ImageLoader.py +621 -0
  8. py2ls/__init__.py +7 -5
  9. py2ls/apptainer2ls.py +3940 -0
  10. py2ls/batman.py +164 -42
  11. py2ls/bio.py +2595 -0
  12. py2ls/cell_image_clf.py +1632 -0
  13. py2ls/container2ls.py +4635 -0
  14. py2ls/corr.py +475 -0
  15. py2ls/data/.DS_Store +0 -0
  16. py2ls/data/email/email_html_template.html +88 -0
  17. py2ls/data/hyper_param_autogluon_zeroshot2024.json +2383 -0
  18. py2ls/data/hyper_param_tabrepo_2024.py +1753 -0
  19. py2ls/data/mygenes_fields_241022.txt +355 -0
  20. py2ls/data/re_common_pattern.json +173 -0
  21. py2ls/data/sns_info.json +74 -0
  22. py2ls/data/styles/.DS_Store +0 -0
  23. py2ls/data/styles/example/.DS_Store +0 -0
  24. py2ls/data/styles/stylelib/.DS_Store +0 -0
  25. py2ls/data/styles/stylelib/grid.mplstyle +15 -0
  26. py2ls/data/styles/stylelib/high-contrast.mplstyle +6 -0
  27. py2ls/data/styles/stylelib/high-vis.mplstyle +4 -0
  28. py2ls/data/styles/stylelib/ieee.mplstyle +15 -0
  29. py2ls/data/styles/stylelib/light.mplstyl +6 -0
  30. py2ls/data/styles/stylelib/muted.mplstyle +6 -0
  31. py2ls/data/styles/stylelib/nature-reviews-latex.mplstyle +616 -0
  32. py2ls/data/styles/stylelib/nature-reviews.mplstyle +616 -0
  33. py2ls/data/styles/stylelib/nature.mplstyle +31 -0
  34. py2ls/data/styles/stylelib/no-latex.mplstyle +10 -0
  35. py2ls/data/styles/stylelib/notebook.mplstyle +36 -0
  36. py2ls/data/styles/stylelib/paper.mplstyle +290 -0
  37. py2ls/data/styles/stylelib/paper2.mplstyle +305 -0
  38. py2ls/data/styles/stylelib/retro.mplstyle +4 -0
  39. py2ls/data/styles/stylelib/sans.mplstyle +10 -0
  40. py2ls/data/styles/stylelib/scatter.mplstyle +7 -0
  41. py2ls/data/styles/stylelib/science.mplstyle +48 -0
  42. py2ls/data/styles/stylelib/std-colors.mplstyle +4 -0
  43. py2ls/data/styles/stylelib/vibrant.mplstyle +6 -0
  44. py2ls/data/tiles.csv +146 -0
  45. py2ls/data/usages_pd.json +1417 -0
  46. py2ls/data/usages_sns.json +31 -0
  47. py2ls/docker2ls.py +5446 -0
  48. py2ls/ec2ls.py +61 -0
  49. py2ls/fetch_update.py +145 -0
  50. py2ls/ich2ls.py +1955 -296
  51. py2ls/im2.py +8242 -0
  52. py2ls/image_ml2ls.py +2100 -0
  53. py2ls/ips.py +33909 -3418
  54. py2ls/ml2ls.py +7700 -0
  55. py2ls/mol.py +289 -0
  56. py2ls/mount2ls.py +1307 -0
  57. py2ls/netfinder.py +873 -351
  58. py2ls/nl2ls.py +283 -0
  59. py2ls/ocr.py +1581 -458
  60. py2ls/plot.py +10394 -314
  61. py2ls/rna2ls.py +311 -0
  62. py2ls/ssh2ls.md +456 -0
  63. py2ls/ssh2ls.py +5933 -0
  64. py2ls/ssh2ls_v01.py +2204 -0
  65. py2ls/stats.py +66 -172
  66. py2ls/temp20251124.py +509 -0
  67. py2ls/translator.py +2 -0
  68. py2ls/utils/decorators.py +3564 -0
  69. py2ls/utils_bio.py +3453 -0
  70. {py2ls-0.1.10.12.dist-info → py2ls-0.2.7.10.dist-info}/METADATA +113 -224
  71. {py2ls-0.1.10.12.dist-info → py2ls-0.2.7.10.dist-info}/RECORD +72 -16
  72. {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()