sshplex 1.0.4__tar.gz → 1.0.10__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.0.4/sshplex.egg-info → sshplex-1.0.10}/PKG-INFO +2 -2
- {sshplex-1.0.4 → sshplex-1.0.10}/README.md +1 -1
- {sshplex-1.0.4 → sshplex-1.0.10}/pyproject.toml +1 -1
- {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/__init__.py +1 -1
- {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/cli.py +2 -1
- {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/config-template.yaml +0 -1
- {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/lib/config.py +3 -1
- sshplex-1.0.10/sshplex/lib/sot/netbox.py +278 -0
- {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/main.py +2 -1
- {sshplex-1.0.4 → sshplex-1.0.10/sshplex.egg-info}/PKG-INFO +2 -2
- {sshplex-1.0.4 → sshplex-1.0.10}/sshplex.egg-info/SOURCES.txt +0 -1
- sshplex-1.0.4/sshplex/lib/sot/netbox.py +0 -174
- sshplex-1.0.4/sshplex/populate_examples.py +0 -0
- {sshplex-1.0.4 → sshplex-1.0.10}/LICENSE +0 -0
- {sshplex-1.0.4 → sshplex-1.0.10}/MANIFEST.in +0 -0
- {sshplex-1.0.4 → sshplex-1.0.10}/setup.cfg +0 -0
- {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/lib/__init__.py +0 -0
- {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/lib/logger.py +0 -0
- {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/lib/multiplexer/__init__.py +0 -0
- {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/lib/multiplexer/base.py +0 -0
- {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/lib/multiplexer/tmux.py +0 -0
- {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/lib/sot/__init__.py +0 -0
- {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/lib/sot/base.py +0 -0
- {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/lib/ssh/__init__.py +0 -0
- {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/lib/ssh/connection.py +0 -0
- {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/lib/ssh/manager.py +0 -0
- {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/lib/ui/__init__.py +0 -0
- {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/lib/ui/host_selector.py +0 -0
- {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/lib/ui/session_manager.py +0 -0
- {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/sshplex_connector.py +0 -0
- {sshplex-1.0.4 → sshplex-1.0.10}/sshplex.egg-info/dependency_links.txt +0 -0
- {sshplex-1.0.4 → sshplex-1.0.10}/sshplex.egg-info/entry_points.txt +0 -0
- {sshplex-1.0.4 → sshplex-1.0.10}/sshplex.egg-info/requires.txt +0 -0
- {sshplex-1.0.4 → sshplex-1.0.10}/sshplex.egg-info/top_level.txt +0 -0
- {sshplex-1.0.4 → sshplex-1.0.10}/tests/test_config.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sshplex
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.10
|
|
4
4
|
Summary: Multiplex your SSH connections with style
|
|
5
5
|
Author-email: MJAHED Sabri <contact@sabrimjahed.com>
|
|
6
6
|
License: MIT
|
|
@@ -44,7 +44,7 @@ Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
|
44
44
|
Requires-Dist: types-PyYAML>=6.0.0; extra == "dev"
|
|
45
45
|
Dynamic: license-file
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+

|
|
48
48
|
|
|
49
49
|
**Multiplex your SSH connections with style**
|
|
50
50
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sshplex"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.10"
|
|
8
8
|
description = "Multiplex your SSH connections with style"
|
|
9
9
|
authors = [{name = "MJAHED Sabri", email = "contact@sabrimjahed.com"}]
|
|
10
10
|
readme = "README.md"
|
|
@@ -4,6 +4,7 @@ import sys
|
|
|
4
4
|
import argparse
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
|
+
from . import __version__
|
|
7
8
|
from .lib.config import load_config
|
|
8
9
|
from .lib.logger import setup_logging, get_logger
|
|
9
10
|
from .lib.sot.netbox import NetBoxProvider
|
|
@@ -16,7 +17,7 @@ def main() -> int:
|
|
|
16
17
|
# Parse command line arguments
|
|
17
18
|
parser = argparse.ArgumentParser(description="SSHplex CLI: Debug interface for NetBox connectivity testing.")
|
|
18
19
|
parser.add_argument('--config', type=str, default=None, help='Path to the configuration file (default: ~/.config/sshplex/sshplex.yaml)')
|
|
19
|
-
parser.add_argument('--version', action='version', version='SSHplex
|
|
20
|
+
parser.add_argument('--version', action='version', version=f'SSHplex {__version__}')
|
|
20
21
|
args = parser.parse_args()
|
|
21
22
|
|
|
22
23
|
# Load configuration (will use default path if none specified)
|
|
@@ -7,10 +7,12 @@ import shutil
|
|
|
7
7
|
import os
|
|
8
8
|
from pydantic import BaseModel, Field, validator
|
|
9
9
|
|
|
10
|
+
from .. import __version__
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
class SSHplexConfig(BaseModel):
|
|
12
14
|
"""SSHplex main configuration."""
|
|
13
|
-
version: str =
|
|
15
|
+
version: str = __version__
|
|
14
16
|
session_prefix: str = "sshplex"
|
|
15
17
|
|
|
16
18
|
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""NetBox Source of Truth provider for SSHplex."""
|
|
2
|
+
|
|
3
|
+
import pynetbox # type: ignore
|
|
4
|
+
from typing import List, Dict, Any, Optional
|
|
5
|
+
from ..logger import get_logger
|
|
6
|
+
from .base import SoTProvider, Host
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class NetBoxProvider(SoTProvider):
|
|
10
|
+
"""NetBox implementation of SoT provider."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, url: str, token: str, verify_ssl: bool = True, timeout: int = 30) -> None:
|
|
13
|
+
"""Initialize NetBox provider.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
url: NetBox instance URL
|
|
17
|
+
token: API token for authentication
|
|
18
|
+
verify_ssl: Whether to verify SSL certificates
|
|
19
|
+
timeout: Request timeout in seconds
|
|
20
|
+
"""
|
|
21
|
+
self.url = url
|
|
22
|
+
self.token = token
|
|
23
|
+
self.verify_ssl = verify_ssl
|
|
24
|
+
self.timeout = timeout
|
|
25
|
+
self.api: Optional[Any] = None
|
|
26
|
+
self.logger = get_logger()
|
|
27
|
+
|
|
28
|
+
def connect(self) -> bool:
|
|
29
|
+
"""Establish connection to NetBox API.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
True if connection successful, False otherwise
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
self.logger.info(f"Connecting to NetBox at {self.url}")
|
|
36
|
+
|
|
37
|
+
self.api = pynetbox.api(
|
|
38
|
+
url=self.url,
|
|
39
|
+
token=self.token
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Configure SSL verification and timeout
|
|
43
|
+
if self.api is not None:
|
|
44
|
+
self.api.http_session.verify = self.verify_ssl
|
|
45
|
+
self.api.http_session.timeout = self.timeout
|
|
46
|
+
|
|
47
|
+
# Log SSL verification status
|
|
48
|
+
if not self.verify_ssl:
|
|
49
|
+
self.logger.warning("SSL certificate verification is DISABLED")
|
|
50
|
+
try:
|
|
51
|
+
import urllib3 # type: ignore
|
|
52
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
53
|
+
except ImportError:
|
|
54
|
+
pass # urllib3 not available, continue anyway
|
|
55
|
+
|
|
56
|
+
# Test the connection
|
|
57
|
+
if self.test_connection():
|
|
58
|
+
self.logger.info("Successfully connected to NetBox")
|
|
59
|
+
return True
|
|
60
|
+
else:
|
|
61
|
+
self.logger.error("Failed to establish NetBox connection")
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
except Exception as e:
|
|
65
|
+
self.logger.error(f"NetBox connection failed: {e}")
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
def test_connection(self) -> bool:
|
|
69
|
+
"""Test connection to NetBox API.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
True if connection is healthy, False otherwise
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
if not self.api:
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
# Try to get NetBox status
|
|
79
|
+
status = self.api.status()
|
|
80
|
+
self.logger.debug(f"NetBox status: {status}")
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
except Exception as e:
|
|
84
|
+
self.logger.error(f"NetBox connection test failed: {e}")
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
def get_hosts(self, filters: Optional[Dict[str, Any]] = None) -> List[Host]:
|
|
88
|
+
"""Retrieve virtual machines and physical devices from NetBox.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
filters: Optional filters to apply (status, role, etc.)
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
List of Host objects
|
|
95
|
+
"""
|
|
96
|
+
if not self.api:
|
|
97
|
+
self.logger.error("NetBox API not connected. Call connect() first.")
|
|
98
|
+
return []
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
self.logger.info("Retrieving VMs and devices from NetBox")
|
|
102
|
+
|
|
103
|
+
# Build filter parameters
|
|
104
|
+
filter_params = {}
|
|
105
|
+
if filters:
|
|
106
|
+
filter_params.update(filters)
|
|
107
|
+
self.logger.info(f"Applying filters: {filter_params}")
|
|
108
|
+
|
|
109
|
+
hosts = []
|
|
110
|
+
vm_count = 0
|
|
111
|
+
device_count = 0
|
|
112
|
+
|
|
113
|
+
# Get virtual machines
|
|
114
|
+
self.logger.info("Querying virtual machines...")
|
|
115
|
+
vms = list(self.api.virtualization.virtual_machines.filter(**filter_params))
|
|
116
|
+
self.logger.info(f"Found {len(vms)} virtual machines")
|
|
117
|
+
|
|
118
|
+
for vm in vms:
|
|
119
|
+
host = self._process_vm(vm)
|
|
120
|
+
if host:
|
|
121
|
+
hosts.append(host)
|
|
122
|
+
vm_count += 1
|
|
123
|
+
|
|
124
|
+
# Get physical devices
|
|
125
|
+
self.logger.info("Querying physical devices...")
|
|
126
|
+
devices = list(self.api.dcim.devices.filter(**filter_params))
|
|
127
|
+
self.logger.info(f"Found {len(devices)} physical devices")
|
|
128
|
+
|
|
129
|
+
for device in devices:
|
|
130
|
+
host = self._process_device(device)
|
|
131
|
+
if host:
|
|
132
|
+
hosts.append(host)
|
|
133
|
+
device_count += 1
|
|
134
|
+
|
|
135
|
+
self.logger.info(f"Retrieved {len(hosts)} total hosts from NetBox ({vm_count} VMs, {device_count} devices)")
|
|
136
|
+
return hosts
|
|
137
|
+
|
|
138
|
+
except Exception as e:
|
|
139
|
+
self.logger.error(f"Failed to retrieve hosts from NetBox: {e}")
|
|
140
|
+
return []
|
|
141
|
+
|
|
142
|
+
def _get_primary_ip(self, vm: Any) -> Optional[str]:
|
|
143
|
+
"""Extract primary IP address from VM object.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
vm: NetBox VM object
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Primary IP address as string, or None if not found
|
|
150
|
+
"""
|
|
151
|
+
try:
|
|
152
|
+
if vm.primary_ip4:
|
|
153
|
+
# Remove CIDR notation if present
|
|
154
|
+
ip = str(vm.primary_ip4).split('/')[0]
|
|
155
|
+
return ip
|
|
156
|
+
elif vm.primary_ip6:
|
|
157
|
+
# Use IPv6 if no IPv4
|
|
158
|
+
ip = str(vm.primary_ip6).split('/')[0]
|
|
159
|
+
return ip
|
|
160
|
+
else:
|
|
161
|
+
return None
|
|
162
|
+
except Exception as e:
|
|
163
|
+
self.logger.debug(f"Error extracting IP for VM {vm.name}: {e}")
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
def _process_vm(self, vm: Any) -> Optional[Host]:
|
|
167
|
+
"""Process a virtual machine object into a Host.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
vm: NetBox VM object
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Host object or None if processing fails
|
|
174
|
+
"""
|
|
175
|
+
try:
|
|
176
|
+
# Extract VM details
|
|
177
|
+
name = vm.name
|
|
178
|
+
ip = self._get_primary_ip(vm)
|
|
179
|
+
|
|
180
|
+
if not ip:
|
|
181
|
+
self.logger.warning(f"VM {name} has no primary IP, skipping")
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
# Get tags as a comma-separated string
|
|
185
|
+
tags = ""
|
|
186
|
+
if hasattr(vm, 'tags') and vm.tags:
|
|
187
|
+
try:
|
|
188
|
+
tags = ", ".join([str(tag) for tag in vm.tags])
|
|
189
|
+
except Exception as e:
|
|
190
|
+
self.logger.debug(f"Error processing tags for VM {name}: {e}")
|
|
191
|
+
|
|
192
|
+
# Create host with metadata
|
|
193
|
+
host = Host(
|
|
194
|
+
name=name,
|
|
195
|
+
ip=ip,
|
|
196
|
+
status=str(vm.status) if vm.status else "unknown",
|
|
197
|
+
role=str(vm.role) if vm.role else "unknown",
|
|
198
|
+
platform="vm", # Mark as virtual machine
|
|
199
|
+
cluster=str(vm.cluster) if vm.cluster else "unknown",
|
|
200
|
+
tags=tags,
|
|
201
|
+
description=str(vm.description) if vm.description else ""
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
self.logger.debug(f"Added VM host: {host}")
|
|
205
|
+
return host
|
|
206
|
+
|
|
207
|
+
except Exception as e:
|
|
208
|
+
self.logger.error(f"Error processing VM {vm.name}: {e}")
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
def _process_device(self, device: Any) -> Optional[Host]:
|
|
212
|
+
"""Process a physical device object into a Host.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
device: NetBox device object
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Host object or None if processing fails
|
|
219
|
+
"""
|
|
220
|
+
try:
|
|
221
|
+
# Extract device details
|
|
222
|
+
name = device.name
|
|
223
|
+
ip = self._get_device_primary_ip(device)
|
|
224
|
+
|
|
225
|
+
if not ip:
|
|
226
|
+
self.logger.warning(f"Device {name} has no primary IP, skipping")
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
# Get tags as a comma-separated string
|
|
230
|
+
tags = ""
|
|
231
|
+
if hasattr(device, 'tags') and device.tags:
|
|
232
|
+
try:
|
|
233
|
+
tags = ", ".join([str(tag) for tag in device.tags])
|
|
234
|
+
except Exception as e:
|
|
235
|
+
self.logger.debug(f"Error processing tags for device {name}: {e}")
|
|
236
|
+
|
|
237
|
+
# Create host with metadata
|
|
238
|
+
host = Host(
|
|
239
|
+
name=name,
|
|
240
|
+
ip=ip,
|
|
241
|
+
status=str(device.status) if device.status else "unknown",
|
|
242
|
+
role=str(device.role) if device.role else "unknown",
|
|
243
|
+
platform=str(device.platform) if device.platform else "device",
|
|
244
|
+
cluster=str(device.rack) if device.rack else "unknown", # Use rack as cluster for devices
|
|
245
|
+
tags=tags,
|
|
246
|
+
description=str(device.comments) if device.comments else ""
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
self.logger.debug(f"Added device host: {host}")
|
|
250
|
+
return host
|
|
251
|
+
|
|
252
|
+
except Exception as e:
|
|
253
|
+
self.logger.error(f"Error processing device {device.name}: {e}")
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
def _get_device_primary_ip(self, device: Any) -> Optional[str]:
|
|
257
|
+
"""Extract primary IP address from device object.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
device: NetBox device object
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Primary IP address as string, or None if not found
|
|
264
|
+
"""
|
|
265
|
+
try:
|
|
266
|
+
if device.primary_ip4:
|
|
267
|
+
# Remove CIDR notation if present
|
|
268
|
+
ip = str(device.primary_ip4).split('/')[0]
|
|
269
|
+
return ip
|
|
270
|
+
elif device.primary_ip6:
|
|
271
|
+
# Use IPv6 if no IPv4
|
|
272
|
+
ip = str(device.primary_ip6).split('/')[0]
|
|
273
|
+
return ip
|
|
274
|
+
else:
|
|
275
|
+
return None
|
|
276
|
+
except Exception as e:
|
|
277
|
+
self.logger.debug(f"Error extracting IP for device {device.name}: {e}")
|
|
278
|
+
return None
|
|
@@ -7,6 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
from datetime import datetime
|
|
8
8
|
from typing import Any
|
|
9
9
|
|
|
10
|
+
from . import __version__
|
|
10
11
|
from .lib.config import load_config
|
|
11
12
|
from .lib.logger import setup_logging, get_logger
|
|
12
13
|
from .lib.sot.netbox import NetBoxProvider
|
|
@@ -41,7 +42,7 @@ def main() -> int:
|
|
|
41
42
|
# Parse command line arguments
|
|
42
43
|
parser = argparse.ArgumentParser(description="SSHplex: Multiplex your SSH connections with style.")
|
|
43
44
|
parser.add_argument('--config', type=str, default=None, help='Path to the configuration file (default: ~/.config/sshplex/sshplex.yaml)')
|
|
44
|
-
parser.add_argument('--version', action='version', version='SSHplex
|
|
45
|
+
parser.add_argument('--version', action='version', version=f'SSHplex {__version__}')
|
|
45
46
|
parser.add_argument('--debug', action='store_true', help='Run in debug mode (CLI only, no TUI)')
|
|
46
47
|
args = parser.parse_args()
|
|
47
48
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sshplex
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.10
|
|
4
4
|
Summary: Multiplex your SSH connections with style
|
|
5
5
|
Author-email: MJAHED Sabri <contact@sabrimjahed.com>
|
|
6
6
|
License: MIT
|
|
@@ -44,7 +44,7 @@ Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
|
44
44
|
Requires-Dist: types-PyYAML>=6.0.0; extra == "dev"
|
|
45
45
|
Dynamic: license-file
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+

|
|
48
48
|
|
|
49
49
|
**Multiplex your SSH connections with style**
|
|
50
50
|
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
"""NetBox Source of Truth provider for SSHplex."""
|
|
2
|
-
|
|
3
|
-
import pynetbox
|
|
4
|
-
from typing import List, Dict, Any, Optional
|
|
5
|
-
from ..logger import get_logger
|
|
6
|
-
from .base import SoTProvider, Host
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class NetBoxProvider(SoTProvider):
|
|
10
|
-
"""NetBox implementation of SoT provider."""
|
|
11
|
-
|
|
12
|
-
def __init__(self, url: str, token: str, verify_ssl: bool = True, timeout: int = 30) -> None:
|
|
13
|
-
"""Initialize NetBox provider.
|
|
14
|
-
|
|
15
|
-
Args:
|
|
16
|
-
url: NetBox instance URL
|
|
17
|
-
token: API token for authentication
|
|
18
|
-
verify_ssl: Whether to verify SSL certificates
|
|
19
|
-
timeout: Request timeout in seconds
|
|
20
|
-
"""
|
|
21
|
-
self.url = url
|
|
22
|
-
self.token = token
|
|
23
|
-
self.verify_ssl = verify_ssl
|
|
24
|
-
self.timeout = timeout
|
|
25
|
-
self.api: Optional[Any] = None
|
|
26
|
-
self.logger = get_logger()
|
|
27
|
-
|
|
28
|
-
def connect(self) -> bool:
|
|
29
|
-
"""Establish connection to NetBox API.
|
|
30
|
-
|
|
31
|
-
Returns:
|
|
32
|
-
True if connection successful, False otherwise
|
|
33
|
-
"""
|
|
34
|
-
try:
|
|
35
|
-
self.logger.info(f"Connecting to NetBox at {self.url}")
|
|
36
|
-
|
|
37
|
-
self.api = pynetbox.api(
|
|
38
|
-
url=self.url,
|
|
39
|
-
token=self.token
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
# Configure SSL verification and timeout
|
|
43
|
-
if self.api is not None:
|
|
44
|
-
self.api.http_session.verify = self.verify_ssl
|
|
45
|
-
self.api.http_session.timeout = self.timeout
|
|
46
|
-
|
|
47
|
-
# Log SSL verification status
|
|
48
|
-
if not self.verify_ssl:
|
|
49
|
-
self.logger.warning("SSL certificate verification is DISABLED")
|
|
50
|
-
try:
|
|
51
|
-
import urllib3
|
|
52
|
-
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
53
|
-
except ImportError:
|
|
54
|
-
pass # urllib3 not available, continue anyway
|
|
55
|
-
|
|
56
|
-
# Test the connection
|
|
57
|
-
if self.test_connection():
|
|
58
|
-
self.logger.info("Successfully connected to NetBox")
|
|
59
|
-
return True
|
|
60
|
-
else:
|
|
61
|
-
self.logger.error("Failed to establish NetBox connection")
|
|
62
|
-
return False
|
|
63
|
-
|
|
64
|
-
except Exception as e:
|
|
65
|
-
self.logger.error(f"NetBox connection failed: {e}")
|
|
66
|
-
return False
|
|
67
|
-
|
|
68
|
-
def test_connection(self) -> bool:
|
|
69
|
-
"""Test connection to NetBox API.
|
|
70
|
-
|
|
71
|
-
Returns:
|
|
72
|
-
True if connection is healthy, False otherwise
|
|
73
|
-
"""
|
|
74
|
-
try:
|
|
75
|
-
if not self.api:
|
|
76
|
-
return False
|
|
77
|
-
|
|
78
|
-
# Try to get NetBox status
|
|
79
|
-
status = self.api.status()
|
|
80
|
-
self.logger.debug(f"NetBox status: {status}")
|
|
81
|
-
return True
|
|
82
|
-
|
|
83
|
-
except Exception as e:
|
|
84
|
-
self.logger.error(f"NetBox connection test failed: {e}")
|
|
85
|
-
return False
|
|
86
|
-
|
|
87
|
-
def get_hosts(self, filters: Optional[Dict[str, Any]] = None) -> List[Host]:
|
|
88
|
-
"""Retrieve virtual machines from NetBox.
|
|
89
|
-
|
|
90
|
-
Args:
|
|
91
|
-
filters: Optional filters to apply (status, role, etc.)
|
|
92
|
-
|
|
93
|
-
Returns:
|
|
94
|
-
List of Host objects
|
|
95
|
-
"""
|
|
96
|
-
if not self.api:
|
|
97
|
-
self.logger.error("NetBox API not connected. Call connect() first.")
|
|
98
|
-
return []
|
|
99
|
-
|
|
100
|
-
try:
|
|
101
|
-
self.logger.info("Retrieving VMs from NetBox")
|
|
102
|
-
|
|
103
|
-
# Build filter parameters
|
|
104
|
-
filter_params = {}
|
|
105
|
-
if filters:
|
|
106
|
-
filter_params.update(filters)
|
|
107
|
-
self.logger.info(f"Applying filters: {filter_params}")
|
|
108
|
-
|
|
109
|
-
# Get virtual machines
|
|
110
|
-
vms = list(self.api.virtualization.virtual_machines.filter(**filter_params))
|
|
111
|
-
|
|
112
|
-
hosts = []
|
|
113
|
-
for vm in vms:
|
|
114
|
-
# Extract VM details
|
|
115
|
-
name = vm.name
|
|
116
|
-
ip = self._get_primary_ip(vm)
|
|
117
|
-
|
|
118
|
-
if not ip:
|
|
119
|
-
self.logger.warning(f"VM {name} has no primary IP, skipping")
|
|
120
|
-
continue
|
|
121
|
-
|
|
122
|
-
# Get tags as a comma-separated string
|
|
123
|
-
tags = ""
|
|
124
|
-
if hasattr(vm, 'tags') and vm.tags:
|
|
125
|
-
try:
|
|
126
|
-
tags = ", ".join([str(tag) for tag in vm.tags])
|
|
127
|
-
except Exception as e:
|
|
128
|
-
self.logger.debug(f"Error processing tags for VM {name}: {e}")
|
|
129
|
-
|
|
130
|
-
# Create host with metadata
|
|
131
|
-
host = Host(
|
|
132
|
-
name=name,
|
|
133
|
-
ip=ip,
|
|
134
|
-
status=str(vm.status) if vm.status else "unknown",
|
|
135
|
-
role=str(vm.role) if vm.role else "unknown",
|
|
136
|
-
platform=str(vm.platform) if vm.platform else "unknown",
|
|
137
|
-
cluster=str(vm.cluster) if vm.cluster else "unknown",
|
|
138
|
-
tags=tags,
|
|
139
|
-
description=str(vm.description) if vm.description else ""
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
hosts.append(host)
|
|
143
|
-
self.logger.debug(f"Added host: {host}")
|
|
144
|
-
|
|
145
|
-
self.logger.info(f"Retrieved {len(hosts)} VMs from NetBox")
|
|
146
|
-
return hosts
|
|
147
|
-
|
|
148
|
-
except Exception as e:
|
|
149
|
-
self.logger.error(f"Failed to retrieve VMs from NetBox: {e}")
|
|
150
|
-
return []
|
|
151
|
-
|
|
152
|
-
def _get_primary_ip(self, vm: Any) -> Optional[str]:
|
|
153
|
-
"""Extract primary IP address from VM object.
|
|
154
|
-
|
|
155
|
-
Args:
|
|
156
|
-
vm: NetBox VM object
|
|
157
|
-
|
|
158
|
-
Returns:
|
|
159
|
-
Primary IP address as string, or None if not found
|
|
160
|
-
"""
|
|
161
|
-
try:
|
|
162
|
-
if vm.primary_ip4:
|
|
163
|
-
# Remove CIDR notation if present
|
|
164
|
-
ip = str(vm.primary_ip4).split('/')[0]
|
|
165
|
-
return ip
|
|
166
|
-
elif vm.primary_ip6:
|
|
167
|
-
# Use IPv6 if no IPv4
|
|
168
|
-
ip = str(vm.primary_ip6).split('/')[0]
|
|
169
|
-
return ip
|
|
170
|
-
else:
|
|
171
|
-
return None
|
|
172
|
-
except Exception as e:
|
|
173
|
-
self.logger.debug(f"Error extracting IP for VM {vm.name}: {e}")
|
|
174
|
-
return None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|