Когда я пришел работать в эту компанию, у меня уже имелась некоторая база по ip аппаратам, нескольким серверам с asterisk и нашлепкой в виде FreeBPX. Кроме того параллельно работала аналоговая АТС Samsung IDCS500 и в общем-то была основной системой связи в компании, ip телефония работала только для отдела продаж. И все бы варилось так и дальше, но в один прекрасный день был дан указ переводить всех на IP телефонию, были оговорены сроки, закуплено оборудование и план по переводу предприятия в 21 век стал претворятся в жизнь.
Первое что начинает беспокоить в такой ситуации, это быстро нарастающее кол-во телефонных аппаратов, которыми надо как-то управлять, второе, что сильно тревожило была телефонная книга. Если с первым нам мог помочь Endpoint Manager (который кстати выпилили из последних версий FreePBX), то вот с книгой возникали некоторые вопросы:

  • Во первых как обеспеспечить её точность при постоянной смене дислокации/текучести пользователей?
  • Во вторых, как полностью обезличить телефоны. И не заполнять каждый раз имя контакта?

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

from scapy.all import sniff
from scapy.layers.inet import IP
import mysql.connector
import ldap
import getpass
import tftpy
import requests
import os
import time
from string import replace

def conn_ldap(login):
    ad = ldap.initialize('ldap://***.local')
    ad.simple_bind_s('voip@***.local', 'password')
    basedn = 'OU=IT,DC=***,DC=LOCAL'
    basedn_user = 'OU=***,OU=***,DC=***,DC=LOCAL'
    scope = ldap.SCOPE_SUBTREE
    filterexp = "(&(sAMAccountName=" + login + ")(ObjectClass=person))"
    filterexp2 = "(&(ObjectClass=organizationUnit))"
    attrlist = ['cn']
    attrlist2 = ['OU']
    search = ad.search_s(basedn, scope, filterexp, attrlist)
    adname = search[0][1]['cn'][0].decode('utf-8')
    if adname == ' ':
        search = ad.search_s(basedn_user, scope, filterexp2, attrlist2)
        for i in range(1, len(search)+1):
            group = search[i][1]['ou'][0]
            basedn_user2 = 'OU='+group+','+basedn_user
            search = ad.search_s(basedn_user2, scope, filterexp, attrlist)
            adname = search[0][1]['cn'][0].decode('utf-8')
            if adname != ' ':
                return adname
        adname = search[0][1]['cn'][0].decode('utf-8')
    ad.unbind_s()
    return adname


def tftp_file_change(config,place,adname,current_account,current_account_password):

    client = tftpy.TftpClient("192.168.0.3", 69)
    client.download('template.cfg', place)
    fileread = open(place, 'r')
    line = fileread.readlines()
    fileread.close()
    line[5] = (('account.1.label = ').encode('utf-8') + adname.encode('utf-8') + '\n')
    line[2] = (('account.1.auth_name = ').encode('utf-8') + current_account.encode('utf-8') + '\n')
    line[3] = (('account.1.display_name = ').encode('utf-8') + current_account.encode('utf-8') + '\n')
    line[6] = (('account.1.password = ').encode('utf-8') + current_account_password[0][0] + '\n')
    filewrite = open(place, 'w')
    for i in line:
      filewrite.write(i)
    filewrite.close()
    print place
    print config
    client.upload(config,place)


def get_phone_inform(ipaddr):
    fileconf = requests.get('http://admin:admin@'+ipaddr+'/servlet?phonecfg=get[&accounts=1]')
    conf = fileconf.text.split('|')
    current_account = conf[2]
    return current_account


def sniff_frame():
    pcapf = sniff(count=1, timeout=70, filter="dst host 192.168.0.3 and port 5060")
    if len(pcapf) == 0:
        exit()
    frame = pcapf[0]
    macaddr = frame.src
    print macaddr[:8]
    if macaddr[:8] != '80:5e:c0':
        exit()
    ipaddr = frame[0][IP].src
    return macaddr, ipaddr


def conn_mysql(query,fquery,macaddr,qwery2):
    connect = mysql.connector.connect(host='192.168.0.3', database='voip', user='voip_wr', password='***')
    cursor = connect.cursor()
    cursor.execute(fquery)
    state = cursor.fetchall()
    state = bool(state[0][0])
    if state == True:
        cursor.execute(qwery2)
        connect.commit()
        connect.close()
    else:
        cursor.execute(query)
        connect.commit()
        connect.close()


