"""Детерминированная идентификация семейства, якорей и параметров услуги. Стабильный якорь экземпляра отделён от изменяемых параметров конфигурации: изменение мощности, объёма ресурса или цены не должно само по себе создавать новую услугу, тогда как разные стойки, облачные платформы и маршруты остаются разными экземплярами. Неизвестные строки сохраняются как family `other`. """ import re _HOMOGLYPHS = str.maketrans({ "A": "А", "B": "В", "C": "С", "E": "Е", "H": "Н", "K": "К", "M": "М", "O": "О", "P": "Р", "T": "Т", "X": "Х", "Y": "У", }) _RACK_RE = re.compile(r"\b\d[A-ZА-ЯЁ]\d[A-ZА-ЯЁ]\d{3}\b") _NUMBER = r"(\d+(?:[.,]\d+)?)" def _normalized(text): """Нормализовать регистр, сохраняя различимые английские маркеры услуг.""" return str(text or "").upper().replace("Ё", "Е") def _decimal_text(value): """Представить числовой фрагмент с точкой и без незначащих нулей.""" return value.replace(",", ".").rstrip("0").rstrip(".") if "." in value or "," in value else value def _first_capture(pattern, text): """Извлечь первое числовое значение, сохранив точность исходной строки.""" match = re.search(pattern, text, re.IGNORECASE) return _decimal_text(match.group(1)) if match else None def _service_family(text): """Назначить наиболее специфичное семейство по доменным словам Nubes.""" rules = ( ("cloud_drive", r"VALO\s+CLOUD|ОБЛАЧН\w*\s+ДИСК"), ("rack_cabinet", r"ШКАФ\s+(?:СЕРВЕРН\w*|\d+\s*U)"), ("pdu", r"БЛОК\s+РОЗЕТОК|\bPDU\b"), ("fiber_line", r"ВОЛС|ВОЛОКН|ЛИНИ\w*\s+СВЯЗИ"), ("l2_channel", r"КАНАЛ\s+СВЯЗИ|\bL2\b|ТОЧКА\s*[-–—]?\s*ТОЧКА"), ("network_port", r"АРЕНДА\s+ПОРТА|ПОРТ\s+В\s+КОММУТАТОРЕ|CROSS\s*[-–]?\s*CONNECT|КРОСС\s*[-–]?\s*СОЕДИН"), ("ip_subnet", r"ПОДСЕТ|IPV[46]|IP\s*[-–]?\s*АДРЕС|/\s*(?:24|29|30)\b"), ("internet", r"ИНТЕРНЕТ|ТРАФИК|DDOS"), ("cloud_iaas", r"NEXT\s+GENERATION\s+CLOUD|ОБЛАЧН\w*|VCPU|VRAM|\bIAAS\b"), ("waf", r"\bWAF\b|ЗАЩИТ\w*\s+ВЕБ"), ("colocation_rack", r"СТОЙКО\s*[-–—]?\s*МЕСТ|COLOCATION|\b\d+\s*U\b"), ) for family, pattern in rules: if re.search(pattern, text, re.IGNORECASE): return family return "other" def _extract_anchors(text, family): """Извлечь идентификаторы экземпляра, не перенося туда изменяемые параметры.""" anchors = set(_RACK_RE.findall(text)) if family == "fiber_line": route = re.search( r"(?:ОТ\s+)?(.{0,100}?)(?:\s+ДО\s+)(.{0,100})", text, re.IGNORECASE, ) if route: anchors.add("ROUTE:" + re.sub(r"\s+", " ", route.group(1) + "->" + route.group(2)).strip()) if family == "cloud_iaas": platform = re.search(r"\b(?:AMD|INTEL)\s*\d+(?:[.,]\d+)?\s*ГГЦ|\bHIGHFREQ\b|\bSTD\b", text) if platform: anchors.add("PLATFORM:" + re.sub(r"\s+", "", platform.group(0))) if family == "ip_subnet": subnet = re.search(r"/\s*(\d{1,2})\b", text) if subnet: anchors.add("PREFIX:/" + subnet.group(1)) if family == "network_port": speed = re.search(r"\b\d+(?:[.,]\d+)?\s*(?:ГБИТ/С|МБИТ/С|GBPS|MBPS)\b", text) if speed: anchors.add("SPEED:" + re.sub(r"\s+", "", speed.group(0))) return "|".join(sorted(anchors)) def _extract_config(text): """Извлечь известные изменяемые численные параметры как точные строки.""" params = {} patterns = { "kw": rf"{_NUMBER}\s*КВТ\b", "units_u": rf"(?:РАЗМЕР\s+СТОЙКИ\s*[:\-]?\s*)?(\d+)\s*U\b", "vcpu": rf"(?:КОЛИЧЕСТВО\s*)?VCPU\s*[:\-]?\s*(\d+)", "vram_gb": rf"(?:ОБЪ[ЕЕ]М\s*)?VRAM\s*[:\-]?\s*{_NUMBER}\s*ГБ", "sata_gb": rf"SATA\s*[:\-]?\s*{_NUMBER}\s*ГБ", "ssd_gb": rf"(?