Compare commits
5 Commits
c2ef36c5a7
...
3a113c9baf
Author | SHA1 | Date | |
---|---|---|---|
|
3a113c9baf | ||
|
2f1347f341 | ||
|
4382942ad8 | ||
|
d6d664b469 | ||
|
a444e41787 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
__pycache__
|
__pycache__
|
||||||
dist/
|
dist/
|
||||||
|
.venv
|
||||||
|
@ -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>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -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]
|
||||||
|
@ -45,8 +45,12 @@ 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
|
||||||
|
reload Reload extension config file
|
||||||
|
claim <ext> <token> claim dect extension
|
||||||
exit Disconnect
|
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"))
|
||||||
|
|
||||||
|
125
fieldpoc/dect.py
125
fieldpoc/dect.py
@ -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)
|
|
||||||
|
users_by_ext = {}
|
||||||
|
users_by_uid = {}
|
||||||
|
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.attach_user_device(u.uid, d.ppn)
|
||||||
self.c.set_user_relation_fixed(u.uid)
|
self.c.set_user_relation_fixed(u.uid)
|
||||||
self.c.set_user_sipauth(u.uid, temp_num, self.get_sip_password_for_number(temp_num))
|
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()
|
||||||
|
@ -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()))
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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",
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user