def check_account(current_account):
    connect = mysql.connector.connect(host='192.168.0.3', database='asterisk', user='voip_wr', password='***')
    cursor = connect.cursor()
    qwery = 'select data from sip where id=' + current_account + ' and keyword="secret";'
    cursor.execute(qwery)
    password = cursor.fetchall()
    if password == ' ':
        exit()
    else:
        return password


if __name__ == '__main__':
    macaddr, ipaddr = sniff_frame()
    current_account = get_phone_inform(ipaddr)
    current_account_password = check_account(current_account)
    macaddr = macaddr.replace(':', '')
    ipaddr = ipaddr.decode('utf-8')
    adname = conn_ldap(getpass.getuser())
    query = 'INSERT INTO station (mac, ip, name, number) VALUES (' + '"' + macaddr + '",' + '"' + ipaddr + '",' + '"' + adname + '",' + '"' + get_phone_inform(ipaddr) + '"' + ')'
    qwery2 = 'UPDATE station SET ip=' + '"' + ipaddr + '"' + ', name=' + '"' + adname + '"' + ', number=' + '"' + get_phone_inform(ipaddr) + '"' + ' WHERE mac=' + '"' + macaddr + '"'
    fquery = 'SELECT EXISTS(SELECT mac FROM voip.station WHERE mac=' + '"' + macaddr + '")'
    query = query.encode('utf-8')
    fquery = fquery.encode('utf-8')
    config = macaddr + '.cfg'
    place = os.path.expanduser("~") + "\\" + "AppData\\Local\\" + config
    conn_mysql(query,fquery,macaddr,qwery2)
    tftp_file_change(config,place,adname,current_account,current_account_password)
    requests.get('http://admin:admin@'+ipaddr+'/cgi-bin/ConfigManApp.com?key=AutoP')
    requests.get('http://admin:admin@'+ipaddr+'/cgi-bin/ConfigManApp.com?key=Reboot')

Программа запускается на компьютере пользователя и работает при условии, что компьютер подключен к сети через телефон, так как Yealink T19 не умеет работать в качестве шлюза.

Для начала нам необходимо понять подключен ли? и какой mac и ip имеет наш телефон.

def sniff_frame():
    pcapf = sniff(count=1, timeout=70, filter="dst host 192.168.0.3 and port 5060")
    if len(pcapf) == 0:
        exit()
    frame = pcapf[0]
    macaddr = frame.src
    print macaddr[:8]
    if macaddr[:8] != '80:5e:c0':
        exit()
    ipaddr = frame[0][IP].src
    return macaddr, ipaddr

Сдесь мы используем функцию sniff из фраемворка scapy, с помошью неё мы получаем заранее определенный udp пакет, ждем 70 секуд и если ничего не поймали выходим.

count=1, timeout=70, filter="dst host 192.168.0.3 and port 5060"

Далее убеждаемся, что аппарат действительно Yealink и возвращаем необходимые значения (ip и mac).

С помощью специального запроса выясняем текущий аккаунт на телефоне. Для этого скачивается текущая конфигурация с телефона и распарсивается.

def get_phone_inform(ipaddr):
    fileconf = requests.get('http://admin:admin@'+ipaddr+'/servlet?phonecfg=get[&accounts=1]')
    conf = fileconf.text.split('|')
    current_account = conf[2]
    return current_account

Выясняем пароль для данного аккаунта. Для этого обращаемся к таблице asterisk.sip и в ней к полю data.

def check_account(current_account):
    connect = mysql.connector.connect(host='192.168.0.3', database='asterisk', user='voip_wr', password='***')
    cursor = connect.cursor()
    qwery = 'select data from sip where id=' + current_account + ' and keyword="secret";'
    cursor.execute(qwery)
    password = cursor.fetchall()
    if password == ' ':
        exit()
    else:
        return password

Ну и для финального этапа подключаемся к ldap AD и с помощью sAMAccountName получаемого через функцию getpass.getuser() забираем cn текущего пользователя (в котором обычно содержится ФИО пользователя).

