Compare commits

...

5 Commits

Author SHA1 Message Date
Ember 'n0emis' Keske 3a113c9baf
try to fix sqyalchemy-error wieh deleting a forkrankmember 2022-07-27 14:12:02 +02:00
Ember 'n0emis' Keske 2f1347f341
allow to claim extensions 2022-07-20 23:39:58 +02:00
Ember 'n0emis' Keske 4382942ad8
sync after creating temp-extensions 2022-07-20 15:44:11 +02:00
Ember 'n0emis' Keske d6d664b469
do more dect sync stuff 2022-07-10 13:04:20 +02:00
Ember 'n0emis' Keske a444e41787
routing: save ids when adding to database, use correct queue 2022-07-10 12:40:05 +02:00
9 changed files with 171 additions and 37 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
__pycache__ __pycache__
dist/ dist/
.venv

View File

@ -333,6 +333,8 @@ cdr_finalize=UPDATE users SET inuse=(CASE WHEN inuse>0 THEN inuse-1 ELSE 0 END)
Just choose one of your RFPs as your OMM. Just choose one of your RFPs as your OMM.
Find out the MAC address and fill it in the according fields in the kea config before. Find out the MAC address and fill it in the according fields in the kea config before.
To be able to have secure sip connections between the OMM and yate, generate a 16-characters long hexadecimal sip-secret.
Create a privileged user account and add the credentials to `fieldpoc_config.json`: Create a privileged user account and add the credentials to `fieldpoc_config.json`:
``` ```
@ -341,6 +343,7 @@ Create a privileged user account and add the credentials to `fieldpoc_config.jso
"host": "10.222.222.11", "host": "10.222.222.11",
"username": "omm", "username": "omm",
"password": "<password>" "password": "<password>"
"sipsecret": "<secret>"
} }
} }
``` ```

View File

@ -42,7 +42,7 @@ class Config:
return self.dect.check() return self.dect.check()
class ExtensionConfig: class ExtensionConfig(ConfigBase):
def __init__(self, c): def __init__(self, c):
self.num = c[0] self.num = c[0]
self._c = c[1] self._c = c[1]

View File

@ -41,12 +41,16 @@ class Controller:
if data == "help": if data == "help":
self.request.sendall("""Availiable commands: self.request.sendall("""Availiable commands:
help Show this info help Show this info
handlers Show currently running handlers handlers Show currently running handlers
sync Start syncing sync Start syncing
queues Show queue stats queues Show queue stats
exit Disconnect reload Reload extension config file
claim <ext> <token> claim dect extension
exit Disconnect
""".encode("utf-8")) """.encode("utf-8"))
elif data == "":
continue
elif data == "quit" or data == "exit": elif data == "quit" or data == "exit":
self.request.sendall("disconnecting\n".encode("utf-8")) self.request.sendall("disconnecting\n".encode("utf-8"))
return return
@ -56,6 +60,14 @@ exit Disconnect
self.fp.queue_all({"type": "sync"}) self.fp.queue_all({"type": "sync"})
elif data == "queues": elif data == "queues":
self.request.sendall(("\n".join(["{} {}".format(name, queue.qsize()) for name, queue in self.fp.queues.items()]) + "\n").encode("utf-8")) self.request.sendall(("\n".join(["{} {}".format(name, queue.qsize()) for name, queue in self.fp.queues.items()]) + "\n").encode("utf-8"))
elif data == "reload":
self.fp.reload_config()
elif data.startswith("claim"):
data = data.split(" ")
if len(data) == 3:
self.fp.queue_all({"type": "claim", "extension": data[1], "token": data[2]})
else:
self.request.sendall("error: You have to specify calling extension and token\n".encode("utf-8"))
else: else:
self.request.sendall("Unknown command, type 'help'\n".encode("utf-8")) self.request.sendall("Unknown command, type 'help'\n".encode("utf-8"))

View File

@ -3,6 +3,7 @@
import logging import logging
import mitel_ommclient2 import mitel_ommclient2
import time import time
import hashlib
logger = logging.getLogger("fieldpoc.dect") logger = logging.getLogger("fieldpoc.dect")
@ -18,23 +19,46 @@ class Dect:
ommsync=True, ommsync=True,
) )
def get_temp_number(self): @property
temp_num_prefix = next(self.fp.extensions.extensions_by_type("temp")).num def temp_num_prefix(self):
return next(self.fp.extensions.extensions_by_type("temp")).num
def load_temp_extensions(self):
current_temp_extension = 0 current_temp_extension = 0
used_temp_extensions = [u.num[len(temp_num_prefix):] for u in self.c.find_users(lambda u: u.num.startswith(temp_num_prefix))] used_temp_extensions = self.c.find_users(lambda u: u.num.startswith(self.temp_num_prefix))
for u in used_temp_extensions:
temp_num = u.num
self.fp.temp_extensions[temp_num] = {
"name": f"Temp {temp_num[4:]}",
"type": "dect",
"trunk": False,
"dialout_allowed": False,
}
def get_temp_number(self):
current_temp_extension = 0
used_temp_extensions = [num[len(self.temp_num_prefix):] for num, ext in self.fp.temp_extensions.items()]
while "{:0>4}".format(current_temp_extension) in used_temp_extensions: while "{:0>4}".format(current_temp_extension) in used_temp_extensions:
current_temp_extension += 1 current_temp_extension += 1
return "{}{:0>4}".format(temp_num_prefix, current_temp_extension) return "{}{:0>4}".format(self.temp_num_prefix, current_temp_extension)
def get_sip_password_for_number(self, num): def get_sip_password_for_number(self, num):
return num return hashlib.sha256(bytes.fromhex((self.fp.config.dect.sipsecret + str(num))[-16:])).hexdigest()[:16]
def create_and_bind_user(self, d, num):
u = self.c.create_user(num)
self.c.attach_user_device(u.uid, d.ppn)
self.c.set_user_relation_fixed(u.uid)
self.c.set_user_sipauth(u.uid, num, self.get_sip_password_for_number(num))
return u
def run(self): def run(self):
logger.info("initialising connection to OMM") logger.info("initialising connection to OMM")
self._init_client() self._init_client()
self.load_temp_extensions()
while True: while True:
msg = self.fp.queues["dect"].get() msg = self.fp.queues["dect"].get()
@ -43,13 +67,90 @@ class Dect:
if msg.get("type") == "stop": if msg.get("type") == "stop":
break break
elif msg.get("type") == "sync": elif msg.get("type") == "sync":
unbound_devices = self.c.find_devices(lambda d: d.relType == mitel_ommclient2.types.PPRelTypeType("Unbound")) logger.info("syncing")
for d in unbound_devices: extensions = self.fp.extensions.extensions_by_type("dect")
print(d) extensions_by_num = {e.num: e for e in extensions}
temp_num = self.get_temp_number() extensions_by_ipei = {e._c['dect_ipei']: e for _, e in extensions_by_num.items() if e._c.get('dect_ipei')}
u = self.c.create_user(temp_num) created_tmp_ext = False
print(u)
self.c.attach_user_device(u.uid, d.ppn) users_by_ext = {}
self.c.set_user_relation_fixed(u.uid) users_by_uid = {}
self.c.set_user_sipauth(u.uid, temp_num, self.get_sip_password_for_number(temp_num)) for user in self.c.get_users():
e = extensions_by_num.get(user.num)
if not e:
# user in omm, but not as dect in nerd
if user.num.startswith(next(self.fp.extensions.extensions_by_type("temp")).num):
users_by_ext[user.num] = user
users_by_uid[user.uid] = user
else:
pass
# TODO: delete in omm
continue
elif e._c['name'] != user.name:
self.c.set_user_name(user.uid, e._c['name'])
if e._c.get('dect_ipei') and user.relType != mitel_ommclient2.types.PPRelTypeType("Unbound"):
d = self.c.get_device(user.ppn)
if d.ipei != e._c['dect_ipei']:
logger.debug(f"Detaching {user} {d}")
self.c.detach_user_device(user.uid, user.ppn)
self.c.set_user_sipauth(user.uid, e.num, self.get_sip_password_for_number(e.num))
users_by_ext[user.num] = user
users_by_uid[user.uid] = user
for d in self.c.get_devices():
e = extensions_by_ipei.get(d.ipei)
if e:
# device is in nerd
u = users_by_ext.get(e.num)
if u and d.relType == mitel_ommclient2.types.PPRelTypeType("Unbound"):
logger.debug(f'Binding user for {d}')
self.c.attach_user_device(u.uid, d.ppn)
self.c.set_user_relation_fixed(u.uid)
elif d.relType != mitel_ommclient2.types.PPRelTypeType("Unbound"):
ui = users_by_uid.get(d.uid)
if ui.num != e.num:
logger.debug(f'User for {d} has wrong number')
if self.fp.temp_extensions.get(ui.num):
self.fp.temp_extensions.pop(ui.num)
self.c.set_user_num(d.uid, e.num)
self.c.set_user_sipauth(d.uid, e.num, self.get_sip_password_for_number(e.num))
self.c.set_user_name(user.uid, e._c['name'])
else:
logger.debug(f'Creating and binding user for {d}')
user = self.create_and_bind_user(d, e.num)
self.c.set_user_name(user.uid, e._c['name'])
elif d.relType == mitel_ommclient2.types.PPRelTypeType("Unbound"):
temp_num = self.get_temp_number()
logger.debug(f'Creating and binding tmp-user for {d}: {temp_num}')
user = self.create_and_bind_user(d, temp_num)
self.c.set_user_name(user.uid, f"Temp {temp_num[4:]}")
self.fp.temp_extensions[temp_num] = {
"name": f"Temp {temp_num[4:]}",
"type": "dect",
"trunk": False,
"dialout_allowed": False,
}
created_tmp_ext = True
if created_tmp_ext:
self.fp.queues['routing'].put({"type": "sync"})
elif msg.get("type") == "claim":
e = None
for ext in self.fp.extensions.extensions_by_type("dect"):
if ext._c.get('dect_claim_token') and ext._c['dect_claim_token'] == msg.get("token")[4:]:
e = ext
break
if e:
user = next(self.c.find_users(lambda u: u.num == msg.get("extension")))
if self.fp.temp_extensions.get(user.num):
self.fp.temp_extensions.pop(user.num)
self.c.set_user_num(user.uid, e.num)
self.c.set_user_sipauth(user.uid, e.num, self.get_sip_password_for_number(e.num))
self.c.set_user_name(user.uid, e._c['name'])
self.c.connection.close()

View File

@ -17,6 +17,7 @@ logger = logging.getLogger("fieldpoc.fieldpoc")
class FieldPOC: class FieldPOC:
config = None config = None
extensions = None extensions = None
temp_extensions = {}
def __init__(self, config_file_path, extensions_file_path): def __init__(self, config_file_path, extensions_file_path):
logger.info("loading configuration") logger.info("loading configuration")
@ -75,6 +76,10 @@ class FieldPOC:
logger.info("started components") logger.info("started components")
def reload_config(self):
self._load_extensions()
self.queue_all({"type": "sync"})
def _load_config(self): def _load_config(self):
self.config = config.Config(json.loads(self.config_file_path.read_text())) self.config = config.Config(json.loads(self.config_file_path.read_text()))

View File

@ -27,11 +27,12 @@ class YwsdYateModel(YateModel):
@classmethod @classmethod
def create(cls, diffsync, ids, attrs): def create(cls, diffsync, ids, attrs):
with diffsync.engine.connect() as conn: with diffsync.engine.connect() as conn:
conn.execute( result = conn.execute(
Yate.table.insert().values( Yate.table.insert().values(
guru3_identifier=ids["guru3_identifier"], **attrs guru3_identifier=ids["guru3_identifier"], **attrs
) )
) )
attrs["yate_id"] = result.inserted_primary_key[0]
return super().create(diffsync, ids=ids, attrs=attrs) return super().create(diffsync, ids=ids, attrs=attrs)
def update(self, attrs): def update(self, attrs):
@ -84,7 +85,7 @@ class YwsdExtensionModel(ExtensionModel):
@classmethod @classmethod
def create(cls, diffsync, ids, attrs): def create(cls, diffsync, ids, attrs):
with diffsync.engine.connect() as conn: with diffsync.engine.connect() as conn:
conn.execute( result = conn.execute(
Extension.table.insert().values( Extension.table.insert().values(
extension=ids["extension"], extension=ids["extension"],
type=attrs["extension_type"], type=attrs["extension_type"],
@ -98,6 +99,7 @@ class YwsdExtensionModel(ExtensionModel):
), ),
) )
) )
attrs["extension_id"] = result.inserted_primary_key[0]
return super().create(diffsync, ids=ids, attrs=attrs) return super().create(diffsync, ids=ids, attrs=attrs)
def update(self, attrs): def update(self, attrs):
@ -210,7 +212,7 @@ class YwsdForkRankModel(ForkRankModel):
@classmethod @classmethod
def create(cls, diffsync, ids, attrs): def create(cls, diffsync, ids, attrs):
with diffsync.engine.connect() as conn: with diffsync.engine.connect() as conn:
conn.execute( result = conn.execute(
ForkRank.table.insert().values( ForkRank.table.insert().values(
extension_id=diffsync.get( extension_id=diffsync.get(
"extension", ids["extension"] "extension", ids["extension"]
@ -219,6 +221,7 @@ class YwsdForkRankModel(ForkRankModel):
**attrs, **attrs,
) )
) )
attrs["forkrank_id"] = result.inserted_primary_key[0]
return super().create(diffsync, ids=ids, attrs=attrs) return super().create(diffsync, ids=ids, attrs=attrs)
def update(self, attrs): def update(self, attrs):
@ -275,7 +278,7 @@ class YwsdForkRankMemberModel(ForkRankMemberModel):
conn.execute( conn.execute(
ForkRank.member_table.update() ForkRank.member_table.update()
.where( .where(
_and( sqlalchemy.and_(
ForkRank.member_table.c.forkrank_id ForkRank.member_table.c.forkrank_id
== self.diffsync.get("forkrank", self.forkrank).forkrank_id, == self.diffsync.get("forkrank", self.forkrank).forkrank_id,
ForkRank.member_table.c.extension_id ForkRank.member_table.c.extension_id
@ -290,7 +293,7 @@ class YwsdForkRankMemberModel(ForkRankMemberModel):
with self.diffsync.engine.connect() as conn: with self.diffsync.engine.connect() as conn:
conn.execute( conn.execute(
ForkRank.member_table.delete().where( ForkRank.member_table.delete().where(
_and( sqlalchemy.and_(
ForkRank.member_table.c.forkrank_id ForkRank.member_table.c.forkrank_id
== self.diffsync.get("forkrank", self.forkrank).forkrank_id, == self.diffsync.get("forkrank", self.forkrank).forkrank_id,
ForkRank.member_table.c.extension_id ForkRank.member_table.c.extension_id
@ -310,11 +313,15 @@ class BackendNerd(diffsync.DiffSync):
top_level = ["yate", "extension", "user", "forkrank", "forkrankmember"] top_level = ["yate", "extension", "user", "forkrank", "forkrankmember"]
def __init__(self, fp, *args, **kwargs):
self.fp = fp
super().__init__(*args, **kwargs)
def load(self, data): def load(self, data):
yate_dect = self.yate( #yate_dect = self.yate(
guru3_identifier="dect", hostname="dect", voip_listener="local" # guru3_identifier="dect", hostname="dect", voip_listener="local"
) #)
self.add(yate_dect) #self.add(yate_dect)
yate_sip = self.yate( yate_sip = self.yate(
guru3_identifier="sip", hostname="sip", voip_listener="local" guru3_identifier="sip", hostname="sip", voip_listener="local"
) )
@ -339,6 +346,8 @@ class BackendNerd(diffsync.DiffSync):
active=True, active=True,
) )
self.add(frm) self.add(frm)
elif value["type"] in ["temp"]:
continue
extension = self.extension( extension = self.extension(
extension=key, extension=key,
@ -361,7 +370,7 @@ class BackendNerd(diffsync.DiffSync):
user = self.user( user = self.user(
username=key, username=key,
displayname=value["name"], displayname=value["name"],
password=value.get("sip_password", key), password=value.get("sip_password", self.fp._dect.get_sip_password_for_number(key)),
user_type=user_type[value["type"]], user_type=user_type[value["type"]],
trunk=value["trunk"], trunk=value["trunk"],
static_target=value.get("static_target", ""), static_target=value.get("static_target", ""),
@ -473,8 +482,8 @@ class Routing:
def run(self): def run(self):
while True: while True:
msg = self.fp.queues["dect"].get() msg = self.fp.queues["routing"].get()
self.fp.queues["dect"].task_done() self.fp.queues["routing"].task_done()
if msg.get("type") == "stop": if msg.get("type") == "stop":
break break
@ -482,8 +491,10 @@ class Routing:
logger.info("syncing") logger.info("syncing")
state_fieldpoc = BackendNerd() state_fieldpoc = BackendNerd(self.fp)
state_fieldpoc.load(self.fp.extensions._c) extensions = self.fp.extensions._c.copy()
extensions['extensions'].update(self.fp.temp_extensions)
state_fieldpoc.load(extensions)
state_yate = BackendYwsd() state_yate = BackendYwsd()
state_yate.load("postgresql+psycopg2://{}:{}@{}/{}".format( state_yate.load("postgresql+psycopg2://{}:{}@{}/{}".format(
self.fp.config.database.username, self.fp.config.database.username,

View File

@ -7,6 +7,7 @@
"host": "10.222.222.11", "host": "10.222.222.11",
"username": "omm", "username": "omm",
"password": "xxx" "password": "xxx"
"sipsecret": "51df84aace052b0e75b8c1da5a6da9e2"
}, },
"yate": { "yate": {
"host": "127.0.0.1", "host": "127.0.0.1",

View File

@ -95,14 +95,14 @@
"name": "Temporary Numbers", "name": "Temporary Numbers",
"trunk": false, "trunk": false,
"dialout_allowed": true, "dialout_allowed": true,
"type": "static" "type": "temp"
}, },
"9999": { "9997": {
"name": "DECT Claim Extensions", "name": "DECT Claim Extensions",
"type": "static", "type": "static",
"dialout_allowed": false, "dialout_allowed": false,
"trunk": true, "trunk": true,
"static_target": "external/nodata//opt/nerdsync/claim.py" "static_target": "external/nodata//run/current-system/sw/bin/dect_claim"
} }
} }
} }