sshplex 1.6.2__tar.gz → 1.6.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sshplex-1.6.2/sshplex.egg-info → sshplex-1.6.3}/PKG-INFO +21 -2
- {sshplex-1.6.2 → sshplex-1.6.3}/README.md +20 -1
- {sshplex-1.6.2 → sshplex-1.6.3}/pyproject.toml +6 -1
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex/__init__.py +1 -1
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex/cli.py +13 -107
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex/config-template.yaml +1 -0
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex/lib/cache.py +2 -2
- sshplex-1.6.3/sshplex/lib/commands.py +110 -0
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex/lib/config.py +3 -5
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex/lib/multiplexer/tmux.py +0 -32
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex/lib/onboarding/wizard.py +1 -1
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex/lib/sot/factory.py +132 -131
- sshplex-1.6.3/sshplex/lib/ui/config_editor.py +1814 -0
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex/lib/ui/host_selector.py +257 -38
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex/lib/ui/session_manager.py +33 -135
- sshplex-1.6.3/sshplex/lib/utils/ssh_config.py +65 -0
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex/main.py +12 -110
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex/sshplex_connector.py +34 -15
- {sshplex-1.6.2 → sshplex-1.6.3/sshplex.egg-info}/PKG-INFO +21 -2
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex.egg-info/SOURCES.txt +4 -1
- {sshplex-1.6.2 → sshplex-1.6.3}/tests/test_config.py +19 -1
- sshplex-1.6.3/tests/test_main.py +27 -0
- sshplex-1.6.2/sshplex/lib/ui/config_editor.py +0 -1049
- {sshplex-1.6.2 → sshplex-1.6.3}/LICENSE +0 -0
- {sshplex-1.6.2 → sshplex-1.6.3}/MANIFEST.in +0 -0
- {sshplex-1.6.2 → sshplex-1.6.3}/setup.cfg +0 -0
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex/lib/__init__.py +0 -0
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex/lib/logger.py +0 -0
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex/lib/multiplexer/__init__.py +0 -0
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex/lib/multiplexer/base.py +0 -0
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex/lib/multiplexer/iterm2_native.py +0 -0
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex/lib/onboarding/__init__.py +0 -0
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex/lib/sot/__init__.py +0 -0
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex/lib/sot/ansible.py +0 -0
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex/lib/sot/base.py +0 -0
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex/lib/sot/consul.py +0 -0
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex/lib/sot/netbox.py +0 -0
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex/lib/sot/static.py +0 -0
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex/lib/ui/__init__.py +0 -0
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex/lib/utils/__init__.py +0 -0
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex/lib/utils/iterm2.py +0 -0
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex.egg-info/dependency_links.txt +0 -0
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex.egg-info/entry_points.txt +0 -0
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex.egg-info/requires.txt +0 -0
- {sshplex-1.6.2 → sshplex-1.6.3}/sshplex.egg-info/top_level.txt +0 -0
- {sshplex-1.6.2 → sshplex-1.6.3}/tests/test_cache.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sshplex
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.3
|
|
4
4
|
Summary: Multiplex your SSH connections with style
|
|
5
5
|
Author-email: MJAHED Sabri <contact@sabrimjahed.com>
|
|
6
6
|
License: MIT
|
|
@@ -62,11 +62,19 @@ SSHplex is a Python-based SSH connection multiplexer with a modern TUI. Connect
|
|
|
62
62
|
- 🖥️ **Modern TUI** - Textual-based host selector with search, sort, and multi-select
|
|
63
63
|
- 🔌 **Multiple Sources** - NetBox, Ansible, Consul, static lists - use them together
|
|
64
64
|
- 📦 **3 Mux Backends** - tmux standalone, tmux + iTerm2, or iTerm2 native (macOS)
|
|
65
|
-
- ✏️ **Config Editor** - Built-in
|
|
65
|
+
- ✏️ **Config Editor** - Built-in editor with compact source cards, static host rows, and full YAML pane
|
|
66
66
|
- 🔄 **Broadcast Input** - Sync commands across multiple SSH sessions
|
|
67
67
|
- 🔐 **SSH Security** - Configurable host key checking and retry logic
|
|
68
68
|
- 🚀 **Fast Startup** - Intelligent caching with configurable TTL
|
|
69
69
|
|
|
70
|
+
## What Is New in QoL v2
|
|
71
|
+
|
|
72
|
+
- **Static host manager in UI** - Add and edit static hosts as rows (`name`, `ip`, `alias`, `user`, `port`, `key_path`) instead of raw YAML blobs.
|
|
73
|
+
- **Per-host SSH preview** - Preview effective SSH values from `ssh -G` in settings and from host selector (`o`).
|
|
74
|
+
- **Smarter table columns** - Detect columns from live hosts/cache/imports, including SSH-oriented fields like `alias`, `user`, `port`, and `key_path`.
|
|
75
|
+
- **Better Sources UX** - Provider toggles and collapsible import cards make large source configs easier to navigate.
|
|
76
|
+
- **Rich YAML view** - Side-by-side YAML edit + syntax-highlight preview for full config inspection.
|
|
77
|
+
|
|
70
78
|
## Quick Start
|
|
71
79
|
|
|
72
80
|
```bash
|
|
@@ -94,6 +102,17 @@ sshplex
|
|
|
94
102
|
| **tmux + iTerm2** | macOS | Native UI + persistence |
|
|
95
103
|
| **iTerm2 native** | macOS | Simple setup, no tmux dependency |
|
|
96
104
|
|
|
105
|
+
## Sources of Truth
|
|
106
|
+
|
|
107
|
+
| Provider | `type` | Extra Dependency | Best For |
|
|
108
|
+
|----------|--------|------------------|----------|
|
|
109
|
+
| **Static** | `static` | None | Small lists, lab hosts, quick manual entries |
|
|
110
|
+
| **NetBox** | `netbox` | None (included in base install) | Inventory-driven infrastructure with metadata |
|
|
111
|
+
| **Ansible** | `ansible` | None | Reusing existing Ansible inventory files |
|
|
112
|
+
| **Consul** | `consul` | `pip install "sshplex[consul]"` | Service discovery and dynamic node catalogs |
|
|
113
|
+
|
|
114
|
+
Provider activation is controlled by `sot.providers`, and each source is configured as an item in `sot.import`.
|
|
115
|
+
|
|
97
116
|
|
|
98
117
|
## Local Demo (Consul + Ansible)
|
|
99
118
|
|
|
@@ -9,11 +9,19 @@ SSHplex is a Python-based SSH connection multiplexer with a modern TUI. Connect
|
|
|
9
9
|
- 🖥️ **Modern TUI** - Textual-based host selector with search, sort, and multi-select
|
|
10
10
|
- 🔌 **Multiple Sources** - NetBox, Ansible, Consul, static lists - use them together
|
|
11
11
|
- 📦 **3 Mux Backends** - tmux standalone, tmux + iTerm2, or iTerm2 native (macOS)
|
|
12
|
-
- ✏️ **Config Editor** - Built-in
|
|
12
|
+
- ✏️ **Config Editor** - Built-in editor with compact source cards, static host rows, and full YAML pane
|
|
13
13
|
- 🔄 **Broadcast Input** - Sync commands across multiple SSH sessions
|
|
14
14
|
- 🔐 **SSH Security** - Configurable host key checking and retry logic
|
|
15
15
|
- 🚀 **Fast Startup** - Intelligent caching with configurable TTL
|
|
16
16
|
|
|
17
|
+
## What Is New in QoL v2
|
|
18
|
+
|
|
19
|
+
- **Static host manager in UI** - Add and edit static hosts as rows (`name`, `ip`, `alias`, `user`, `port`, `key_path`) instead of raw YAML blobs.
|
|
20
|
+
- **Per-host SSH preview** - Preview effective SSH values from `ssh -G` in settings and from host selector (`o`).
|
|
21
|
+
- **Smarter table columns** - Detect columns from live hosts/cache/imports, including SSH-oriented fields like `alias`, `user`, `port`, and `key_path`.
|
|
22
|
+
- **Better Sources UX** - Provider toggles and collapsible import cards make large source configs easier to navigate.
|
|
23
|
+
- **Rich YAML view** - Side-by-side YAML edit + syntax-highlight preview for full config inspection.
|
|
24
|
+
|
|
17
25
|
## Quick Start
|
|
18
26
|
|
|
19
27
|
```bash
|
|
@@ -41,6 +49,17 @@ sshplex
|
|
|
41
49
|
| **tmux + iTerm2** | macOS | Native UI + persistence |
|
|
42
50
|
| **iTerm2 native** | macOS | Simple setup, no tmux dependency |
|
|
43
51
|
|
|
52
|
+
## Sources of Truth
|
|
53
|
+
|
|
54
|
+
| Provider | `type` | Extra Dependency | Best For |
|
|
55
|
+
|----------|--------|------------------|----------|
|
|
56
|
+
| **Static** | `static` | None | Small lists, lab hosts, quick manual entries |
|
|
57
|
+
| **NetBox** | `netbox` | None (included in base install) | Inventory-driven infrastructure with metadata |
|
|
58
|
+
| **Ansible** | `ansible` | None | Reusing existing Ansible inventory files |
|
|
59
|
+
| **Consul** | `consul` | `pip install "sshplex[consul]"` | Service discovery and dynamic node catalogs |
|
|
60
|
+
|
|
61
|
+
Provider activation is controlled by `sot.providers`, and each source is configured as an item in `sot.import`.
|
|
62
|
+
|
|
44
63
|
|
|
45
64
|
## Local Demo (Consul + Ansible)
|
|
46
65
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sshplex"
|
|
7
|
-
version = "1.6.
|
|
7
|
+
version = "1.6.3"
|
|
8
8
|
description = "Multiplex your SSH connections with style"
|
|
9
9
|
authors = [{name = "MJAHED Sabri", email = "contact@sabrimjahed.com"}]
|
|
10
10
|
readme = "README.md"
|
|
@@ -113,6 +113,11 @@ module = ["consul"]
|
|
|
113
113
|
ignore_missing_imports = true
|
|
114
114
|
|
|
115
115
|
|
|
116
|
+
[[tool.mypy.overrides]]
|
|
117
|
+
module = ["pynetbox", "pynetbox.*", "pyperclip", "pyperclip.*"]
|
|
118
|
+
ignore_missing_imports = true
|
|
119
|
+
|
|
120
|
+
|
|
116
121
|
[[tool.mypy.overrides]]
|
|
117
122
|
module = ["sshplex.lib.sot.consul"]
|
|
118
123
|
ignore_errors = true
|
|
@@ -5,9 +5,9 @@ import sys
|
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
7
|
from . import __version__
|
|
8
|
-
from .lib.
|
|
8
|
+
from .lib.commands import clear_cache, run_debug_mode, show_config_info
|
|
9
|
+
from .lib.config import load_config
|
|
9
10
|
from .lib.logger import get_logger, setup_logging
|
|
10
|
-
from .lib.sot.factory import SoTFactory
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def main() -> int:
|
|
@@ -62,11 +62,15 @@ Examples:
|
|
|
62
62
|
logger.info("SSHplex CLI started")
|
|
63
63
|
|
|
64
64
|
if args.clear_cache:
|
|
65
|
-
return clear_cache(config, logger)
|
|
65
|
+
return clear_cache(config, logger, no_cache_message="Clearing cache...")
|
|
66
66
|
elif args.list_providers:
|
|
67
67
|
return list_providers(config, logger)
|
|
68
68
|
else:
|
|
69
|
-
return
|
|
69
|
+
return run_debug_mode(
|
|
70
|
+
config,
|
|
71
|
+
logger,
|
|
72
|
+
footer_note="Note: For the full TUI interface, run the main application",
|
|
73
|
+
)
|
|
70
74
|
else:
|
|
71
75
|
# Default to debug mode if no specific action
|
|
72
76
|
print("SSHplex CLI - Loading configuration...")
|
|
@@ -81,7 +85,11 @@ Examples:
|
|
|
81
85
|
logger = get_logger()
|
|
82
86
|
logger.info("SSHplex CLI debug mode started")
|
|
83
87
|
|
|
84
|
-
return
|
|
88
|
+
return run_debug_mode(
|
|
89
|
+
config,
|
|
90
|
+
logger,
|
|
91
|
+
footer_note="Note: For the full TUI interface, run the main application",
|
|
92
|
+
)
|
|
85
93
|
|
|
86
94
|
except FileNotFoundError as e:
|
|
87
95
|
print(f"Error: {e}")
|
|
@@ -99,25 +107,6 @@ Examples:
|
|
|
99
107
|
return 1
|
|
100
108
|
|
|
101
109
|
|
|
102
|
-
def show_config_info() -> int:
|
|
103
|
-
"""Show configuration file paths and status."""
|
|
104
|
-
info = get_config_info()
|
|
105
|
-
|
|
106
|
-
print("📁 SSHplex Configuration Information")
|
|
107
|
-
print("=" * 50)
|
|
108
|
-
print(f"Config Directory: {info['default_config_path'].rsplit('/', 1)[0]}")
|
|
109
|
-
print(f"Config File: {info['default_config_path']}")
|
|
110
|
-
print(f"Config Exists: {'✅ Yes' if info['default_config_exists'] else '❌ No'}")
|
|
111
|
-
print(f"Template File: {info['template_path']}")
|
|
112
|
-
print(f"Template Exists: {'✅ Yes' if info['template_exists'] else '❌ No'}")
|
|
113
|
-
print()
|
|
114
|
-
|
|
115
|
-
if not info['default_config_exists']:
|
|
116
|
-
print("💡 Run 'sshplex' to create a default configuration file")
|
|
117
|
-
|
|
118
|
-
return 0
|
|
119
|
-
|
|
120
|
-
|
|
121
110
|
def list_providers(config: Any, logger: Any) -> int:
|
|
122
111
|
"""List all configured SoT providers."""
|
|
123
112
|
logger.info("Listing configured SoT providers")
|
|
@@ -156,88 +145,5 @@ def list_providers(config: Any, logger: Any) -> int:
|
|
|
156
145
|
return 0
|
|
157
146
|
|
|
158
147
|
|
|
159
|
-
def clear_cache(config: Any, logger: Any) -> int:
|
|
160
|
-
"""Clear the host cache."""
|
|
161
|
-
logger.info("Clearing host cache")
|
|
162
|
-
|
|
163
|
-
from .lib.cache import HostCache
|
|
164
|
-
|
|
165
|
-
cache = HostCache(
|
|
166
|
-
cache_dir=config.cache.cache_dir,
|
|
167
|
-
cache_ttl_hours=config.cache.ttl_hours
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
cache_info = cache.get_cache_info()
|
|
171
|
-
if cache_info:
|
|
172
|
-
print(f"🗑️ Clearing cache ({cache_info.get('host_count', 0)} hosts, age: {cache_info.get('age_hours', 0):.1f}h)")
|
|
173
|
-
else:
|
|
174
|
-
print("🗑️ Clearing cache...")
|
|
175
|
-
|
|
176
|
-
if cache.clear_cache():
|
|
177
|
-
print("✅ Cache cleared successfully")
|
|
178
|
-
return 0
|
|
179
|
-
else:
|
|
180
|
-
print("❌ Failed to clear cache")
|
|
181
|
-
return 1
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
def debug_mode(config: Any, logger: Any) -> int:
|
|
185
|
-
"""Run debug mode - SoT provider connection and host listing test."""
|
|
186
|
-
logger.info("Running CLI debug mode - SoT provider connectivity test")
|
|
187
|
-
|
|
188
|
-
# Initialize SoT factory
|
|
189
|
-
logger.info("Initializing SoT factory")
|
|
190
|
-
sot_factory = SoTFactory(config)
|
|
191
|
-
|
|
192
|
-
# Check cache status
|
|
193
|
-
cache_info = sot_factory.get_cache_info()
|
|
194
|
-
if cache_info:
|
|
195
|
-
print(f"📦 Cache: {cache_info.get('host_count', 0)} hosts cached ({cache_info.get('age_hours', 0):.1f}h old)")
|
|
196
|
-
|
|
197
|
-
# Initialize all providers
|
|
198
|
-
if not sot_factory.initialize_providers():
|
|
199
|
-
logger.error("Failed to initialize any SoT providers")
|
|
200
|
-
print("❌ Failed to initialize any SoT providers")
|
|
201
|
-
print("Check your configuration and network connectivity")
|
|
202
|
-
return 1
|
|
203
|
-
|
|
204
|
-
print(f"✅ Successfully initialized {sot_factory.get_provider_count()} SoT provider(s): {', '.join(sot_factory.get_provider_names())}")
|
|
205
|
-
|
|
206
|
-
# Test all connections
|
|
207
|
-
logger.info("Testing SoT provider connections...")
|
|
208
|
-
connection_results = sot_factory.test_all_connections()
|
|
209
|
-
|
|
210
|
-
for provider_name, status in connection_results.items():
|
|
211
|
-
if status:
|
|
212
|
-
print(f"✅ {provider_name}: Connection successful")
|
|
213
|
-
else:
|
|
214
|
-
print(f"❌ {provider_name}: Connection failed")
|
|
215
|
-
|
|
216
|
-
# Retrieve hosts from all providers
|
|
217
|
-
logger.info("Retrieving hosts from all SoT providers...")
|
|
218
|
-
hosts = sot_factory.get_all_hosts()
|
|
219
|
-
|
|
220
|
-
# Display results
|
|
221
|
-
if hosts:
|
|
222
|
-
logger.info(f"Successfully retrieved {len(hosts)} hosts")
|
|
223
|
-
print(f"\n📋 Found {len(hosts)} hosts from all providers:")
|
|
224
|
-
print("-" * 80)
|
|
225
|
-
for i, host in enumerate(hosts, 1):
|
|
226
|
-
status = getattr(host, 'status', host.metadata.get('status', 'unknown'))
|
|
227
|
-
sources = host.metadata.get('sources', ['unknown'])
|
|
228
|
-
source_str = ', '.join(sources) if isinstance(sources, list) else str(sources)
|
|
229
|
-
print(f"{i:3d}. {host.name:<25} {host.ip:<15} [{status:<8}] ({source_str})")
|
|
230
|
-
print("-" * 80)
|
|
231
|
-
else:
|
|
232
|
-
logger.warning("No hosts found matching the filters")
|
|
233
|
-
print("⚠️ No hosts found matching the configured filters")
|
|
234
|
-
print("Check your SoT provider filters in the configuration")
|
|
235
|
-
|
|
236
|
-
logger.info("SSHplex CLI debug mode completed successfully")
|
|
237
|
-
print("\n✅ CLI debug mode completed successfully")
|
|
238
|
-
print("Note: For the full TUI interface, run the main application")
|
|
239
|
-
return 0
|
|
240
|
-
|
|
241
|
-
|
|
242
148
|
if __name__ == "__main__":
|
|
243
149
|
sys.exit(main())
|
|
@@ -22,13 +22,13 @@ class HostCache:
|
|
|
22
22
|
"""Initialize the cache manager.
|
|
23
23
|
|
|
24
24
|
Args:
|
|
25
|
-
cache_dir: Directory to store cache files (defaults to
|
|
25
|
+
cache_dir: Directory to store cache files (defaults to ~/.cache/sshplex)
|
|
26
26
|
cache_ttl_hours: Cache time-to-live in hours (default 24 hours)
|
|
27
27
|
"""
|
|
28
28
|
self.logger = get_logger()
|
|
29
29
|
|
|
30
30
|
if cache_dir is None:
|
|
31
|
-
cache_dir = os.path.expanduser("
|
|
31
|
+
cache_dir = os.path.expanduser("~/.cache/sshplex")
|
|
32
32
|
|
|
33
33
|
self.cache_dir = Path(cache_dir)
|
|
34
34
|
self.cache_ttl = timedelta(hours=cache_ttl_hours)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Shared command helpers for main and CLI entry points."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .cache import HostCache
|
|
8
|
+
from .config import get_config_info
|
|
9
|
+
from .sot.factory import SoTFactory
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def show_config_info() -> int:
|
|
13
|
+
"""Show configuration file paths and status."""
|
|
14
|
+
info = get_config_info()
|
|
15
|
+
|
|
16
|
+
print("📁 SSHplex Configuration Information")
|
|
17
|
+
print("=" * 50)
|
|
18
|
+
print(f"Config Directory: {info['default_config_path'].rsplit('/', 1)[0]}")
|
|
19
|
+
print(f"Config File: {info['default_config_path']}")
|
|
20
|
+
print(f"Config Exists: {'✅ Yes' if info['default_config_exists'] else '❌ No'}")
|
|
21
|
+
print(f"Template File: {info['template_path']}")
|
|
22
|
+
print(f"Template Exists: {'✅ Yes' if info['template_exists'] else '❌ No'}")
|
|
23
|
+
print()
|
|
24
|
+
|
|
25
|
+
if not info['default_config_exists']:
|
|
26
|
+
print("💡 Run 'sshplex' to create a default configuration file")
|
|
27
|
+
|
|
28
|
+
return 0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def clear_cache(config: Any, logger: Any, no_cache_message: str = "No cache to clear") -> int:
|
|
32
|
+
"""Clear the host cache."""
|
|
33
|
+
logger.info("Clearing host cache")
|
|
34
|
+
|
|
35
|
+
cache = HostCache(
|
|
36
|
+
cache_dir=config.cache.cache_dir,
|
|
37
|
+
cache_ttl_hours=config.cache.ttl_hours,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
cache_info = cache.get_cache_info()
|
|
41
|
+
if cache_info:
|
|
42
|
+
print(
|
|
43
|
+
"🗑️ Clearing cache "
|
|
44
|
+
f"({cache_info.get('host_count', 0)} hosts, age: {cache_info.get('age_hours', 0):.1f}h)"
|
|
45
|
+
)
|
|
46
|
+
else:
|
|
47
|
+
print(f"🗑️ {no_cache_message}")
|
|
48
|
+
return 0
|
|
49
|
+
|
|
50
|
+
if cache.clear_cache():
|
|
51
|
+
print("✅ Cache cleared successfully")
|
|
52
|
+
return 0
|
|
53
|
+
|
|
54
|
+
print("❌ Failed to clear cache")
|
|
55
|
+
return 1
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def run_debug_mode(config: Any, logger: Any, footer_note: str = "") -> int:
|
|
59
|
+
"""Run provider connectivity + host retrieval debug flow."""
|
|
60
|
+
logger.info("Running debug mode - SoT provider connectivity test")
|
|
61
|
+
|
|
62
|
+
sot_factory = SoTFactory(config)
|
|
63
|
+
|
|
64
|
+
cache_info = sot_factory.get_cache_info()
|
|
65
|
+
if cache_info:
|
|
66
|
+
print(f"📦 Cache: {cache_info.get('host_count', 0)} hosts cached ({cache_info.get('age_hours', 0):.1f}h old)")
|
|
67
|
+
|
|
68
|
+
if not sot_factory.initialize_providers():
|
|
69
|
+
logger.error("Failed to initialize any SoT providers")
|
|
70
|
+
print("❌ Failed to initialize any SoT providers")
|
|
71
|
+
print("Check your configuration and network connectivity")
|
|
72
|
+
return 1
|
|
73
|
+
|
|
74
|
+
print(
|
|
75
|
+
"✅ Successfully initialized "
|
|
76
|
+
f"{sot_factory.get_provider_count()} SoT provider(s): {', '.join(sot_factory.get_provider_names())}"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
logger.info("Testing SoT provider connections...")
|
|
80
|
+
connection_results = sot_factory.test_all_connections()
|
|
81
|
+
|
|
82
|
+
for provider_name, status in connection_results.items():
|
|
83
|
+
if status:
|
|
84
|
+
print(f"✅ {provider_name}: Connection successful")
|
|
85
|
+
else:
|
|
86
|
+
print(f"❌ {provider_name}: Connection failed")
|
|
87
|
+
|
|
88
|
+
logger.info("Retrieving hosts from all SoT providers...")
|
|
89
|
+
hosts = sot_factory.get_all_hosts()
|
|
90
|
+
|
|
91
|
+
if hosts:
|
|
92
|
+
logger.info(f"Successfully retrieved {len(hosts)} hosts")
|
|
93
|
+
print(f"\n📋 Found {len(hosts)} hosts from all providers:")
|
|
94
|
+
print("-" * 80)
|
|
95
|
+
for i, host in enumerate(hosts, 1):
|
|
96
|
+
status = getattr(host, 'status', host.metadata.get('status', 'unknown'))
|
|
97
|
+
sources = host.metadata.get('sources', ['unknown'])
|
|
98
|
+
source_str = ', '.join(sources) if isinstance(sources, list) else str(sources)
|
|
99
|
+
print(f"{i:3d}. {host.name:<25} {host.ip:<15} [{status:<8}] ({source_str})")
|
|
100
|
+
print("-" * 80)
|
|
101
|
+
else:
|
|
102
|
+
logger.warning("No hosts found matching the filters")
|
|
103
|
+
print("⚠️ No hosts found matching the configured filters")
|
|
104
|
+
print("Check your SoT provider filters in the configuration")
|
|
105
|
+
|
|
106
|
+
logger.info("SSHplex debug mode completed successfully")
|
|
107
|
+
print("\n✅ Debug mode completed successfully")
|
|
108
|
+
if footer_note:
|
|
109
|
+
print(footer_note)
|
|
110
|
+
return 0
|
|
@@ -34,6 +34,7 @@ class LoggingConfig(BaseModel):
|
|
|
34
34
|
|
|
35
35
|
class UIConfig(BaseModel):
|
|
36
36
|
"""User interface configuration."""
|
|
37
|
+
theme: str = "textual-dark"
|
|
37
38
|
show_log_panel: bool = True
|
|
38
39
|
log_panel_height: int = 20 # Percentage of screen height
|
|
39
40
|
table_columns: list = Field(default_factory=lambda: ["name", "ip", "cluster", "role", "tags"])
|
|
@@ -190,7 +191,7 @@ class SoTConfig(BaseModel):
|
|
|
190
191
|
class CacheConfig(BaseModel):
|
|
191
192
|
"""Host cache configuration."""
|
|
192
193
|
enabled: bool = True
|
|
193
|
-
cache_dir: str = "
|
|
194
|
+
cache_dir: str = "~/.cache/sshplex"
|
|
194
195
|
ttl_hours: int = Field(default=24, description="Cache time-to-live in hours")
|
|
195
196
|
|
|
196
197
|
|
|
@@ -283,10 +284,7 @@ def load_config(config_path: Optional[str] = None) -> Config:
|
|
|
283
284
|
print(" - netbox.token: Your NetBox API token")
|
|
284
285
|
print(" - ssh.username: Your SSH username")
|
|
285
286
|
print(" - ssh.key_path: Path to your SSH private key")
|
|
286
|
-
print("\n🚀
|
|
287
|
-
# Exit gracefully to let user configure
|
|
288
|
-
import sys
|
|
289
|
-
sys.exit(0)
|
|
287
|
+
print("\n🚀 Continuing with generated defaults. You can adjust settings later in the Config editor (key: e).")
|
|
290
288
|
except Exception as e:
|
|
291
289
|
raise FileNotFoundError(f"SSHplex: Could not initialize default config: {e}") from e
|
|
292
290
|
else:
|
|
@@ -12,11 +12,6 @@ from ..logger import get_logger
|
|
|
12
12
|
from .base import MultiplexerBase
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
class TmuxError(Exception):
|
|
16
|
-
"""Raised when tmux operations fail."""
|
|
17
|
-
pass
|
|
18
|
-
|
|
19
|
-
|
|
20
15
|
class TmuxManager(MultiplexerBase):
|
|
21
16
|
"""tmux implementation for SSHplex multiplexer."""
|
|
22
17
|
|
|
@@ -517,30 +512,3 @@ class TmuxManager(MultiplexerBase):
|
|
|
517
512
|
except Exception as e:
|
|
518
513
|
self.logger.error(f"SSHplex: Failed to disable broadcast mode: {e}")
|
|
519
514
|
return False
|
|
520
|
-
|
|
521
|
-
def toggle_broadcast(self) -> bool:
|
|
522
|
-
"""Toggle broadcast mode for all windows in the session."""
|
|
523
|
-
try:
|
|
524
|
-
if not self.session:
|
|
525
|
-
self.logger.error("SSHplex: No session available for broadcast")
|
|
526
|
-
return False
|
|
527
|
-
|
|
528
|
-
# Check current broadcast state of first window with multiple panes
|
|
529
|
-
current_state = False
|
|
530
|
-
for window in self.windows.values():
|
|
531
|
-
if window and len(window.panes) > 1:
|
|
532
|
-
# Get current synchronize-panes setting
|
|
533
|
-
result = window.cmd('show-window-options', '-v', 'synchronize-panes')
|
|
534
|
-
if result and hasattr(result, 'stdout') and result.stdout:
|
|
535
|
-
current_state = result.stdout[0].strip() == 'on'
|
|
536
|
-
break
|
|
537
|
-
|
|
538
|
-
# Toggle the state
|
|
539
|
-
if current_state:
|
|
540
|
-
return self.disable_broadcast()
|
|
541
|
-
else:
|
|
542
|
-
return self.enable_broadcast()
|
|
543
|
-
|
|
544
|
-
except Exception as e:
|
|
545
|
-
self.logger.error(f"SSHplex: Failed to toggle broadcast mode: {e}")
|
|
546
|
-
return False
|
|
@@ -340,7 +340,7 @@ class OnboardingWizard:
|
|
|
340
340
|
self.console.print("\n🔄 Testing connection...")
|
|
341
341
|
|
|
342
342
|
try:
|
|
343
|
-
import pynetbox
|
|
343
|
+
import pynetbox # type: ignore[import-untyped]
|
|
344
344
|
nb = pynetbox.api(config['url'], token=config['token'])
|
|
345
345
|
if config.get('verify_ssl') is False:
|
|
346
346
|
nb.http_session.verify = False
|