Рефакторинг. docstring

This commit is contained in:
parent 060f4de175
commit 37f9e3abfb
14 changed files with 1234 additions and 243 deletions

View file

@ -11,7 +11,6 @@ from functools import lru_cache
import json
from pathlib import Path
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
import subprocess
from models.subnet import SubnetScanResult, Device
@ -26,11 +25,33 @@ logging.basicConfig(
logger = logging.getLogger(__name__)
class NetworkScanner:
"""Класс для сканирования подсети с использованием ARP-запросов.
Выполняет обнаружение устройств в указанной подсети через ARP-пакеты.
"""
def __init__(self, interface: str):
"""Инициализирует сканер сети с указанным сетевым интерфейсом.
Args:
interface (str): Название сетевого интерфейса (например, "eth0").
"""
self.interface = interface
conf.iface = interface
def scan(self, subnet: str) -> List[Tuple[str, str]]:
"""Сканирует подсеть с использованием ARP-запросов.
Args:
subnet (str): Подсеть в формате CIDR (например, "192.168.1.0/24").
Returns:
List[Tuple[str, str]]: Список кортежей (IP-адрес, MAC-адрес) обнаруженных
устройств.
Raises:
Exception: Если произошла ошибка при отправке ARP-пакетов.
"""
try:
arp = ARP(pdst=subnet)
ether = Ether(dst="ff:ff:ff:ff:ff:ff")
@ -46,13 +67,42 @@ class NetworkScanner:
return []
class PortScanner:
"""Класс для сканирования открытых портов на устройствах.
Выполняет сканирование портов с использованием TCP SYN-запросов.
"""
def __init__(self, interface: str):
"""Инициализирует сканер портов с указанным сетевым интерфейсом.
Args:
interface (str): Название сетевого интерфейса (например, "eth0").
"""
self.interface = interface
def scan(self, ip: str) -> List[str]:
"""Сканирует открытые порты на указанном IP-адресе.
Args:
ip (str): IP-адрес устройства для сканирования.
Returns:
List[str]: Список имён открытых портов (например, "HTTP", "SSH").
Raises:
Exception: Если произошла ошибка при сканировании портов.
"""
open_ports = []
def check_port(port: int) -> Optional[str]:
"""Проверяет доступность конкретного порта.
Args:
port (int): Номер порта для проверки.
Returns:
Optional[str]: Имя порта, если открыт, иначе None.
"""
try:
pkt = sr1(
IP(dst=ip) / TCP(dport=port, flags="S"),
@ -80,13 +130,33 @@ class PortScanner:
return []
class DeviceInfoCollector:
"""Класс для сбора информации об устройствах, включая вендоров и хостнеймы.
Управляет кэшированием данных NetBIOS и DNS.
"""
def __init__(self):
"""Инициализирует коллектор информации об устройствах.
Создаёт объект MacLookup для определения вендоров и инициализирует кэши.
"""
self.mac_lookup = MacLookup()
self.nbtscan_cache = {}
self.nbtscan_results = {}
@lru_cache(maxsize=1000)
def get_vendor(self, mac: str) -> str:
"""Определяет вендора по MAC-адресу с использованием кэширования.
Args:
mac (str): MAC-адрес устройства в формате XX:XX:XX:XX:XX:XX.
Returns:
str: Название вендора или "Unknown", если вендор не определён.
Raises:
Exception: Если произошла ошибка при запросе к базе вендоров.
"""
try:
return self.mac_lookup.lookup(mac)
except Exception as e:
@ -95,6 +165,13 @@ class DeviceInfoCollector:
return "Unknown"
def scan_nbtscan(self, subnet: str) -> None:
"""Выполняет сканирование NetBIOS для указанной подсети.
Args:
subnet (str): Подсеть в формате CIDR (например, "192.168.1.0/24").
Сохраняет результаты в кэш, игнорируя хостнеймы "<unknown>".
"""
if subnet not in self.nbtscan_results:
try:
logger.debug(f"Запуск nbtscan для подсети {subnet}")
@ -121,6 +198,16 @@ class DeviceInfoCollector:
logger.error(f"NetBIOS: Ошибка для подсети {subnet}: {str(e)}")
def get_hostname(self, ip: str) -> Optional[str]:
"""Определяет хостнейм для указанного IP-адреса.
Args:
ip (str): IP-адрес устройства.
Returns:
Optional[str]: Хостнейм, если найден, иначе None.
Сначала пытается использовать DNS, затем NetBIOS через nbtscan.
"""
try:
socket.setdefaulttimeout(2)
return socket.gethostbyaddr(ip)[0]
@ -138,12 +225,33 @@ class DeviceInfoCollector:
return None
class DeviceStorage:
"""Класс для хранения и управления данными об устройствах.
Хранит устройства в JSON-файле и удаляет устаревшие записи.
"""
def __init__(self, storage_path: str = "etc/gonets/SRV/storage/devices/devices.json"):
"""Инициализирует хранилище устройств с указанным путём.
Args:
storage_path (str, optional): Путь к файлу хранения.
По умолчанию "etc/gonets/SRV/storage/devices/devices.json".
Создаёт родительские директории, если они не существуют.
"""
self.storage_file = Path(storage_path)
self.storage_file.parent.mkdir(parents=True, exist_ok=True)
self.offline_timeout = timedelta(days=7)
def load_devices(self) -> Dict[str, Device]:
"""Загружает устройства из файла хранения.
Returns:
Dict[str, Device]: Словарь устройств, где ключ — MAC-адрес.
Удаляет устройства с неверным форматом last_seen и возвращает пустой словарь
при ошибке.
"""
try:
if self.storage_file.exists():
with open(self.storage_file, "r", encoding="utf-8") as f:
@ -160,6 +268,15 @@ class DeviceStorage:
return {}
def _clean_old_devices(self, devices: Dict[str, Device]) -> Dict[str, Device]:
"""Очищает устаревшие устройства из словаря.
Args:
devices (Dict[str, Device]): Словарь устройств с MAC-адресами как ключами.
Returns:
Dict[str, Device]: Отфильтрованный словарь, исключающий устройства
offline более 7 дней.
"""
now = datetime.now()
filtered = {}
for mac, device in devices.items():
@ -177,6 +294,14 @@ class DeviceStorage:
return filtered
def save_devices(self, devices: List[Device]) -> None:
"""Сохраняет список устройств в файл.
Args:
devices (List[Device]): Список объектов Device для сохранения.
Raises:
Exception: Если произошла ошибка при записи в файл.
"""
try:
data = SubnetScanResult(devices=devices).model_dump()
with open(self.storage_file, "w", encoding="utf-8") as f:
@ -186,7 +311,16 @@ class DeviceStorage:
logger.error(f"Ошибка при сохранении устройств: {str(e)}")
class SubnetService:
"""Сервис для сканирования подсетей и управления устройствами.
Объединяет функционал сканирования сети, портов и хранения данных об устройствах.
"""
def __init__(self):
"""Инициализирует сервис подсети с настройкой всех компонентов.
Автоматически определяет интерфейс и инициализирует сканеры и хранилище.
"""
self.interface = self._get_default_interface()
self.network_scanner = NetworkScanner(self.interface)
self.port_scanner = PortScanner(self.interface)
@ -194,6 +328,13 @@ class SubnetService:
self.storage = DeviceStorage()
def _get_default_interface(self) -> str:
"""Определяет имя сетевого интерфейса по умолчанию.
Returns:
str: Название интерфейса (например, "eth0").
Использует информацию о шлюзе или первый доступный интерфейс, отличный от "lo".
"""
try:
gateways = netifaces.gateways()
default_gateway = gateways.get("default", {}).get(netifaces.AF_INET)
@ -209,6 +350,19 @@ class SubnetService:
return "eth0"
def scan_subnet(self, subnet: str, ports: List[int] = None) -> SubnetScanResult:
"""Сканирует указанную подсеть и возвращает результаты.
Args:
subnet (str): Подсеть в формате CIDR (например, "192.168.1.0/24").
ports (List[int], optional): Список портов для сканирования.
По умолчанию None (используются порты из config.ports).
Returns:
SubnetScanResult: Объект с результатами сканирования, содержащий список
устройств.
Сохраняет данные в хранилище и обновляет статус устройств.
"""
start_time = time.time()
devices = []
now = datetime.now().astimezone()
@ -228,6 +382,14 @@ class SubnetService:
self.device_info.scan_nbtscan(subnet)
def process_device(ip_mac: Tuple[str, str]) -> Optional[Device]:
"""Обрабатывает устройство из пары IP и MAC.
Args:
ip_mac (Tuple[str, str]): Кортеж с IP-адресом и MAC-адресом.
Returns:
Optional[Device]: Объект Device или None, если устройство локальное.
"""
ip, mac = ip_mac
if local_device and ip == local_device.ip:
return None
@ -286,6 +448,14 @@ class SubnetService:
return SubnetScanResult(devices=devices)
def _get_local_device(self, subnet: str) -> Optional[Device]:
"""Определяет локальное устройство в указанной подсети.
Args:
subnet (str): Подсеть в формате CIDR (например, "192.168.1.0/24").
Returns:
Optional[Device]: Объект локального устройства или None, если не найдено.
"""
now = datetime.now().astimezone()
formatted_now = now.strftime("%d-%m-%Y, %H:%M")
try: