isptools-pro 3.0.0__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.
autofix/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from .engine import AutoFixEngine
2
+ from .permissions import PermissionManager, PermissionLevel
3
+ from .rules import AUTOFIX_RULES
4
+
5
+ __all__ = ["AutoFixEngine", "PermissionManager", "PermissionLevel", "AUTOFIX_RULES"]
autofix/engine.py ADDED
@@ -0,0 +1,91 @@
1
+ import logging
2
+ from .permissions import PermissionManager, PermissionLevel
3
+ from .rules import AUTOFIX_RULES
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ class AutoFixEngine:
8
+ """Engine for automatically detecting and fixing network issues."""
9
+
10
+ def __init__(self, permission_manager: PermissionManager):
11
+ self.permission_manager = permission_manager
12
+ self.history = []
13
+
14
+ def analyze(self, device_host: str, stats: dict) -> list[dict]:
15
+ """Analyze stats against rules and return potential fixes."""
16
+ detected_problems = []
17
+ for rule in AUTOFIX_RULES:
18
+ if rule["trigger"](stats):
19
+ detected_problems.append({
20
+ "name": rule["name"],
21
+ "description": rule["description"],
22
+ "fixes": rule["fixes"]
23
+ })
24
+ return detected_problems
25
+
26
+ def apply_fixes(self, driver, stats: dict) -> list[dict]:
27
+ """Analyze and apply allowed fixes."""
28
+ device_host = driver.device.host
29
+ device_type = driver.device.device_type
30
+ problems = self.analyze(device_host, stats)
31
+ results = []
32
+
33
+ for problem in problems:
34
+ for fix in problem["fixes"]:
35
+ # Check if fix applies to this device type
36
+ if fix["devices"] != ["all"] and device_type not in fix["devices"]:
37
+ continue
38
+
39
+ action = fix["action"]
40
+ required_level = fix["permission"]
41
+
42
+ # Request permission
43
+ if self.permission_manager.request_permission(action, device_host, required_level, problem["description"]):
44
+ status = self._execute_fix(driver, action)
45
+ else:
46
+ status = 'denied' if self.permission_manager.level < required_level else 'skipped'
47
+
48
+ result = {
49
+ "problem": problem["name"],
50
+ "action": action,
51
+ "status": status
52
+ }
53
+ self.history.append(result)
54
+ results.append(result)
55
+
56
+ if status == 'applied':
57
+ logger.info(f"AutoFix: Applied {action} to {device_host} for {problem['name']}")
58
+ else:
59
+ logger.debug(f"AutoFix: {action} on {device_host} status: {status}")
60
+
61
+ return results
62
+
63
+ def _execute_fix(self, driver, action: str) -> str:
64
+ """Execute the actual fix on the driver."""
65
+ try:
66
+ if action == "reboot_device":
67
+ driver.reboot()
68
+ return 'applied'
69
+ elif action == "send_alert":
70
+ # Alerting is handled externally usually, but we mark as applied
71
+ return 'applied'
72
+
73
+ # Call driver-specific methods
74
+ method = getattr(driver, action, None)
75
+ if method:
76
+ if method():
77
+ return 'applied'
78
+ else:
79
+ return 'failed'
80
+ else:
81
+ logger.warning(f"Driver {driver.__class__.__name__} does not implement {action}")
82
+ return 'failed'
83
+ except Exception as e:
84
+ logger.error(f"Error executing fix {action} on {driver.device.host}: {e}")
85
+ return 'failed'
86
+
87
+ def get_fix_log(self, device_host: str = None) -> list[dict]:
88
+ """Return history of all applied fixes."""
89
+ if device_host:
90
+ return [h for h in self.history if h.get('host') == device_host]
91
+ return self.history
autofix/permissions.py ADDED
@@ -0,0 +1,31 @@
1
+ from enum import IntEnum
2
+
3
+ class PermissionLevel(IntEnum):
4
+ READ_ONLY = 0 # فقط قراءة، بدون أي تعديل
5
+ SAFE_FIX = 1 # إصلاح آمن (restart interface, clear arp)
6
+ MODERATE_FIX = 2 # إصلاح متوسط (change tx power, channel)
7
+ FULL_AUTO = 3 # صلاحية كاملة (reboot, change config)
8
+
9
+ class PermissionManager:
10
+ def __init__(self, level: PermissionLevel = PermissionLevel.READ_ONLY, require_confirm: bool = True):
11
+ self.level = level
12
+ self.require_confirm = require_confirm
13
+ self.confirm_callback = None
14
+
15
+ def set_confirm_callback(self, callback: callable):
16
+ """Set a callback function for confirmation.
17
+ Callback receives: (action, device_host, details) -> returns bool
18
+ """
19
+ self.confirm_callback = callback
20
+
21
+ def request_permission(self, action: str, device_host: str, required_level: PermissionLevel, details: str = "") -> bool:
22
+ """Check if action is allowed based on current permission level and confirmation."""
23
+ if self.level < required_level:
24
+ return False
25
+
26
+ if self.require_confirm:
27
+ if self.confirm_callback:
28
+ return self.confirm_callback(action, device_host, details)
29
+ return False # No callback set but confirmation required
30
+
31
+ return True
autofix/rules.py ADDED
@@ -0,0 +1,64 @@
1
+ from .permissions import PermissionLevel
2
+
3
+ AUTOFIX_RULES = [
4
+ {
5
+ "name": "low_signal",
6
+ "trigger": lambda stats: stats.get("signal", 0) < -80,
7
+ "description": "Signal dropped below -80 dBm",
8
+ "fixes": [
9
+ {"action": "increase_tx_power", "permission": PermissionLevel.MODERATE_FIX, "devices": ["ubiquiti"]},
10
+ {"action": "send_alert", "permission": PermissionLevel.READ_ONLY, "devices": ["all"]},
11
+ ]
12
+ },
13
+ {
14
+ "name": "high_cpu",
15
+ "trigger": lambda stats: stats.get("cpu_load", 0) > 90,
16
+ "description": "CPU load exceeded 90%",
17
+ "fixes": [
18
+ {"action": "clear_connections", "permission": PermissionLevel.SAFE_FIX, "devices": ["mikrotik", "cisco"]},
19
+ {"action": "reboot_device", "permission": PermissionLevel.FULL_AUTO, "devices": ["all"]},
20
+ ]
21
+ },
22
+ {
23
+ "name": "interface_down",
24
+ "trigger": lambda stats: stats.get("interface_status") == "down",
25
+ "description": "Interface went down",
26
+ "fixes": [
27
+ {"action": "restart_interface", "permission": PermissionLevel.SAFE_FIX, "devices": ["all"]},
28
+ ]
29
+ },
30
+ {
31
+ "name": "high_noise",
32
+ "trigger": lambda stats: stats.get("noise", -95) > -85,
33
+ "description": "Noise floor too high",
34
+ "fixes": [
35
+ {"action": "change_channel", "permission": PermissionLevel.MODERATE_FIX, "devices": ["ubiquiti", "mikrotik"]},
36
+ {"action": "scan_best_channel", "permission": PermissionLevel.MODERATE_FIX, "devices": ["ubiquiti"]},
37
+ ]
38
+ },
39
+ {
40
+ "name": "low_ccq",
41
+ "trigger": lambda stats: stats.get("ccq", 100) < 60,
42
+ "description": "CCQ dropped below 60%",
43
+ "fixes": [
44
+ {"action": "reset_link", "permission": PermissionLevel.SAFE_FIX, "devices": ["ubiquiti"]},
45
+ {"action": "change_channel_width", "permission": PermissionLevel.MODERATE_FIX, "devices": ["ubiquiti"]},
46
+ ]
47
+ },
48
+ {
49
+ "name": "bandwidth_overload",
50
+ "trigger": lambda stats: stats.get("tx_bytes", 0) > 900_000_000, # ~900 Mbps
51
+ "description": "Bandwidth near capacity",
52
+ "fixes": [
53
+ {"action": "enable_queue", "permission": PermissionLevel.MODERATE_FIX, "devices": ["mikrotik"]},
54
+ ]
55
+ },
56
+ {
57
+ "name": "arp_flood",
58
+ "trigger": lambda stats: stats.get("arp_entries", 0) > 500,
59
+ "description": "ARP table flooding",
60
+ "fixes": [
61
+ {"action": "clear_arp", "permission": PermissionLevel.SAFE_FIX, "devices": ["mikrotik"]},
62
+ ]
63
+ },
64
+ ]
core/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .base import BaseDevice
2
+ from .exceptions import ISPToolsError, ConnectionError, AuthenticationError, CommandError
3
+
4
+ __all__ = ["BaseDevice", "ISPToolsError", "ConnectionError", "AuthenticationError", "CommandError"]
core/base.py ADDED
@@ -0,0 +1,47 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import List, Dict, Any, Optional
3
+ from ..models.device import Device
4
+ from ..models.stats import DeviceStats
5
+ from ..models.station import Station
6
+
7
+ class BaseDevice(ABC):
8
+ """Abstract base class for all ISP device drivers."""
9
+
10
+ def __init__(self, device: Device):
11
+ self.device = device
12
+ self.is_connected = False
13
+
14
+ @abstractmethod
15
+ def connect(self) -> bool:
16
+ """Establish a connection to the device."""
17
+ pass
18
+
19
+ @abstractmethod
20
+ def disconnect(self):
21
+ """Close the connection to the device."""
22
+ pass
23
+
24
+ @abstractmethod
25
+ def get_stats(self) -> DeviceStats:
26
+ """Fetch device performance statistics."""
27
+ pass
28
+
29
+ @abstractmethod
30
+ def get_clients(self) -> List[Station]:
31
+ """Fetch connected stations/clients."""
32
+ pass
33
+
34
+ @abstractmethod
35
+ def get_signal(self) -> Dict[str, Any]:
36
+ """Fetch signal strength, noise, and CCQ for wireless devices."""
37
+ pass
38
+
39
+ @abstractmethod
40
+ def apply_config(self, config: Dict[str, Any]) -> bool:
41
+ """Apply a configuration to the device."""
42
+ pass
43
+
44
+ @abstractmethod
45
+ def reboot(self) -> bool:
46
+ """Reboot the device."""
47
+ pass
core/exceptions.py ADDED
@@ -0,0 +1,15 @@
1
+ class ISPToolsError(Exception):
2
+ """Base exception for all isptools errors."""
3
+ pass
4
+
5
+ class ConnectionError(ISPToolsError):
6
+ """Raised when connection to a device fails."""
7
+ pass
8
+
9
+ class AuthenticationError(ISPToolsError):
10
+ """Raised when authentication fails."""
11
+ pass
12
+
13
+ class CommandError(ISPToolsError):
14
+ """Raised when a command fails to execute on the device."""
15
+ pass
drivers/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ from .mikrotik import MikroTikDriver
2
+ from .ubiquiti import UbiquitiDriver
3
+ from .cisco import CiscoDriver
4
+ from .snmp import SNMPDriver
5
+
6
+ __all__ = ["MikroTikDriver", "UbiquitiDriver", "CiscoDriver", "SNMPDriver"]
drivers/cisco.py ADDED
@@ -0,0 +1,165 @@
1
+ import logging
2
+ import re
3
+ from typing import List, Dict, Any
4
+ from datetime import datetime
5
+ from netmiko import ConnectHandler
6
+ from ..core.base import BaseDevice
7
+ from ..core.exceptions import ConnectionError, AuthenticationError, CommandError
8
+ from ..models.stats import DeviceStats
9
+ from ..models.station import Station
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class CiscoDriver(BaseDevice):
14
+ """Driver for Cisco devices using Netmiko (SSH)."""
15
+
16
+ def __init__(self, device):
17
+ super().__init__(device)
18
+ self.port = device.port or 22
19
+ self.connection = None
20
+
21
+ def connect(self) -> bool:
22
+ try:
23
+ device_params = {
24
+ 'device_type': 'cisco_ios',
25
+ 'host': self.device.host,
26
+ 'username': self.device.username,
27
+ 'password': self.device.password,
28
+ 'port': self.port,
29
+ }
30
+ self.connection = ConnectHandler(**device_params)
31
+ self.is_connected = True
32
+ logger.info(f"Connected to Cisco device at {self.device.host}")
33
+ return True
34
+ except Exception as e:
35
+ logger.error(f"Failed to connect to Cisco at {self.device.host}: {e}")
36
+ raise ConnectionError(f"Cisco connection failed: {e}")
37
+
38
+ def disconnect(self):
39
+ if self.connection:
40
+ self.connection.disconnect()
41
+ self.is_connected = False
42
+ logger.info(f"Disconnected from Cisco device at {self.device.host}")
43
+
44
+ def get_stats(self) -> DeviceStats:
45
+ try:
46
+ # Get CPU load
47
+ cpu_output = self.connection.send_command("show processes cpu sorted | exclude 0.00%")
48
+ cpu_match = re.search(r"five seconds: (\d+)%", cpu_output)
49
+ cpu_load = float(cpu_match.group(1)) if cpu_match else 0.0
50
+
51
+ # Get Memory
52
+ mem_output = self.connection.send_command("show memory statistics")
53
+ mem_match = re.search(r"Processor.* (\d+) total,.* (\d+) used", mem_output)
54
+ if mem_match:
55
+ total_mem = int(mem_match.group(1))
56
+ used_mem = int(mem_match.group(2))
57
+ mem_used_pct = (used_mem / total_mem) * 100
58
+ else:
59
+ mem_used_pct = 0.0
60
+
61
+ # Get Interfaces/Bytes
62
+ int_output = self.connection.send_command("show interfaces")
63
+ tx_bytes = sum(int(b) for b in re.findall(r"(\d+) bytes output", int_output))
64
+ rx_bytes = sum(int(b) for b in re.findall(r"(\d+) bytes input", int_output))
65
+
66
+ # Get Uptime
67
+ ver_output = self.connection.send_command("show version")
68
+ uptime_match = re.search(r"uptime is (.*)", ver_output)
69
+ uptime = uptime_match.group(1) if uptime_match else ""
70
+
71
+ return DeviceStats(
72
+ timestamp=datetime.now(),
73
+ cpu_load=cpu_load,
74
+ memory_used=mem_used_pct,
75
+ tx_bytes=tx_bytes,
76
+ rx_bytes=rx_bytes,
77
+ uptime=uptime
78
+ )
79
+ except Exception as e:
80
+ raise CommandError(f"Failed to get stats from Cisco: {e}")
81
+
82
+ def get_clients(self) -> List[Station]:
83
+ try:
84
+ stations = []
85
+ # Try DHCP bindings first
86
+ dhcp_output = self.connection.send_command("show ip dhcp binding")
87
+ # Format: 192.168.1.10 0100.50b6.075c.e1 Feb 16 2026 12:00 PM Automatic
88
+ dhcp_matches = re.findall(r"(\d+\.\d+\.\d+\.\d+)\s+([0-9a-f\.]+)", dhcp_output)
89
+
90
+ for ip, mac in dhcp_matches:
91
+ stations.append(Station(mac=mac, ip=ip))
92
+
93
+ # If no DHCP, try ARP
94
+ if not stations:
95
+ arp_output = self.connection.send_command("show arp")
96
+ # Format: Internet 192.168.1.1 - 0000.0c07.ac01 ARPA FastEthernet0/0
97
+ arp_matches = re.findall(r"Internet\s+(\d+\.\d+\.\d+\.\d+)\s+.*?\s+([0-9a-f\.]+)", arp_output)
98
+ for ip, mac in arp_matches:
99
+ stations.append(Station(mac=mac, ip=ip))
100
+
101
+ return stations
102
+ except Exception as e:
103
+ raise CommandError(f"Failed to get clients from Cisco: {e}")
104
+
105
+ def get_signal(self) -> Dict[str, Any]:
106
+ # Cisco is not wireless by default
107
+ return {}
108
+
109
+ def apply_config(self, config: Dict[str, Any]) -> bool:
110
+ try:
111
+ # Expecting config as a list of commands under 'commands' key
112
+ commands = config.get('commands', [])
113
+ if commands:
114
+ self.connection.send_config_set(commands)
115
+ return True
116
+ return False
117
+ except Exception as e:
118
+ raise CommandError(f"Failed to apply config to Cisco: {e}")
119
+
120
+ def reboot(self) -> bool:
121
+ try:
122
+ # reload command usually requires confirmation
123
+ self.connection.send_command("reload", expect_string=r"confirm")
124
+ self.connection.send_command("\n")
125
+ return True
126
+ except Exception as e:
127
+ logger.warning(f"Reboot command sent to Cisco, connection may drop: {e}")
128
+ return True
129
+
130
+ # --- AutoFix Actions ---
131
+
132
+ def restart_interface(self, name: str) -> bool:
133
+ """Shutdown and no shutdown an interface."""
134
+ try:
135
+ commands = [
136
+ f"interface {name}",
137
+ "shutdown",
138
+ "no shutdown"
139
+ ]
140
+ self.connection.send_config_set(commands)
141
+ return True
142
+ except Exception as e:
143
+ logger.error(f"Failed to restart interface {name} on Cisco: {e}")
144
+ return False
145
+
146
+ def get_neighbors(self) -> List[Dict[str, Any]]:
147
+ """Get neighbors via LLDP."""
148
+ try:
149
+ output = self.connection.send_command("show lldp neighbors", use_textfsm=True)
150
+ # If textfsm is not available or fails, output is just a string
151
+ if isinstance(output, str):
152
+ # Simple regex parsing if textfsm fails
153
+ neighbors = []
154
+ matches = re.findall(r"(\S+)\s+(\S+)\s+\d+\s+.*?\s+(\S+)", output)
155
+ for neighbor_id, local_int, remote_int in matches:
156
+ neighbors.append({
157
+ "neighbor_id": neighbor_id,
158
+ "local_interface": local_int,
159
+ "neighbor_interface": remote_int
160
+ })
161
+ return neighbors
162
+ return output
163
+ except Exception as e:
164
+ logger.error(f"Failed to get neighbors from Cisco: {e}")
165
+ return []
drivers/mikrotik.py ADDED
@@ -0,0 +1,179 @@
1
+ import logging
2
+ import time
3
+ import routeros_api
4
+ from typing import List, Dict, Any
5
+ from datetime import datetime
6
+ from ..core.base import BaseDevice
7
+ from ..core.exceptions import ConnectionError, AuthenticationError, CommandError
8
+ from ..models.stats import DeviceStats
9
+ from ..models.station import Station
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class MikroTikDriver(BaseDevice):
14
+ """Driver for MikroTik devices using RouterOS API."""
15
+
16
+ def __init__(self, device):
17
+ super().__init__(device)
18
+ self.port = device.port or 8728
19
+ self.connection = None
20
+ self.api = None
21
+
22
+ def connect(self) -> bool:
23
+ try:
24
+ self.connection = routeros_api.RouterOsApiConnection(
25
+ self.device.host,
26
+ username=self.device.username,
27
+ password=self.device.password,
28
+ port=self.port
29
+ )
30
+ self.api = self.connection.get_api()
31
+ self.is_connected = True
32
+ logger.info(f"Connected to MikroTik device at {self.device.host}")
33
+ return True
34
+ except Exception as e:
35
+ logger.error(f"Failed to connect to MikroTik at {self.device.host}: {e}")
36
+ raise ConnectionError(f"MikroTik connection failed: {e}")
37
+
38
+ def disconnect(self):
39
+ if self.connection:
40
+ self.connection.disconnect()
41
+ self.is_connected = False
42
+ logger.info(f"Disconnected from MikroTik device at {self.device.host}")
43
+
44
+ def get_stats(self) -> DeviceStats:
45
+ try:
46
+ resource = self.api.get_resource('/system/resource').get()[0]
47
+ interfaces = self.api.get_resource('/interface').get()
48
+
49
+ tx_bytes = sum(int(i.get('tx-byte', 0)) for i in interfaces)
50
+ rx_bytes = sum(int(i.get('rx-byte', 0)) for i in interfaces)
51
+
52
+ return DeviceStats(
53
+ timestamp=datetime.now(),
54
+ cpu_load=float(resource.get('cpu-load', 0)),
55
+ memory_used=100 - (float(resource.get('free-memory', 0)) / float(resource.get('total-memory', 1)) * 100),
56
+ tx_bytes=tx_bytes,
57
+ rx_bytes=rx_bytes,
58
+ uptime=resource.get('uptime', ''),
59
+ temperature=float(resource.get('board-temperature1', 0)) or float(resource.get('cpu-temperature', 0))
60
+ )
61
+ except Exception as e:
62
+ raise CommandError(f"Failed to get stats from MikroTik: {e}")
63
+
64
+ def get_clients(self) -> List[Station]:
65
+ try:
66
+ stations = []
67
+ reg_table = self.api.get_resource('/interface/wireless/registration-table').get()
68
+ leases = self.api.get_resource('/ip/dhcp-server/lease').get()
69
+
70
+ lease_map = {l.get('mac-address'): l.get('address', '') for l in leases}
71
+
72
+ for s in reg_table:
73
+ mac = s.get('mac-address')
74
+ stations.append(Station(
75
+ mac=mac,
76
+ ip=lease_map.get(mac, ''),
77
+ signal=int(s.get('signal-strength', '0').split('dBm')[0]),
78
+ noise=int(s.get('signal-to-noise', '0')), # Note: ROS might not provide direct noise in some versions
79
+ ccq=int(s.get('tx-ccq', '0')),
80
+ tx_rate=float(s.get('tx-rate', '0').split('Mbps')[0] or 0),
81
+ rx_rate=float(s.get('rx-rate', '0').split('Mbps')[0] or 0),
82
+ uptime=s.get('uptime', '')
83
+ ))
84
+ return stations
85
+ except Exception as e:
86
+ raise CommandError(f"Failed to get clients from MikroTik: {e}")
87
+
88
+ def get_signal(self) -> Dict[str, Any]:
89
+ try:
90
+ reg_table = self.api.get_resource('/interface/wireless/registration-table').get()
91
+ if not reg_table:
92
+ return {}
93
+
94
+ # Returning stats for the first client as a summary, or could be averaged
95
+ s = reg_table[0]
96
+ return {
97
+ 'signal': s.get('signal-strength'),
98
+ 'noise': s.get('signal-to-noise'),
99
+ 'ccq': s.get('tx-ccq')
100
+ }
101
+ except Exception as e:
102
+ raise CommandError(f"Failed to get signal from MikroTik: {e}")
103
+
104
+ def apply_config(self, config: Dict[str, Any]) -> bool:
105
+ try:
106
+ # Example: config = {'/ip/address': {'address': '1.1.1.1/24', 'interface': 'ether1'}}
107
+ for path, params in config.items():
108
+ resource = self.api.get_resource(path)
109
+ resource.set(**params)
110
+ return True
111
+ except Exception as e:
112
+ raise CommandError(f"Failed to apply config to MikroTik: {e}")
113
+
114
+ def reboot(self) -> bool:
115
+ try:
116
+ self.api.get_resource('/system/reboot').call('reboot')
117
+ return True
118
+ except Exception as e:
119
+ # Rebooting might break the connection and throw an error, which is expected
120
+ logger.warning(f"Reboot command sent to MikroTik, connection may drop: {e}")
121
+ return True
122
+
123
+ # --- AutoFix Actions ---
124
+
125
+ def clear_connections(self) -> bool:
126
+ """Remove all firewall connections."""
127
+ try:
128
+ resource = self.api.get_resource('/ip/firewall/connection')
129
+ conns = resource.get()
130
+ for conn in conns:
131
+ resource.remove(id=conn['.id'])
132
+ return True
133
+ except Exception as e:
134
+ logger.error(f"Failed to clear connections on MikroTik: {e}")
135
+ return False
136
+
137
+ def clear_arp(self) -> bool:
138
+ """Flush ARP table."""
139
+ try:
140
+ # ROS API doesn't have a direct 'flush' command exposed easily, we remove all dynamic entries
141
+ resource = self.api.get_resource('/ip/arp')
142
+ arps = resource.get()
143
+ for arp in arps:
144
+ if arp.get('dynamic') == 'true':
145
+ resource.remove(id=arp['.id'])
146
+ return True
147
+ except Exception as e:
148
+ logger.error(f"Failed to clear ARP on MikroTik: {e}")
149
+ return False
150
+
151
+ def restart_interface(self, name: str = 'ether1') -> bool:
152
+ """Disable then enable an interface."""
153
+ try:
154
+ resource = self.api.get_resource('/interface')
155
+ resource.set(id=name, disabled='yes')
156
+ time.sleep(1)
157
+ resource.set(id=name, disabled='no')
158
+ return True
159
+ except Exception as e:
160
+ logger.error(f"Failed to restart interface {name} on MikroTik: {e}")
161
+ return False
162
+
163
+ def enable_queue(self, limit: str = '10M/10M') -> bool:
164
+ """Add a simple queue for bandwidth control."""
165
+ try:
166
+ resource = self.api.get_resource('/queue/simple')
167
+ resource.add(name='autofix-limit', target='0.0.0.0/0', max_limit=limit)
168
+ return True
169
+ except Exception as e:
170
+ logger.error(f"Failed to enable queue on MikroTik: {e}")
171
+ return False
172
+
173
+ def get_neighbors(self) -> List[Dict[str, Any]]:
174
+ """Get neighbors via /ip/neighbor."""
175
+ try:
176
+ return self.api.get_resource('/ip/neighbor').get()
177
+ except Exception as e:
178
+ logger.error(f"Failed to get neighbors from MikroTik: {e}")
179
+ return []