Verified Commit 8a2fe12b authored by Štěpán Henek's avatar Štěpán Henek 🌩

remote: adding subordinates implemented

parent 42302309
Pipeline #45353 passed with stage
in 24 minutes and 42 seconds
......@@ -56,6 +56,14 @@ def path_exists(path):
return os.path.exists(inject_file_root(path))
def makedirs(path: str, mask: int):
""" Creates directories on the given path
:param path: path to be created
:param mask: last dir mask
"""
os.makedirs(inject_file_root(path), mask)
class BaseFile(object):
def _file_content(self, path):
""" Returns a content of a file
......
......@@ -23,7 +23,9 @@ import logging
import tarfile
import base64
import json
import uuid
import typing
import pathlib
import shutil
from io import BytesIO
from collections import OrderedDict
......@@ -31,16 +33,20 @@ from collections import OrderedDict
from foris_controller.app import app_info
from foris_controller_backends.cmdline import AsyncCommand, BaseCmdLine
from foris_controller_backends.files import BaseFile
from foris_controller_backends.files import BaseFile, makedirs, inject_file_root
from foris_controller_backends.uci import (
UciBackend, get_option_named, parse_bool, UciException, store_bool,
get_option_anonymous,
get_option_anonymous, get_sections_by_type
)
from foris_controller.utils import RWLock
from foris_controller_backends.services import OpenwrtServices
logger = logging.getLogger(__name__)
subordinate_dir_lock = RWLock(app_info["lock_backend"])
class CaGenAsync(AsyncCommand):
def generate_ca(self, notify_function, exit_notify_function, reset_notify_function):
......@@ -223,6 +229,77 @@ class RemoteUci(object):
return result
def list_subordinates(self):
with UciBackend() as backend:
fosquitto_data = backend.read("fosquitto")
res = []
# custom names map
name_map = {
e["name"]: e["data"].get("custom_name", "")
for e in get_sections_by_type(fosquitto_data, "fosquitto", "alias")
}
for item in get_sections_by_type(fosquitto_data, "fosquitto", "subordinate"):
if "id" not in item["data"]:
continue
controller_id = item["data"]["id"]
enabled = parse_bool(item["data"].get("enabled", "0"))
custom_name = name_map.get(controller_id, "")
res.append(
{"controller_id": controller_id, "enabled": enabled, "custom_name": custom_name}
)
return res
@staticmethod
def add_subordinate(controller_id: str, address: str, port: int):
with UciBackend() as backend:
new_section = backend.add_section("fosquitto", "subordinate")
backend.set_option("fosquitto", new_section, "id", controller_id)
backend.set_option("fosquitto", new_section, "enabled", store_bool(True))
backend.set_option("fosquitto", new_section, "address", address)
backend.set_option("fosquitto", new_section, "port", port)
def set_subordinate(self, controller_id: str, enabled: bool, custom_name: str) -> bool:
with UciBackend() as backend:
fosquitto_data = backend.read("fosquitto")
section = None
for item in get_sections_by_type(fosquitto_data, "fosquitto", "subordinate"):
if item["data"].get("id", None) == controller_id:
section = item["name"]
if not section:
return False
backend.set_option("fosquitto", section, "enabled", store_bool(enabled))
backend.add_section("fosquitto", "alias", controller_id)
backend.set_option("fosquitto", controller_id, "custom_name", custom_name)
with OpenwrtServices() as services:
services.reload("fosquitto")
return True
@staticmethod
def del_subordinate(controller_id: str) -> bool:
with UciBackend() as backend:
fosquitto_data = backend.read("fosquitto")
section = None
for item in get_sections_by_type(fosquitto_data, "fosquitto", "subordinate"):
if item["data"].get("id", None) == controller_id:
section = item["name"]
if not section:
return False
backend.del_section("fosquitto", section)
try:
backend.del_section("fosquitto", controller_id)
except UciException:
pass
with OpenwrtServices() as services:
services.reload("fosquitto")
return True
class RemoteFiles(BaseFile):
BASE_CERT_PATH = "/etc/ssl/ca/remote"
......@@ -324,3 +401,68 @@ class RemoteFiles(BaseFile):
fake_file.close()
return base64.b64encode(final_content).decode()
@staticmethod
def extract_token_subordinate(token: str) -> typing.Tuple[dict, dict]:
token_data = BytesIO(base64.b64decode(token))
with tarfile.open(fileobj=token_data, mode="r:gz") as tar:
config_file = [e.name for e in tar.getmembers() if e.name.endswith(".json")][0]
with tar.extractfile(config_file) as f:
conf = json.load(f)
file_data = {}
for member in tar.getmembers():
with tar.extractfile(member.name) as f:
file_data[os.path.basename(member.name)] = f.read()
return conf, file_data
@staticmethod
def store_subordinate_files(controller_id: str, file_data: dict):
path_root = pathlib.Path("/etc/fosquitto/bridges") / controller_id
makedirs(str(path_root), 0o0777)
for name, content in file_data.items():
new_file = pathlib.Path(inject_file_root(str(path_root / name)))
new_file.touch(0o0600)
with new_file.open("wb") as f:
f.write(content)
f.flush()
@staticmethod
def remove_subordinate(controller_id: str):
path = pathlib.Path("/etc/fosquitto/bridges") / controller_id
shutil.rmtree(inject_file_root(str(path)), True)
class RemoteComplex:
def add_subordinate(self, token):
if not app_info["bus"] == "mqtt":
return {"result": False}
conf, file_data = RemoteFiles.extract_token_subordinate(token)
with subordinate_dir_lock.writelock:
forbidden_controller_ids = [app_info["controller_id"]] + [
e["controller_id"] for e in RemoteUci().list_subordinates()
] # my controller_id + already stored controller ids
if conf["device_id"] in forbidden_controller_ids:
return {"result": False}
RemoteFiles.store_subordinate_files(conf["device_id"], file_data)
RemoteUci.add_subordinate(conf["device_id"], conf["ipv4_ips"][0], conf["port"])
with OpenwrtServices() as services:
services.reload("fosquitto")
return {"result": True, "controller_id": conf["device_id"]}
def del_subordinate(self, controller_id):
with subordinate_dir_lock.writelock:
if not RemoteUci.del_subordinate(controller_id):
return False
RemoteFiles.remove_subordinate(controller_id)
with OpenwrtServices() as services:
services.reload("fosquitto")
return True
......@@ -89,8 +89,6 @@ def get_sections_by_type(data, config, section_type):
e for e in data[config]
if e["type"] == section_type
]
if not res:
raise UciRecordNotFound(config, section_type=section_type)
return res
......
......@@ -77,6 +77,30 @@ class RemoteModule(BaseModule):
def action_get_token(self, data):
return self.handler.get_token(**data)
def action_list_subordinates(self, data):
return {"subordinates": self.handler.list_subordinates()}
def action_add_subordinate(self, data):
res = self.handler.add_subordinate(**data)
if res["result"]:
self.notify(
"add_subordinate",
{"controller_id": res["controller_id"]}
)
return res
def action_del_subordinate(self, data):
res = self.handler.del_subordinate(**data)
if res:
self.notify("del_subordinate", data)
return {"result": res}
def action_set_subordinate(self, data):
res = self.handler.set_subordinate(**data)
if res:
self.notify("set_subordinate", data)
return {"result": res}
@wrap_required_functions([
'generate_ca',
......@@ -87,6 +111,10 @@ class RemoteModule(BaseModule):
'get_settings',
'update_settings',
'get_token',
'list_subordinates',
'add_subordinate',
'del_subordinate',
'set_subordinate',
])
class Handler(object):
pass
......@@ -17,9 +17,14 @@
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
import json
import logging
import random
import base64
import tarfile
import typing
from io import BytesIO
from foris_controller.app import app_info
from foris_controller.handler_base import BaseMockHandler
......@@ -32,13 +37,14 @@ logger = logging.getLogger(__name__)
class MockRemoteHandler(Handler, BaseMockHandler):
ca_generated = False
tokens = []
tokens: typing.List[dict] = []
current_id = 2
settings = {
"enabled": False,
"wan_access": False,
"port": 11884,
}
subordinates: typing.List[dict] = []
@logger_wrapper(logger)
def generate_ca(self, notify, exit_notify, reset_notify):
......@@ -105,3 +111,56 @@ class MockRemoteHandler(Handler, BaseMockHandler):
if filtered[0]["status"] == "revoked":
return {"status": "revoked"}
return {"status": "valid", "token": base64.b64encode(b'some data').decode()}
@logger_wrapper(logger)
def list_subordinates(self):
if app_info["bus"] != "mqtt":
return []
return MockRemoteHandler.subordinates
@logger_wrapper(logger)
def add_subordinate(self, token) -> dict:
if app_info["bus"] != "mqtt":
return {"result": False}
token_data = BytesIO(base64.b64decode(token))
with tarfile.open(fileobj=token_data, mode="r:gz") as tar:
config_name = [e for e in tar.getmembers() if e.name.endswith("conf.json")][0]
with tar.extractfile(config_name) as f:
controller_id = json.load(f)["device_id"]
for record in MockRemoteHandler.subordinates:
if record["controller_id"] == controller_id:
return {"result": False} # already present
MockRemoteHandler.subordinates.append({
"controller_id": controller_id,
"enabled": True,
"custom_name": "",
})
return {"result": True, "controller_id": controller_id}
@logger_wrapper(logger)
def del_subordinate(self, controller_id) -> bool:
if app_info["bus"] != "mqtt":
return False
mapped = {e["controller_id"]: e for e in MockRemoteHandler.subordinates}
if controller_id not in mapped:
return False
del mapped[controller_id]
MockRemoteHandler.subordinates = list(mapped.items())
return True
@logger_wrapper(logger)
def set_subordinate(self, controller_id, enabled, custom_name) -> bool:
if app_info["bus"] != "mqtt":
return False
mapped = {e["controller_id"]: e for e in MockRemoteHandler.subordinates}
if controller_id not in mapped:
return False
mapped[controller_id]["enabled"] = enabled
mapped[controller_id]["custom_name"] = custom_name
return True
......@@ -22,7 +22,9 @@ import logging
from foris_controller.handler_base import BaseOpenwrtHandler
from foris_controller.utils import logger_wrapper
from foris_controller_backends.remote import CaGenAsync, CaGenCmds, RemoteUci, RemoteFiles
from foris_controller_backends.remote import (
CaGenAsync, CaGenCmds, RemoteUci, RemoteFiles, RemoteComplex
)
from .. import Handler
......@@ -35,6 +37,7 @@ class OpenwrtRemoteHandler(Handler, BaseOpenwrtHandler):
cmds = CaGenCmds()
uci = RemoteUci()
files = RemoteFiles()
complex = RemoteComplex()
@logger_wrapper(logger)
def generate_ca(self, notify, exit_notify, reset_notify):
......@@ -76,3 +79,19 @@ class OpenwrtRemoteHandler(Handler, BaseOpenwrtHandler):
"status": "valid",
"token": self.files.get_token(id=id, name=filtered[0]["name"])
}
@logger_wrapper(logger)
def list_subordinates(self):
return OpenwrtRemoteHandler.uci.list_subordinates()
@logger_wrapper(logger)
def add_subordinate(self, token):
return OpenwrtRemoteHandler.complex.add_subordinate(token)
@logger_wrapper(logger)
def del_subordinate(self, controller_id):
return OpenwrtRemoteHandler.complex.del_subordinate(controller_id)
@logger_wrapper(logger)
def set_subordinate(self, controller_id, enabled, custom_name):
return OpenwrtRemoteHandler.uci.set_subordinate(controller_id, enabled, custom_name)
......@@ -2,7 +2,17 @@
"definitions": {
"cert_id": {"type": "string", "pattern": "^([0-9a-fA-F][0-9a-fA-F])+$"},
"cert_name": {"type": "string", "pattern": "^[a-zA-Z0-9_.-]{1,64}$"},
"controller_id": {"type": "string", "pattern": "^[a-zA-Z0-9]{16}$"}
"controller_id": {"type": "string", "pattern": "^[a-zA-Z0-9]{16}$"},
"subordinate": {
"type": "object",
"properties": {
"controller_id": {"$ref": "#/definitions/controller_id"},
"enabled": {"type": "boolean"},
"custom_name": {"type": "string"}
},
"additionalProperties": false,
"required": ["controller_id", "enabled", "custom_name"]
}
},
"oneOf": [
{
......@@ -422,6 +432,214 @@
},
"additionalProperties": false,
"required": ["data"]
},
{
"description": "Request to obtain a list of subordinates",
"properties": {
"module": {"enum": ["remote"]},
"kind": {"enum": ["request"]},
"action": {"enum": ["list_subordinates"]}
},
"additionalProperties": false
},
{
"description": "Reply to obtain a list subordinates",
"properties": {
"module": {"enum": ["remote"]},
"kind": {"enum": ["reply"]},
"action": {"enum": ["list_subordinates"]},
"data": {
"type": "object",
"properties": {
"subordinates": {
"items": {"$ref": "#/definitions/subordinate"}
}
},
"additionalProperties": false,
"required": ["subordinates"]
}
},
"additionalProperties": false,
"required": ["data"]
},
{
"description": "Request to add a subordinate",
"properties": {
"module": {"enum": ["remote"]},
"kind": {"enum": ["request"]},
"action": {"enum": ["add_subordinate"]},
"data": {
"type": "object",
"properties": {
"token": {"type": "string"}
},
"additionalProperties": false,
"required": ["token"]
}
},
"additionalProperties": false,
"required": ["data"]
},
{
"description": "Reply to add a subordinate",
"properties": {
"module": {"enum": ["remote"]},
"kind": {"enum": ["reply"]},
"action": {"enum": ["add_subordinate"]},
"data": {
"oneOf": [
{
"type": "object",
"properties": {
"result": {"enum": [true]},
"controller_id": {"$ref": "#/definitions/controller_id"}
},
"additionalProperties": false,
"required": ["result", "controller_id"]
},
{
"type": "object",
"properties": {
"result": {"enum": [false]}
},
"additionalProperties": false,
"required": ["result"]
}
]
}
},
"additionalProperties": false,
"required": ["data"]
},
{
"description": "Notification for adding a subordinate",
"properties": {
"module": {"enum": ["remote"]},
"kind": {"enum": ["notification"]},
"action": {"enum": ["add_subordinate"]},
"data": {
"type": "object",
"properties": {
"controller_id": {"$ref": "#/definitions/controller_id"}
},
"additionalProperties": false
}
},
"additionalProperties": false,
"required": ["data"]
},
{
"description": "Request to remove a subordinate",
"properties": {
"module": {"enum": ["remote"]},
"kind": {"enum": ["request"]},
"action": {"enum": ["del_subordinate"]},
"data": {
"type": "object",
"properties": {
"controller_id": {"$ref": "#/definitions/controller_id"}
},
"additionalProperties": false,
"required": ["controller_id"]
}
},
"additionalProperties": false,
"required": ["data"]
},
{
"description": "Notification that a subordinate was removed",
"properties": {
"module": {"enum": ["remote"]},
"kind": {"enum": ["notification"]},
"action": {"enum": ["del_subordinate"]},
"data": {
"type": "object",
"properties": {
"controller_id": {"$ref": "#/definitions/controller_id"}
},
"additionalProperties": false,
"required": ["controller_id"]
}
},
"additionalProperties": false,
"required": ["data"]
},
{
"description": "Reply to remove a subordinate",
"properties": {
"module": {"enum": ["remote"]},
"kind": {"enum": ["reply"]},
"action": {"enum": ["del_subordinate"]},
"data": {
"type": "object",
"properties": {
"result": {"type": "boolean"}
},
"additionalProperties": false,
"required": ["result"]
}
},
"additionalProperties": false,
"required": ["data"]
},
{
"description": "Request to update subordinate",
"properties": {
"module": {"enum": ["remote"]},
"kind": {"enum": ["request"]},
"action": {"enum": ["set_subordinate"]},
"data": {
"type": "object",
"properties": {
"controller_id": {"$ref": "#/definitions/controller_id"},
"enabled": {"type": "boolean"},
"custom_name": {"type": "string"}
},
"additionalProperties": false,
"required": ["controller_id", "enabled", "custom_name"]
}
},
"additionalProperties": false,
"required": ["data"]
},
{
"description": "Reply to update subordinate",
"properties": {
"module": {"enum": ["remote"]},
"kind": {"enum": ["reply"]},
"action": {"enum": ["set_subordinate"]},
"data": {
"type": "object",
"properties": {
"result": {"type": "boolean"}
},
"additionalProperties": false,
"required": ["result"]
}
},
"additionalProperties": false,
"required": ["data"]
},
{
"description": "Notification that a subordinate was updated",
"properties": {
"module": {"enum": ["remote"]},
"kind": {"enum": ["notification"]},
"action": {"enum": ["set_subordinate"]},
"data": {
"type": "object",
"properties": {
"controller_id": {"$ref": "#/definitions/controller_id"},
"enabled": {"type": "boolean"},
"custom_name": {"type": "string"}
},
"additionalProperties": false,
"required": ["controller_id", "enabled", "custom_name"]
}
},
"additionalProperties": false,
"required": ["data"]
}
]
}
This diff is collapsed.
......@@ -421,8 +421,7 @@ def test_parse_read_data(uci_configs_init, lock_backend):
with pytest.raises(UciRecordNotFound):
uci.get_option_named(res2, 'test2', 'named2', 'non_existing')
assert "def1" == uci.get_option_named(res2, 'test2', 'named2', 'non_existing', default="def1")
with pytest.raises(UciRecordNotFound):
uci.get_sections_by_type(res2, 'test2', 'non_existing')
assert uci.get_sections_by_type(res2, 'test2', 'non_existing') == []
with pytest.raises(UciRecordNotFound):
uci.get_section_idx(res2, 'test2', 'anonymous', 99)
with pytest.raises(UciRecordNotFound):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment