Реагируем на вандализм кабеля быстро, повсеместно и без физических ловушек

Привет.
Есть желание поделиться с сообществом идеей, которая реализована в компании провайдере для оперативного реагирования на повреждение медного кабеля. Речь пойдет о витой паре и Ethernet. Конечно не претендую на изящность решений, однако сервис показал хорошие результаты работы.

Для тех кому лень читать) Как это работает: мониторим падения сессий на радиусе, группируем по коммутаторам, тестим линию, шлем уведомление.

Весь код проекта дать не могу по корпоративным причинам, а тот что есть, уберу под спойлеры для заинтересовавшихся. Да и реализация под каждого провайдера будет разниться. Пост скорее преследует цель поделиться идеей, которая возможно кому-то поможет.
Оборудование в компании на 99% состоит из D-link, поэтому SNMP MIB приведены для этого вендора. Некоторые из них RFC и должны подходить и к другим производителям.

Немного истории о том, из чего всё это вытекло.
Все началось весной 2018. На группу техподдержки (ТП) возросла нагрузка. Помимо отработки звонков абонентов, ТП также координировала монтажников при подключении новых абонентов, а также при выезде на восстановление и дебаг уже существующих клиентов. Нужно было немного разгрузить ТП и дать некие инструменты в руки монтажникам. Было решено сочинить мессенджерного «бота», который на вход принимал бы логин/договор абонента и монтажник прямо в полях мог бы сам произвести минимальный дебаг.

Вставлять весь функционал в одно приложение не хотелось, т.к. по сути такой функционал пригодился бы и техподу в браузере в том же CRM при отработке звонка, поэтому было решено вынести механизмы взаимодействия с оборудованием сети, биллингом, радиусом в отдельный сервис, сделать ему API и подключить по API и бота, и CRM и всё что угодно.

Теперь немного кода и перейдем к сути поста.

И так, что же может потребоваться монтажнику в полях:
1) Тест кабеля конечно же.
2) Просмотр ошибок на порту
3) Просмотр статус порта
4) Посмотреть есть ли МАК-адреса на порту. (вдруг абонент включил кабель в LAN порт вместо WAN)
5) IPTV подписки
6) Посмотреть логи авторизаций
7) Баланс, статус.

Взаимодействовать с коммутаторами будем по SNMP, и кое-где по telnet.
В качестве веб-фреймворка использовал Bottle.
И так,

импортируем нужные либы
#!/usr/bin/python
# -*- coding: utf_8 -*-
 
from bottle import route, run, template, auth_basic, request, error
from lib import crm, snmp, gis, billing
import time


Добавляем лист с API ключами и декораторы для проверки, не будем же мы отдавать данные всем подряд).

код
apikeys = ['RANDOM_KEY1', 'RANDOM_KEY2']

api_error = '{"error":"apikey invalid"}'
host_down_error = '{"error":"host down"}'


def apikey_checker(fn):
    def wrapper(*args, **kwargs):
        if not check_apikey():
            return api_error
        return fn(*args, **kwargs)
    return wrapper


def check_apikey():
    return 'apikey' in request.query and request.query['apikey'] in apikeys


Ну и собственно пара функций для взаимодействия с оборудованием.

код
@route('/port_status//')
@apikey_checker
def get_port_status(ip=' ', port=' '):
    return snmp.port_status(ip, port)

@route('/cable_test//')
@apikey_checker
def get_cable_test(ip, port):
    return snmp.cable_test(ip, port)


Внутри snmp либы у нас словарь с расшифровкой возвращаемых SNMP статусов пары на порту.

Словарь статусов
pair_status = {
    '0': 'ok',
    '1': 'open',
    '2': 'short',
    '3': 'open-short',
    '4': 'crosstalk',
    '5': 'unknown',
    '6': 'count',
    '7': 'no-cable',
    '8': 'other'
}


Заготовка словаря под результат измерений порта. Его мы будем копировать, чтоб не городить каждый раз новый.

Скрытый текст
pair_result = {
    'pairs': {
        1: {
            'status': '-',
            'length': '-'
        },
        2: {
            'status': '-',
            'length': '-'
        },
        3: {
            'status': '-',
            'length': '-'
        },
        4: {
            'status': '-',
            'length': '-'
        },
    }
}


Функция