def conn_ldap(login):
    ad = ldap.initialize('ldap://***.local')
    ad.simple_bind_s('voip@***.local', 'password')
    basedn = 'OU=***,DC=***,DC=LOCAL'
    basedn_user = 'OU=***,OU=***,DC=***,DC=LOCAL'
    scope = ldap.SCOPE_SUBTREE
    filterexp = "(&(sAMAccountName=" + login + ")(ObjectClass=person))"
    filterexp2 = "(&(ObjectClass=organizationUnit))"
    attrlist = ['cn']
    attrlist2 = ['OU']
    search = ad.search_s(basedn, scope, filterexp, attrlist)
    adname = search[0][1]['cn'][0].decode('utf-8')
    if adname == ' ':
        search = ad.search_s(basedn_user, scope, filterexp2, attrlist2)
        for i in range(1, len(search)+1):
            group = search[i][1]['ou'][0]
            basedn_user2 = 'OU='+group+','+basedn_user
            search = ad.search_s(basedn_user2, scope, filterexp, attrlist)
            adname = search[0][1]['cn'][0].decode('utf-8')
            if adname != ' ':
                return adname
        adname = search[0][1]['cn'][0].decode('utf-8')
    ad.unbind_s()
    return adname

Подключаемся к заранее созданной таблице в бд (у меня была создана там же) и вносим все то, что мы узнали, а именно: ip, mac, имя пользователя.

def conn_mysql(query,fquery,macaddr,qwery2):
    connect = mysql.connector.connect(host='192.168.0.3', database='voip', user='voip_wr', password='***')
    cursor = connect.cursor()
    cursor.execute(fquery)
    state = cursor.fetchall()
    state = bool(state[0][0])
    if state == True:
        cursor.execute(qwery2)
        connect.commit()
        connect.close()
    else:
        cursor.execute(query)
        connect.commit()
        connect.close()

На этом можно было бы остановится, ведь мы уже создали динамическую адресную книгу спросите вы, но я пошел дальше и прикрутил сюда же автопровизионнинг аппаратов.

Для этого с заранее настроенного tftp сервера скачивается template конфигурация, в которую мы вносим свои изменения и сохраняем с как mac.cfg. Тоесть для Yealink существуют два вида конфигурации, одна глобальная, а вторая применяется к конкретному телефону и должна быть вида mac_телефона.cfg

После всех изменений в файле и сохранению его обратно на tftp сервер мы отдаем команду телефону на провизионинг и перезагрузку аппарата.

def tftp_file_change(config,place,adname,current_account,current_account_password):

    client = tftpy.TftpClient("192.168.0.3", 69)
    client.download('template.cfg', place)
    fileread = open(place, 'r')
    line = fileread.readlines()
    fileread.close()
    line[5] = (('account.1.label = ').encode('utf-8') + adname.encode('utf-8') + '\n')
    line[2] = (('account.1.auth_name = ').encode('utf-8') + current_account.encode('utf-8') + '\n')
    line[3] = (('account.1.display_name = ').encode('utf-8') + current_account.encode('utf-8') + '\n')
    line[6] = (('account.1.password = ').encode('utf-8') + current_account_password[0][0] + '\n')
    filewrite = open(place, 'w')
    for i in line:
      filewrite.write(i)
    filewrite.close()
    print place
    print config
    client.upload(config,place)

requests.get('http://admin:admin@'+ipaddr+'/cgi-bin/ConfigManApp.com?key=AutoP')
requests.get('http://admin:admin@'+ipaddr+'/cgi-bin/ConfigManApp.com?key=Reboot')

После перезагрузки аппарата мы получаем полное фио на экране телефона + всегда корректно заполненную адресную книгу в лице БД, далее остается только прикрутить XML и немного PHP для динамического отображения контента. Таких примеров масса, есть даже у самого YEALINK.

P.S.: Для пущей масштабируемости можно вынести основные настройки (переменные) в отдельный файлик.

Комментарии (2)


  1. antirek
    01.09.2019 19:18

    Всегда полагал, что автопровижн задуман как раз чтобы убрать все эти admin:admin, а то через админку телефона все кому не лень ходят ))


    1. Omenus Автор
      02.09.2019 00:06

      Ага, правда не вижу в этом особой проблемы.