sshplex 1.6.4__tar.gz → 1.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sshplex-1.6.4/sshplex.egg-info → sshplex-1.8.0}/PKG-INFO +7 -5
- {sshplex-1.6.4 → sshplex-1.8.0}/README.md +4 -1
- {sshplex-1.6.4 → sshplex-1.8.0}/pyproject.toml +3 -4
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex/__init__.py +1 -1
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex/cli.py +8 -0
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex/config-template.yaml +23 -1
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex/lib/cache.py +29 -23
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex/lib/config.py +97 -23
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex/lib/onboarding/wizard.py +250 -21
- sshplex-1.8.0/sshplex/lib/sot/base.py +108 -0
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex/lib/sot/factory.py +189 -66
- sshplex-1.8.0/sshplex/lib/sot/git.py +517 -0
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex/lib/sot/netbox.py +17 -2
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex/lib/ui/config_editor.py +634 -95
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex/lib/ui/host_selector.py +85 -11
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex/lib/ui/session_manager.py +48 -48
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex/sshplex_connector.py +46 -21
- {sshplex-1.6.4 → sshplex-1.8.0/sshplex.egg-info}/PKG-INFO +7 -5
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex.egg-info/SOURCES.txt +4 -1
- {sshplex-1.6.4 → sshplex-1.8.0}/tests/test_cache.py +7 -0
- {sshplex-1.6.4 → sshplex-1.8.0}/tests/test_config.py +21 -0
- sshplex-1.8.0/tests/test_sshplex_connector.py +84 -0
- sshplex-1.8.0/tests/test_ui_config_editor_columns.py +35 -0
- sshplex-1.6.4/sshplex/lib/sot/base.py +0 -57
- {sshplex-1.6.4 → sshplex-1.8.0}/LICENSE +0 -0
- {sshplex-1.6.4 → sshplex-1.8.0}/MANIFEST.in +0 -0
- {sshplex-1.6.4 → sshplex-1.8.0}/setup.cfg +0 -0
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex/lib/__init__.py +0 -0
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex/lib/commands.py +0 -0
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex/lib/logger.py +0 -0
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex/lib/multiplexer/__init__.py +0 -0
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex/lib/multiplexer/base.py +0 -0
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex/lib/multiplexer/iterm2_native.py +0 -0
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex/lib/multiplexer/tmux.py +0 -0
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex/lib/onboarding/__init__.py +0 -0
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex/lib/sot/__init__.py +0 -0
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex/lib/sot/ansible.py +0 -0
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex/lib/sot/consul.py +0 -0
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex/lib/sot/static.py +0 -0
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex/lib/ui/__init__.py +0 -0
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex/lib/utils/__init__.py +0 -0
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex/lib/utils/iterm2.py +0 -0
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex/lib/utils/ssh_config.py +0 -0
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex/main.py +0 -0
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex.egg-info/dependency_links.txt +0 -0
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex.egg-info/entry_points.txt +0 -0
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex.egg-info/requires.txt +0 -0
- {sshplex-1.6.4 → sshplex-1.8.0}/sshplex.egg-info/top_level.txt +0 -0
- {sshplex-1.6.4 → sshplex-1.8.0}/tests/test_commands.py +0 -0
- {sshplex-1.6.4 → sshplex-1.8.0}/tests/test_iterm2_session_manager.py +0 -0
- {sshplex-1.6.4 → sshplex-1.8.0}/tests/test_main.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sshplex
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8.0
|
|
4
4
|
Summary: Multiplex your SSH connections with style
|
|
5
5
|
Author-email: MJAHED Sabri <contact@sabrimjahed.com>
|
|
6
6
|
License: MIT
|
|
@@ -17,15 +17,14 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
17
17
|
Classifier: Operating System :: POSIX :: Linux
|
|
18
18
|
Classifier: Operating System :: MacOS
|
|
19
19
|
Classifier: Programming Language :: Python :: 3
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
22
20
|
Classifier: Programming Language :: Python :: 3.10
|
|
23
21
|
Classifier: Programming Language :: Python :: 3.11
|
|
24
22
|
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
24
|
Classifier: Topic :: System :: Networking
|
|
26
25
|
Classifier: Topic :: System :: Systems Administration
|
|
27
26
|
Classifier: Topic :: Terminals
|
|
28
|
-
Requires-Python: >=3.
|
|
27
|
+
Requires-Python: >=3.10
|
|
29
28
|
Description-Content-Type: text/markdown
|
|
30
29
|
License-File: LICENSE
|
|
31
30
|
Requires-Dist: pynetbox==7.6.1
|
|
@@ -90,7 +89,7 @@ sshplex
|
|
|
90
89
|
|
|
91
90
|
### Prerequisites
|
|
92
91
|
|
|
93
|
-
- Python 3.
|
|
92
|
+
- Python 3.10+
|
|
94
93
|
- tmux (Linux/macOS) and/or iTerm2 (macOS)
|
|
95
94
|
- SSH key configured for target hosts
|
|
96
95
|
|
|
@@ -110,9 +109,12 @@ sshplex
|
|
|
110
109
|
| **NetBox** | `netbox` | None (included in base install) | Inventory-driven infrastructure with metadata |
|
|
111
110
|
| **Ansible** | `ansible` | None | Reusing existing Ansible inventory files |
|
|
112
111
|
| **Consul** | `consul` | `pip install "sshplex[consul]"` | Service discovery and dynamic node catalogs |
|
|
112
|
+
| **Git** | `git` | `git` binary in PATH | Git-backed inventories with auto-pull (`static` or `ansible` YAML) |
|
|
113
113
|
|
|
114
114
|
Provider activation is controlled by `sot.providers`, and each source is configured as an item in `sot.import`.
|
|
115
115
|
|
|
116
|
+
Use multiple `git` imports and tune `priority` for deterministic overrides.
|
|
117
|
+
|
|
116
118
|
|
|
117
119
|
## Local Demo (Consul + Ansible)
|
|
118
120
|
|
|
@@ -37,7 +37,7 @@ sshplex
|
|
|
37
37
|
|
|
38
38
|
### Prerequisites
|
|
39
39
|
|
|
40
|
-
- Python 3.
|
|
40
|
+
- Python 3.10+
|
|
41
41
|
- tmux (Linux/macOS) and/or iTerm2 (macOS)
|
|
42
42
|
- SSH key configured for target hosts
|
|
43
43
|
|
|
@@ -57,9 +57,12 @@ sshplex
|
|
|
57
57
|
| **NetBox** | `netbox` | None (included in base install) | Inventory-driven infrastructure with metadata |
|
|
58
58
|
| **Ansible** | `ansible` | None | Reusing existing Ansible inventory files |
|
|
59
59
|
| **Consul** | `consul` | `pip install "sshplex[consul]"` | Service discovery and dynamic node catalogs |
|
|
60
|
+
| **Git** | `git` | `git` binary in PATH | Git-backed inventories with auto-pull (`static` or `ansible` YAML) |
|
|
60
61
|
|
|
61
62
|
Provider activation is controlled by `sot.providers`, and each source is configured as an item in `sot.import`.
|
|
62
63
|
|
|
64
|
+
Use multiple `git` imports and tune `priority` for deterministic overrides.
|
|
65
|
+
|
|
63
66
|
|
|
64
67
|
## Local Demo (Consul + Ansible)
|
|
65
68
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sshplex"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.8.0"
|
|
8
8
|
description = "Multiplex your SSH connections with style"
|
|
9
9
|
authors = [{name = "MJAHED Sabri", email = "contact@sabrimjahed.com"}]
|
|
10
10
|
readme = "README.md"
|
|
@@ -18,17 +18,16 @@ classifiers = [
|
|
|
18
18
|
"Operating System :: POSIX :: Linux",
|
|
19
19
|
"Operating System :: MacOS",
|
|
20
20
|
"Programming Language :: Python :: 3",
|
|
21
|
-
"Programming Language :: Python :: 3.8",
|
|
22
|
-
"Programming Language :: Python :: 3.9",
|
|
23
21
|
"Programming Language :: Python :: 3.10",
|
|
24
22
|
"Programming Language :: Python :: 3.11",
|
|
25
23
|
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
26
25
|
"Topic :: System :: Networking",
|
|
27
26
|
"Topic :: System :: Systems Administration",
|
|
28
27
|
"Topic :: Terminals",
|
|
29
28
|
]
|
|
30
29
|
keywords = ["ssh", "tmux", "multiplexer", "netbox", "tui", "terminal"]
|
|
31
|
-
requires-python = ">=3.
|
|
30
|
+
requires-python = ">=3.10"
|
|
32
31
|
dependencies = [
|
|
33
32
|
"pynetbox==7.6.1",
|
|
34
33
|
"textual==8.0.0",
|
|
@@ -127,6 +127,8 @@ def list_providers(config: Any, logger: Any) -> int:
|
|
|
127
127
|
status_icon = "📝"
|
|
128
128
|
elif provider.type == "consul":
|
|
129
129
|
status_icon = "🔍"
|
|
130
|
+
elif provider.type == "git":
|
|
131
|
+
status_icon = "🔄"
|
|
130
132
|
|
|
131
133
|
print(f"{i}. {status_icon} {provider.name}")
|
|
132
134
|
print(f" Type: {provider.type}")
|
|
@@ -137,6 +139,12 @@ def list_providers(config: Any, logger: Any) -> int:
|
|
|
137
139
|
print(f" Paths: {', '.join(provider.inventory_paths)}")
|
|
138
140
|
elif provider.type == "consul" and provider.config:
|
|
139
141
|
print(f" Host: {provider.config.host}:{provider.config.port}")
|
|
142
|
+
elif provider.type == "git" and provider.repo_url:
|
|
143
|
+
branch = provider.branch or "main"
|
|
144
|
+
inventory_format = provider.inventory_format or "static"
|
|
145
|
+
print(f" Repo: {provider.repo_url} [{branch}, {inventory_format}]")
|
|
146
|
+
source_pattern = provider.source_pattern or "hosts/**/*.y*ml"
|
|
147
|
+
print(f" Source: {source_pattern}")
|
|
140
148
|
elif provider.type == "static" and provider.hosts:
|
|
141
149
|
print(f" Hosts: {len(provider.hosts)} defined")
|
|
142
150
|
|
|
@@ -4,7 +4,7 @@ sshplex:
|
|
|
4
4
|
|
|
5
5
|
# Source of Truth configuration
|
|
6
6
|
sot:
|
|
7
|
-
providers: ["static", "netbox", "ansible", "consul"] # Available: static, netbox, ansible, consul
|
|
7
|
+
providers: ["static", "netbox", "ansible", "consul", "git"] # Available: static, netbox, ansible, consul, git
|
|
8
8
|
import:
|
|
9
9
|
- name: "production-servers"
|
|
10
10
|
type: static
|
|
@@ -71,6 +71,28 @@ sot:
|
|
|
71
71
|
verify: false
|
|
72
72
|
dc: "lisbon"
|
|
73
73
|
cert: "" # Path to SSL certificate (optional)
|
|
74
|
+
- name: "personal-git-hosts"
|
|
75
|
+
type: git
|
|
76
|
+
repo_url: "git@github.com:your-user/sshplex-hosts.git"
|
|
77
|
+
branch: "main"
|
|
78
|
+
source_pattern: "hosts/**/*.y*ml"
|
|
79
|
+
inventory_format: "static" # static, ansible
|
|
80
|
+
auto_pull: true
|
|
81
|
+
pull_interval_seconds: 300
|
|
82
|
+
priority: 100
|
|
83
|
+
pull_strategy: "ff-only"
|
|
84
|
+
- name: "git-ansible-inventory"
|
|
85
|
+
type: git
|
|
86
|
+
repo_url: "git@github.com:your-org/ansible-inventory.git"
|
|
87
|
+
branch: "main"
|
|
88
|
+
source_pattern: "inventory/**/*.y*ml"
|
|
89
|
+
inventory_format: "ansible"
|
|
90
|
+
default_filters:
|
|
91
|
+
groups: ["webservers", "databases"]
|
|
92
|
+
auto_pull: true
|
|
93
|
+
pull_interval_seconds: 300
|
|
94
|
+
priority: 50
|
|
95
|
+
pull_strategy: "ff-only"
|
|
74
96
|
|
|
75
97
|
ssh:
|
|
76
98
|
username: "admin"
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""SSHplex host cache management for optimized startup performance."""
|
|
2
2
|
|
|
3
|
+
import contextlib
|
|
3
4
|
import os
|
|
5
|
+
import tempfile
|
|
4
6
|
import threading
|
|
5
7
|
from datetime import datetime, timedelta
|
|
6
8
|
from pathlib import Path
|
|
@@ -28,9 +30,9 @@ class HostCache:
|
|
|
28
30
|
self.logger = get_logger()
|
|
29
31
|
|
|
30
32
|
if cache_dir is None:
|
|
31
|
-
cache_dir =
|
|
33
|
+
cache_dir = "~/.cache/sshplex"
|
|
32
34
|
|
|
33
|
-
self.cache_dir = Path(cache_dir)
|
|
35
|
+
self.cache_dir = Path(cache_dir).expanduser()
|
|
34
36
|
self.cache_ttl = timedelta(hours=cache_ttl_hours)
|
|
35
37
|
self.cache_file = self.cache_dir / "hosts.yaml"
|
|
36
38
|
self.metadata_file = self.cache_dir / "cache_metadata.yaml"
|
|
@@ -70,19 +72,37 @@ class HostCache:
|
|
|
70
72
|
with open(self.metadata_file) as f:
|
|
71
73
|
metadata = yaml.safe_load(f)
|
|
72
74
|
|
|
73
|
-
if not metadata or 'timestamp' not in metadata:
|
|
75
|
+
if not isinstance(metadata, dict) or 'timestamp' not in metadata:
|
|
74
76
|
return False
|
|
75
77
|
|
|
76
78
|
cache_time = datetime.fromisoformat(metadata['timestamp'])
|
|
77
79
|
return datetime.now() - cache_time < self.cache_ttl
|
|
78
80
|
|
|
79
|
-
except (yaml.YAMLError, ValueError, KeyError) as e:
|
|
81
|
+
except (yaml.YAMLError, ValueError, KeyError, OSError) as e:
|
|
80
82
|
self.logger.warning(f"Failed to validate cache: {e}")
|
|
81
83
|
return False
|
|
82
84
|
except Exception as e:
|
|
83
85
|
self.logger.error(f"Unexpected error validating cache: {e}")
|
|
84
86
|
return False
|
|
85
87
|
|
|
88
|
+
def _atomic_write_yaml(self, file_path: Path, payload: Any) -> None:
|
|
89
|
+
"""Write YAML payload atomically to avoid partial/corrupt cache writes."""
|
|
90
|
+
fd, temp_path_text = tempfile.mkstemp(
|
|
91
|
+
prefix=f".{file_path.name}.",
|
|
92
|
+
suffix=".tmp",
|
|
93
|
+
dir=str(self.cache_dir),
|
|
94
|
+
text=True,
|
|
95
|
+
)
|
|
96
|
+
temp_path = Path(temp_path_text)
|
|
97
|
+
try:
|
|
98
|
+
with os.fdopen(fd, "w") as handle:
|
|
99
|
+
yaml.safe_dump(payload, handle, default_flow_style=False, sort_keys=True)
|
|
100
|
+
os.replace(temp_path, file_path)
|
|
101
|
+
finally:
|
|
102
|
+
if temp_path.exists():
|
|
103
|
+
with contextlib.suppress(OSError):
|
|
104
|
+
temp_path.unlink()
|
|
105
|
+
|
|
86
106
|
def save_hosts(self, hosts: List[Host], provider_info: Dict[str, Any]) -> bool:
|
|
87
107
|
"""Save hosts to cache with metadata.
|
|
88
108
|
|
|
@@ -95,19 +115,10 @@ class HostCache:
|
|
|
95
115
|
"""
|
|
96
116
|
with self._lock:
|
|
97
117
|
try:
|
|
98
|
-
|
|
99
|
-
hosts_data = []
|
|
100
|
-
for host in hosts:
|
|
101
|
-
host_dict = {
|
|
102
|
-
'name': host.name,
|
|
103
|
-
'ip': host.ip,
|
|
104
|
-
'metadata': host.metadata
|
|
105
|
-
}
|
|
106
|
-
hosts_data.append(host_dict)
|
|
118
|
+
hosts_data = [host.to_dict() for host in hosts]
|
|
107
119
|
|
|
108
120
|
# Save hosts data
|
|
109
|
-
|
|
110
|
-
yaml.dump(hosts_data, f, default_flow_style=False, sort_keys=True)
|
|
121
|
+
self._atomic_write_yaml(self.cache_file, hosts_data)
|
|
111
122
|
|
|
112
123
|
# Save metadata
|
|
113
124
|
cache_metadata = {
|
|
@@ -117,8 +128,7 @@ class HostCache:
|
|
|
117
128
|
'cache_version': '1.0'
|
|
118
129
|
}
|
|
119
130
|
|
|
120
|
-
|
|
121
|
-
yaml.dump(cache_metadata, f, default_flow_style=False, sort_keys=True)
|
|
131
|
+
self._atomic_write_yaml(self.metadata_file, cache_metadata)
|
|
122
132
|
|
|
123
133
|
self.logger.info(f"Successfully cached {len(hosts)} hosts to {self.cache_file}")
|
|
124
134
|
return True
|
|
@@ -155,12 +165,8 @@ class HostCache:
|
|
|
155
165
|
if 'name' not in host_dict or 'ip' not in host_dict:
|
|
156
166
|
self.logger.warning(f"Skipping host with missing required fields: {host_dict}")
|
|
157
167
|
continue
|
|
158
|
-
|
|
159
|
-
host = Host(
|
|
160
|
-
name=host_dict['name'],
|
|
161
|
-
ip=host_dict['ip'],
|
|
162
|
-
**host_dict.get('metadata', {})
|
|
163
|
-
)
|
|
168
|
+
|
|
169
|
+
host = Host.from_dict(host_dict)
|
|
164
170
|
hosts.append(host)
|
|
165
171
|
|
|
166
172
|
self.logger.info(f"Successfully loaded {len(hosts)} hosts from cache")
|
|
@@ -5,10 +5,27 @@ from pathlib import Path
|
|
|
5
5
|
from typing import Any, Dict, List, Optional
|
|
6
6
|
|
|
7
7
|
import yaml
|
|
8
|
-
from pydantic import BaseModel, Field
|
|
8
|
+
from pydantic import BaseModel, Field, field_validator
|
|
9
9
|
|
|
10
10
|
from .. import __version__
|
|
11
11
|
|
|
12
|
+
SUPPORTED_SOT_PROVIDER_TYPES = ("static", "netbox", "ansible", "consul", "git")
|
|
13
|
+
SOT_PROVIDER_LABELS = {
|
|
14
|
+
"static": "Static",
|
|
15
|
+
"netbox": "NetBox",
|
|
16
|
+
"ansible": "Ansible",
|
|
17
|
+
"consul": "Consul",
|
|
18
|
+
"git": "Git",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
SUPPORTED_MUX_BACKENDS = ("tmux", "iterm2-native")
|
|
22
|
+
MUX_BACKEND_LABELS = {
|
|
23
|
+
"tmux": "tmux",
|
|
24
|
+
"iterm2-native": "iTerm2 Native",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
SUPPORTED_GIT_INVENTORY_FORMATS = ("static", "ansible")
|
|
28
|
+
|
|
12
29
|
|
|
13
30
|
class SSHplexConfig(BaseModel):
|
|
14
31
|
"""SSHplex main configuration."""
|
|
@@ -16,15 +33,6 @@ class SSHplexConfig(BaseModel):
|
|
|
16
33
|
session_prefix: str = "sshplex"
|
|
17
34
|
|
|
18
35
|
|
|
19
|
-
class NetBoxConfig(BaseModel):
|
|
20
|
-
"""NetBox connection configuration."""
|
|
21
|
-
url: str = Field(..., description="NetBox instance URL")
|
|
22
|
-
token: str = Field(..., description="NetBox API token")
|
|
23
|
-
verify_ssl: bool = True
|
|
24
|
-
timeout: int = 30
|
|
25
|
-
default_filters: Dict[str, str] = Field(default_factory=dict)
|
|
26
|
-
|
|
27
|
-
|
|
28
36
|
class LoggingConfig(BaseModel):
|
|
29
37
|
"""Logging configuration."""
|
|
30
38
|
enabled: bool = True
|
|
@@ -42,7 +50,7 @@ class UIConfig(BaseModel):
|
|
|
42
50
|
class Proxy(BaseModel):
|
|
43
51
|
"""ImportProxies configuration with defaults."""
|
|
44
52
|
name: str = Field("", description="Proxy name")
|
|
45
|
-
imports: list = Field(
|
|
53
|
+
imports: list = Field(default_factory=list, description="List of imports that will use this proxy")
|
|
46
54
|
host: str = Field("", description="Proxy host or ip")
|
|
47
55
|
username: str = Field("", description="Proxy username")
|
|
48
56
|
key_path: str = Field("", description="Proxy key")
|
|
@@ -109,6 +117,16 @@ class TmuxConfig(BaseModel):
|
|
|
109
117
|
description="Split pattern for iTerm2 native: alternate, vertical, horizontal"
|
|
110
118
|
)
|
|
111
119
|
|
|
120
|
+
@field_validator("backend")
|
|
121
|
+
@classmethod
|
|
122
|
+
def validate_backend_value(cls, value: str) -> str:
|
|
123
|
+
normalized = str(value or "").strip().lower()
|
|
124
|
+
if normalized not in SUPPORTED_MUX_BACKENDS:
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f"Invalid backend: {value}. Must be one of: {list(SUPPORTED_MUX_BACKENDS)}"
|
|
127
|
+
)
|
|
128
|
+
return normalized
|
|
129
|
+
|
|
112
130
|
def validate_backend_config(self) -> bool:
|
|
113
131
|
"""Validate backend configuration.
|
|
114
132
|
|
|
@@ -118,10 +136,9 @@ class TmuxConfig(BaseModel):
|
|
|
118
136
|
import platform
|
|
119
137
|
|
|
120
138
|
# Validate backend option
|
|
121
|
-
|
|
122
|
-
if self.backend not in valid_backends:
|
|
139
|
+
if self.backend not in SUPPORTED_MUX_BACKENDS:
|
|
123
140
|
raise ValueError(
|
|
124
|
-
f"Invalid backend: {self.backend}. Must be one of: {
|
|
141
|
+
f"Invalid backend: {self.backend}. Must be one of: {list(SUPPORTED_MUX_BACKENDS)}"
|
|
125
142
|
)
|
|
126
143
|
|
|
127
144
|
# Validate iTerm2 native mode on macOS only
|
|
@@ -146,11 +163,6 @@ class TmuxConfig(BaseModel):
|
|
|
146
163
|
return True
|
|
147
164
|
|
|
148
165
|
|
|
149
|
-
class AnsibleConfig(BaseModel):
|
|
150
|
-
"""Ansible inventory configuration."""
|
|
151
|
-
inventory_paths: List[str] = Field(default_factory=list, description="List of paths to Ansible inventory YAML files")
|
|
152
|
-
default_filters: Dict[str, Any] = Field(default_factory=dict)
|
|
153
|
-
|
|
154
166
|
class ConsulConfig(BaseModel):
|
|
155
167
|
"""Consul-specific configuration with defaults."""
|
|
156
168
|
host: str = Field("consul.example.com", description="Consul host address")
|
|
@@ -164,7 +176,7 @@ class ConsulConfig(BaseModel):
|
|
|
164
176
|
class SoTImportConfig(BaseModel):
|
|
165
177
|
"""Individual SoT import configuration."""
|
|
166
178
|
name: str = Field(..., description="Unique name for this import")
|
|
167
|
-
type: str = Field(..., description="Provider type:
|
|
179
|
+
type: str = Field(..., description=f"Provider type: {', '.join(SUPPORTED_SOT_PROVIDER_TYPES)}")
|
|
168
180
|
|
|
169
181
|
# Static provider fields
|
|
170
182
|
hosts: Optional[List[Dict[str, Any]]] = None
|
|
@@ -182,11 +194,75 @@ class SoTImportConfig(BaseModel):
|
|
|
182
194
|
# Consul provider fields
|
|
183
195
|
config: Optional[ConsulConfig] = None
|
|
184
196
|
|
|
197
|
+
# Git provider fields
|
|
198
|
+
repo_url: Optional[str] = None
|
|
199
|
+
branch: Optional[str] = "main"
|
|
200
|
+
source_pattern: Optional[str] = "hosts/**/*.y*ml"
|
|
201
|
+
auto_pull: Optional[bool] = True
|
|
202
|
+
pull_interval_seconds: Optional[int] = 300
|
|
203
|
+
priority: Optional[int] = 100
|
|
204
|
+
pull_strategy: Optional[str] = "ff-only"
|
|
205
|
+
inventory_format: Optional[str] = "static"
|
|
206
|
+
|
|
207
|
+
@field_validator("type")
|
|
208
|
+
@classmethod
|
|
209
|
+
def validate_provider_type(cls, value: str) -> str:
|
|
210
|
+
normalized = str(value or "").strip().lower()
|
|
211
|
+
if normalized not in SUPPORTED_SOT_PROVIDER_TYPES:
|
|
212
|
+
raise ValueError(
|
|
213
|
+
f"Unsupported provider type '{value}'. "
|
|
214
|
+
f"Supported values: {list(SUPPORTED_SOT_PROVIDER_TYPES)}"
|
|
215
|
+
)
|
|
216
|
+
return normalized
|
|
217
|
+
|
|
218
|
+
@field_validator("pull_strategy")
|
|
219
|
+
@classmethod
|
|
220
|
+
def validate_pull_strategy(cls, value: Optional[str]) -> Optional[str]:
|
|
221
|
+
if value is None:
|
|
222
|
+
return value
|
|
223
|
+
normalized = str(value).strip().lower()
|
|
224
|
+
if normalized and normalized != "ff-only":
|
|
225
|
+
raise ValueError("git pull_strategy only supports 'ff-only'")
|
|
226
|
+
return normalized or "ff-only"
|
|
227
|
+
|
|
228
|
+
@field_validator("inventory_format")
|
|
229
|
+
@classmethod
|
|
230
|
+
def validate_inventory_format(cls, value: Optional[str]) -> Optional[str]:
|
|
231
|
+
if value is None:
|
|
232
|
+
return value
|
|
233
|
+
normalized = str(value).strip().lower()
|
|
234
|
+
if normalized not in SUPPORTED_GIT_INVENTORY_FORMATS:
|
|
235
|
+
raise ValueError(
|
|
236
|
+
f"Unsupported git inventory format '{value}'. "
|
|
237
|
+
f"Supported values: {list(SUPPORTED_GIT_INVENTORY_FORMATS)}"
|
|
238
|
+
)
|
|
239
|
+
return normalized
|
|
240
|
+
|
|
185
241
|
class SoTConfig(BaseModel):
|
|
186
242
|
"""Source of Truth configuration."""
|
|
187
|
-
providers: List[str] = Field(
|
|
243
|
+
providers: List[str] = Field(
|
|
244
|
+
default_factory=list,
|
|
245
|
+
description="List of SoT providers to use: static, netbox, ansible, consul, git",
|
|
246
|
+
)
|
|
188
247
|
import_: List[SoTImportConfig] = Field(alias='import', default_factory=list, description="List of import configurations")
|
|
189
248
|
|
|
249
|
+
@field_validator("providers")
|
|
250
|
+
@classmethod
|
|
251
|
+
def validate_enabled_providers(cls, value: List[str]) -> List[str]:
|
|
252
|
+
normalized: List[str] = []
|
|
253
|
+
for provider in value or []:
|
|
254
|
+
provider_name = str(provider or "").strip().lower()
|
|
255
|
+
if not provider_name:
|
|
256
|
+
continue
|
|
257
|
+
if provider_name not in SUPPORTED_SOT_PROVIDER_TYPES:
|
|
258
|
+
raise ValueError(
|
|
259
|
+
f"Unsupported provider '{provider_name}'. "
|
|
260
|
+
f"Supported values: {list(SUPPORTED_SOT_PROVIDER_TYPES)}"
|
|
261
|
+
)
|
|
262
|
+
if provider_name not in normalized:
|
|
263
|
+
normalized.append(provider_name)
|
|
264
|
+
return normalized
|
|
265
|
+
|
|
190
266
|
|
|
191
267
|
class CacheConfig(BaseModel):
|
|
192
268
|
"""Host cache configuration."""
|
|
@@ -199,8 +275,6 @@ class Config(BaseModel):
|
|
|
199
275
|
"""Main SSHplex configuration model."""
|
|
200
276
|
sshplex: SSHplexConfig = Field(default_factory=SSHplexConfig)
|
|
201
277
|
sot: SoTConfig = Field(default_factory=SoTConfig)
|
|
202
|
-
netbox: Optional[NetBoxConfig] = None
|
|
203
|
-
ansible_inventory: Optional[AnsibleConfig] = None
|
|
204
278
|
ssh: SSHConfig = Field(default_factory=SSHConfig)
|
|
205
279
|
tmux: TmuxConfig = Field(default_factory=TmuxConfig)
|
|
206
280
|
logging: LoggingConfig = Field(default_factory=LoggingConfig)
|