Привет Хабр! Меня зовут Дмитрий и я разработчик DCIменеджер — панели управления оборудованием ISPsystem. Я довольно долго провёл в команде, занимающейся разработкой программного обеспечения для управления коммутаторами.
Вместе мы переживали взлеты и падения: от написания сервисов по управлению оборудованием до краха офисной сети и часовых свиданий в серверной в надежде не потерять своих близких.
И вот пришло время испытаний.
Некоторые процессоры нам удалось охватить готовыми решениями для тестирования.
Но с Джунипером это не сработало.
Исследования и внедрение послужили идеей написания этой статьи.
Если интересно, добро пожаловать под кат! DCImanager работает с разными типами оборудования: коммутаторами, распределителями питания, серверами.
В настоящее время DCImanager поддерживает четыре обработчика переключателей.
Два по протоколу SNMP (Cisco Catalyst и общий snmp common) и еще два по протоколу NETCONF (Juniper с поддержкой ELS и без нее).
Все работы с оборудованием мы подробно освещаем тестами.
Для автоматического тестирования невозможно использовать реальное оборудование: тесты запускаются при каждом нажатии и выполняются параллельно.
Вот почему мы стараемся использовать эмуляторы.
Обработчики, поддерживающие протокол SNMP, нам удалось охватить тестами с использованием библиотеки SNMP Agent Simulator. Но были проблемы с Джунипером.
Поискав готовые решения, мы выбрали пару библиотек, но одна из них не запустилась, а другая не сделала того, что нужно — я потратил больше времени на то, чтобы оживить это чудо.
Возник вопрос, как эмулировать работу коммутаторов Juniper? Juniper использует протокол NETCONF, который, в свою очередь, работает поверх SSH. В голове промелькнула идея написать небольшой сервис, который бы работал поверх SSH и эмулировал работу свитча.
Соответственно, нам понадобится сам сервис, а также «снимок» Juniper для эмуляции данных.
В snmpsim снимок представляет собой полную копию состояния коммутатора со всеми поддерживаемыми OID и их текущими значениями.В Джунипере все немного сложнее: такую картинку сделать нельзя.
Здесь имеется в виду снапшот как набор шаблонов типа: запрос-ответ.
Часть первая: посадка архитектуры
Сейчас мы активно расширяем наш «зоопарк» процессоров для работы со свитчами.Скоро у нас появятся новые процессоры, и мы не сможем охватить их все готовыми решениями для тестирования.
Однако можно попробовать написать общую сервисную архитектуру, которая будет моделировать работу разных устройств по разным протоколам.
В самом простом варианте — фабрика, которая в зависимости от протокола и обработчика (некоторые свитчи могут работать по нескольким протоколам) будет возвращать объект свитча, в котором уже будет реализована вся логика его поведения.
В случае Juniper это небольшой парсер запросов.
В зависимости от входного rpc-запроса с параметрами он выполнит необходимые действия.
Важное ограничение: мы не сможем полностью смоделировать работу переключателя.
На описание всей логики уйдет много времени, а добавляя новый функционал в реальный обработчик, нам придется редактировать макет переключателя.
Часть вторая: выбор почвы для посадки
Мой взгляд упал на библиотеку paramiko, предоставляющую удобный интерфейс для работы по протоколу SSH. Для начала я не хотел разбирать архитектуру, а проверил базовые вещи, например соединение и какой-нибудь простой запрос.Мы все еще проводим исследования.
Поэтому с авторизацией не заморачиваемся: простой ServerInterface и сокет-сервер в сочетании дают нам нечто похожее на рабочий вариант:
Примерная реализация того, что хотелось бы видеть, но выглядит пугающе Когда клиент подключается к серверу, второй должен ответить списком своих возможностей.class SshServer(paramiko.ServerInterface): def check_auth_password(self, user, password): if user == SSH_USER_NAME and password == SSH_USER_PASSWORD: return paramiko.AUTH_SUCCESSFUL return paramiko.AUTH_FAILED socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) socket.bind(("127.0.0.1", 8300)) socket.listen(10) client, address = socket.accept() session = paramiko.Transport(client) server = SshServer() session.start_server(server=server)
Например вот так: reply = """
<hello>
<capabilities>
<capability>urn:ietf:params:xml:ns:netconf:base:1.0</capability>
<capability>xml.juniper.net/netconf/junos/1.0</capability>
<capability>xml.juniper.net/dmi/system/1.0</capability>
</capabilities>
<session-id>1</session-id>
</hello>
]]>]]>
"""
socket.send(reply)
Да, это XML ]]> ]]> Во всяком случае, код нестабильный.
В этой реализации есть проблема с закрытием сокета.
Я нашел пару зарегистрированных проблем с этой проблемой в Paramiko. Я отложил его на время, решив проверить оставшийся вариант.
Часть третья: посадка
Туз в рукаве — перевернутый.Это среда разработки сетевых приложений с поддержкой большого количества протоколов.
Он имеет обширную документацию и замечательный модуль.
Кред , что нам поможет. Кред — это механизм аутентификации, который позволяет подключаться к системе по различным сетевым протоколам в зависимости от ваших требований.
Для организации всей логики используется Область — часть приложения, отвечающая за бизнес-логику и доступ к ее объектам.
Но обо всем по порядку.
Ядро входа в систему Портал .
Если мы хотим написать надстройку по сетевому протоколу, мы определяем стандартный портал.
У него уже есть методы:
- логин (даёт клиенту доступ к подсистеме)
- RegisterChecker (непосредственная проверка учетных данных).
Поскольку клиент уже авторизован, именно здесь начинается логика нашей надстройки SSH. У этого интерфейса есть только один метод requestAvatar, который вызывается при успешной авторизации в Портале и возвращает основной объект — SwitchProtocolAvatar: @implementer(portal.IRealm)
class SwitchRealm(object):
def __init__(self, switch_obj):
self.switch_obj = switch_obj
def requestAvatar(self, avatarId, mind, *interfaces):
return interfaces[0], SwitchProtocolAvatar(avatarId, switch_obj=self.switch_obj), lambda: None
Простейшая реализация объекта Realm, возвращающая требуемый аватар.
За управление бизнес-логикой отвечают специальные объекты — Аватары.
В нашем случае именно здесь начинается надстройка к протоколу SSH. При отправке запроса данные поступают в SwitchProtocolAvatar, который проверяет движок запросов и обновляет конфигурацию: class SwitchProtocolAvatar(avatar.ConchUser):
def __init__(self, username, switch_core):
avatar.ConchUser.__init__(self)
self.username = username
self.channelLookup.update({b'session': session.SSHSession})
netconf_protocol = switch_core.get_netconf_protocol()
if netconf_protocol:
self.subsystemLookup.update({b'netconf': netconf_protocol})
Проверяем подсистему и обновляем конфигурацию при условии, что этот обработчик переключения работает через NETCONF Кстати о протоколах.
Не забывайте, что мы работаем с NETCONF, и приступим.
Протокол используется для написания дополнений к существующим протоколам и реализации собственной логики.
Интерфейс этого класса прост:
- dataReceived — используется для обработки событий получения данных;
- makeConnection — используется для установления соединения;
- ConnectionMade — используется, когда соединение уже установлено.
Здесь вы можете определить некоторую логику, прежде чем клиент начнет отправлять запросы.
В нашем случае нам нужно отправить список наших возможностей.
class Netconf(Protocol):
def __init__(self, capabilities=None):
self.session_count = 0
self.capabilities = capabilities
def __call__(self, *args, **kwargs):
return self
def connectionMade(self):
self.session_count += 1
self.send_capabilities()
def send_capabilities(self):
rpc_capabilities_reply = "<hello><capabilities>{capabilities}</capabilities>" \
"<session-id>{session_id}</session-id></hello>]]>]]>"
rpc_capabilities = "".
join(f"<capability>{cap}</capability>" for cap in self.capabilities)
self.transport.write(rpc_capabilities_reply.format(capabilities=rpc_capabilities,
session_id=self.session_count))
def dataReceived(self, data):
# Process received data
pass
Минимальная реализация оболочки протокола.
Удалена ненужная логика для ясности.
Начинаем скатывать нашу матрешку.
Поскольку мы используем надстройку для SSH, нам необходимо реализовать логику сервера SSH. В нем мы определим ключи для сервера и обработчики для SSH-сервисов.
Реализация этого класса нас не особо интересует, так как авторизация будет по паролю: class SshServerFactory(factory.SSHFactory):
protocol = SSHServerTransport
publicKeys = {b'ssh-rsa': keys.Key.fromFile(SERVER_RSA_PUBLIC)}
privateKeys = {b'ssh-rsa': keys.Key.fromFile(SERVER_RSA_PRIVATE)}
services = {
b'ssh-userauth': userauth.SSHUserAuthServer,
b'ssh-connection': connection.SSHConnection
}
def getPrimes(self):
return PRIMES
Реализация SSH-сервера Для работы SSH-сервера необходимо определить логику сеансов, которая работает вне зависимости от того, по какому протоколу они к нам пришли и какой интерфейс запрошен: class EchoProtocol(protocol.Protocol):
def dataReceived(self, data):
if data == b'\r':
data = b'\r\n'
elif data == b'\x03': # Ctrl+C
self.transport.loseConnection()
return
self.transport.write(data)
class Session:
def __init__(self, avatar):
pass
def getPty(self, term, windowSize, attrs):
pass
def execCommand(self, proto, cmd):
pass
def openShell(self, transport):
protocol = EchoProtocol()
protocol.makeConnection(transport)
transport.makeConnection(session.wrapProtocol(protocol))
def eofReceived(self):
pass
def closed(self):
pass
Логика сеанса для всех описанных интерфейсов Чуть не забыл про сам обработчик.
После всех проверок и авторизаций логика перемещается в объект, эмулирующий работу коммутатора.
Здесь можно определить логику обработки запросов: получение или редактирование интерфейсов, настройку устройств и т.д. class Juniper:
def __init__(self):
self.protocol = Netconf(capabilities=self.capabilities())
def get_netconf_protocol(self):
return self.protocol
@staticmethod
def capabilities():
return [
"Candidate1_0urn:ietf:params:xml:ns:netconf:capability:candidate:1.0",
"urn:ietf:params:xml:ns:netconf:capability:confirmed-commit:1.0",
"urn:ietf:params:xml:ns:netconf:capability:validate:1.0",
"urn:ietf:params:xml:ns:netconf:capability:url:1.0Эprotocol=http,ftp,file",
"urn:ietf:params:netconf:capability:candidate:1.0",
"urn:ietf:params:netconf:capability:confirmed-commit:1.0",
"urn:ietf:params:netconf:capability:validate:1.0",
"urn:ietf:params:netconf:capability:url:1.0Эscheme=http,ftp,file"
]
Основная логика обработчика.
Вырезали весь функционал и обработку запросов, оставив только приобретение возможностей Что ж, мы наконец-то объединим все это воедино.
Регистрируем адаптер сеанса (описываем поведение подключения), определяем метод подключения по логину и паролю, настраиваем Портал и запускаем наш сервис: components.registerAdapter(Session, SwitchProtocolAvatar, session.ISession)
switch_factory = SwitchFactory()
switch = switch_factory.get("juniper")
portal = portal.Portal(CustomRealm(switch))
credential_source = InMemoryUsernamePasswordDatabaseDontUse()
credential_source.addUser(b'admin', b'admin')
portal.registerChecker(credential_source)
SshServerFactory.portal = portal
reactor.listenTCP(830, SshServerFactory())
reactor.run()
Настройка и запуск сервера Запускаем мок-сервер.
Для проверки работоспособности можно подключиться с помощью библиотеки ncclient. Достаточно простой проверки соединения и просмотра возможностей сервера: from ncclient import manager
connection = manager.connect(host="127.0.0.1",
port=830,
username="admin",
password="admin",
timeout=60,
device_params={'name': 'junos'},
hostkey_verify=False)
for capability in connection.server_capabilities:
print(capability)
Подключаемся к мок-серверу по протоколу NETCONF и выводим список возможностей сервера.
Результат запроса представлен ниже.
Мы успешно установили соединение, и сервер выдал нам список своих возможностей: Candidate1_0urn:ietf:params:xml:ns:netconf:capability:candidate:1.0
urn:ietf:params:xml:ns:netconf:capability:confirmed-commit:1.0
urn:ietf:params:xml:ns:netconf:capability:validate:1.0
urn:ietf:params:xml:ns:netconf:capability:url:1.0Эprotocol=http,ftp,file
urn:ietf:params:netconf:capability:candidate:1.0
urn:ietf:params:netconf:capability:confirmed-commit:1.0
urn:ietf:params:netconf:capability:validate:1.0
urn:ietf:params:netconf:capability:url:1.0Эscheme=http,ftp,file
Возможности сервера
Заключение
У этого решения есть немало плюсов и минусов.С одной стороны, мы тратим много времени на реализацию и описание всей логики обработки запросов.
С другой стороны, мы получаем возможность гибкой настройки и эмуляции поведения.
Но главное — масштабируемость.
Платформа Twisted богата функциональностью и поддерживает большое количество протоколов, поэтому вы можете легко описывать новые интерфейсы обработчиков.
И если все хорошо продумать, эту архитектуру можно использовать не только для работы с коммутаторами, но и для другого оборудования.
Хотелось бы узнать мнение читателей.
Делали ли вы это и если да, то какие технологии использовали и как выстроили процесс тестирования? Теги: #python #программирование #разработка с использованием #ispsystem #juniper #juniper
-
План B
19 Oct, 24 -
Как Записывать Скринкасты
19 Oct, 24