теста кабеля
def cable_test(ip, port):

    if not check_ip(ip):  # чекаем не прислали ли нам ерунду вместо IP
        return {'error': "IP %s invalid" % (ip)}

    host_status = check_host(ip)  # чекаем доступен ли свитч по управлению
    if host_status['status'] == 'down':
        return {'error': u"Свитч недоступен"}

    result = copy.deepcopy(pair_result)

    # не тестим кабель, если порт UP, т.к. есть оборудование которое теряет
    # линк на порту при тестировании.
    if port_status(ip, port)['status'] == 'down':
        try:

            mib = '.1.3.6.1.4.1.171.12.58.1.1.1.12.%s' % str(
                port)  # миб инициализации тестирования на порту

            # запускаем тест и ждем секунду пока он завершится
            snmp_int_set(ip, mib, 1)
            time.sleep(1)

            # забираем результаты измерений
            result['pairs'][1]['status'] = pair_status[
                snmp_get(ip, '.1.3.6.1.4.1.171.12.58.1.1.1.4.%s' % str(port))]
            result['pairs'][2]['status'] = pair_status[
                snmp_get(ip, '.1.3.6.1.4.1.171.12.58.1.1.1.5.%s' % str(port))]
            result['pairs'][3]['status'] = pair_status[
                snmp_get(ip, '.1.3.6.1.4.1.171.12.58.1.1.1.6.%s' % str(port))]
            result['pairs'][4]['status'] = pair_status[
                snmp_get(ip, '.1.3.6.1.4.1.171.12.58.1.1.1.7.%s' % str(port))]

            result['pairs'][1]['length'] = snmp_get(
                ip, '.1.3.6.1.4.1.171.12.58.1.1.1.8.%s' % str(port))
            result['pairs'][2]['length'] = snmp_get(
                ip, '.1.3.6.1.4.1.171.12.58.1.1.1.9.%s' % str(port))
            result['pairs'][3]['length'] = snmp_get(
                ip, '.1.3.6.1.4.1.171.12.58.1.1.1.10.%s' % str(port))
            result['pairs'][4]['length'] = snmp_get(
                ip, '.1.3.6.1.4.1.171.12.58.1.1.1.11.%s' % str(port))

            return result
        except Exception as e:
            print(e)
            return {'error': u'Возникла ошибка при тестировании кабеля'}
    else:
        return {'error': u'Порт не готов к тестированию. Возможно порт Link UP.'}


фунция вернёт

результат
{
    "pairs": {
        "1": {
            "status": "other",
            "length": "0"
        },
        "2": {
            "status": "open",
            "length": "4"
        },
        "3": {
            "status": "open",
            "length": "4"
        },
        "4": {
            "status": "other",
            "length": "0"
        }
    }
}



Позже добавил еще аналогичную функцию, исключительно для скрипта, она на вход принимает список портов, а не один, и не проверяет статус порта перед тестированием, это не нужно при массовом падении линков.

Вот так примерно стал выглядеть бот
image

Теперь к сути поста.
До реализации дебаг сервера использовалась технология аналогичная описанной в посте habr.com/post/188730. Петля на порту с включенным SNMP трапом. При падении «самолинка» на порту в мониторинг падало сообщение об этом.
Первым делом прикрутил скрипт, чтоб при падении отлеживаемого линка дебаг-сервер сходил на коммутатор, проверил, действительно ли лежит порт, а не просто моргнул, и пары на нем открыты или закорочены, и после этого отправил сообщение операторам.
Однако, таких физических ловушек было примерно только на 10% коммутаторов, а этого оказалось мало.

Позже придумали мониторить радиус. И это позволило увеличить процент покрытия мониторингом до 100%. И вот тут уже все разнится от инфраструктуры провайдера.
Периодически смотрим, сколько упало клиентских сессий с того или иного коммутатора. Сделать это легко, если на коммутаторах включены circuit_id, который имеет вид
D4: CA:6D:0A:66: C9:: 192.168.20.86:: 20
Тут у нас MAC абонента, IP коммутатора, номер порта абонента. Т.е. всё что нужно для дебага.
Группируем завершенные сессии по IP коммутатора, если таких сессий больше некоторого количества (у нас установлен триггер на 2 сессии в минуту), то скрипт обращается к дебаг серверу и тестит порты упавших сессий. Если порты всё также лежат и на них открыты или закорочены пары кабеля, и длинна хотя бы на двух портах одинакова (± 2 метра), а именно так выглядит обрез кабеля глазами свитча, то считаем ситуацию подозрительной и отправляем сообщение оператору.
Конечно будут ложные срабатывания, когда моргнёт свет в доме, или просто совпадет что абоненты выключат кабель одновременно и длина будет одинакова, но это тот случай, как говорится, когда лучше перебдеть. Кроме того можно сделать ограничение по длине (реагировать только на короткие длины), количеству одновременных падений и пр.

Вот реальное сообщение о подозрительном событии.

image

И результаты отработки таких сообщений

image

Был случай, когда скрипт прислал подобное сообщение, и через пару секунд свитч ушел в оффлайн, т.к. повредили оптику, и если бы не оперативность софта, то ситуацию приняли бы за типичное отключение электричества в доме.
В другой раз управляющая компания без предупреждения начала делать ремонт крыши и к ним прилетели ВОХР с автоматами, внезапный стресс для слесарей)

Так скрипт стал показывать неплохие результаты и за 4 месяца работы было успешно отработано ВОХР, полицией, и самими сотрудниками провайдера более 10 случаев вандализма. Поэтому я и решил поделиться концептом такого мониторинга.

Сейчас скрипт мониторит около 15000 коммутаторов без каких-либо физических «ловушек» и SNMP трапов.

Всем удачи в новом году!

© Habrahabr.ru