Verified Commit 2101a7e3 authored by Štěpán Henek's avatar Štěpán Henek 🌩

fixup! WIP: remote: adding subordinates

parent 05eea248
Pipeline #45290 failed with stage
in 6 minutes and 20 seconds
......@@ -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"], "custom_name": data["custom_name"]}
)
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, custom_name, 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,
"custom_name": custom_name,
"enabled": True,
})
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
......@@ -76,3 +76,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):
raise NotImplementedError()
@logger_wrapper(logger)
def add_subordinate(self, custom_name, token):
raise NotImplementedError()
@logger_wrapper(logger)
def del_subordinate(self, controller_id):
raise NotImplementedError()
@logger_wrapper(logger)
def set_subordinate(self, controller_id, enabled, custom_name):
raise NotImplementedError()
......@@ -3,16 +3,15 @@
"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}$"},
"subordinate_state": {"enum":["online", "offline", "unknown"]},
"subordinate": {
"type": "object",
"properties": {
"controller_id": {"$ref": "#/definitions/controller_id"},
"enabled": {"type": "boolean"},
"state": {"$ref": "#/definitions/subordinate_state"}
"custom_name": {"type": "string"}
},
"additionalProperties": false,
"required": ["controller_id", "enabled", "state"]
"required": ["controller_id", "enabled", "custom_name"]
}
},
"oneOf": [
......@@ -464,7 +463,26 @@
"required": ["data"]
},
{
"description": "Request to add subordinate",
"description": "Request to add a subordinate",
"properties": {
"module": {"enum": ["remote"]},
"kind": {"enum": ["request"]},
"action": {"enum": ["add_subordinate"]},
"data": {
"type": "object",
"properties": {
"custom_name": {"type": "string"},
"token": {"type": "string"}
},
"additionalProperties": false,
"required": ["custom_name", "token"]
}
},
"additionalProperties": false,
"required": ["data"]
},
{
"description": "Reply to add a subordinate",
"properties": {
"module": {"enum": ["remote"]},
"kind": {"enum": ["reply"]},
......@@ -474,34 +492,88 @@
{
"type": "object",
"properties": {
"custom_name": {"type": "string"},
"token": {"type": "string"}
"result": {"enum": [true]},
"controller_id": {"$ref": "#/definitions/controller_id"}
},
"additionalProperties": false,
"required": ["custom_name", "token"]
"required": ["result", "controller_id"]
},
{
"type": "object",
"properties": {
"custom_name": {"type": "string"},
"new_controller_id": {"$ref": "#/definitions/controller_id"},
"via": {"$ref": "#/definitions/controller_id"}
"result": {"enum": [false]}
},
"additionalProperties": false,
"required": ["custom_name", "new_controller_id", "via"]
"required": ["result"]
}
]
}
},
"additionalProperties": false,
"required": ["data"]
},
{
"description": "Reply to add subordinate",
"description": "Notification for adding a subordinate",
"properties": {
"module": {"enum": ["remote"]},
"kind": {"enum": ["reply"]},
"kind": {"enum": ["notification"]},
"action": {"enum": ["add_subordinate"]},
"data": {
"type": "object",
"properties": {
"custom_name": {"type": "string"},
"controller_id": {"$ref": "#/definitions/controller_id"}
},
"additionalProperties": false,
"required": ["custom_name"]
}
},
"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": {
......@@ -513,6 +585,64 @@
},
"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"]
}
]
}
......@@ -38,6 +38,39 @@ from foris_controller_testtools.utils import (
CERT_PATH = "/tmp/test-cagen/"
def prepare_subordinate_token(controller_id):
def add_to_tar(tar, name, content):
data = content.encode()
fake_file = BytesIO(data)
info = tarfile.TarInfo(name=name)
info.size = len(data)
info.mode = 0o0600
fake_file.seek(0)
tar.addfile(info, fake_file)
fake_file.close()
new_file = BytesIO()
with tarfile.open(fileobj=new_file, mode="w:gz") as tar:
add_to_tar(tar, f"some_name/token.crt", "token cert content")
add_to_tar(tar, f"some_name/token.key", "token key content")
add_to_tar(tar, f"some_name/ca.crt", "ca cert content")
add_to_tar(tar, f"some_name/conf.json", json.dumps({
"name": "some_name",
"hostname": "localhost",
"ipv4_ips": [],
"dhcp_names": [],
"port": 11884,
"device_id": controller_id,
}))
new_file.seek(0)
final_content = new_file.read()
new_file.close()
return base64.b64encode(final_content).decode()
@pytest.fixture(scope="function")
def empty_certs():
try:
......@@ -907,3 +940,222 @@ def test_enable_empty(
}
})
assert not res["data"]["result"]
@pytest.mark.only_message_buses(['unix-socket', 'ubus'])
def test_complex_subordinates_unsupported(uci_configs_init, infrastructure, start_buses, file_root_init):
res = infrastructure.process_message({
"module": "remote",
"action": "list_subordinates",
"kind": "request",
})
assert res == {
"module": "remote",
"action": "list_subordinates",
"kind": "reply",
"data": {"subordinates": []}
}
res = infrastructure.process_message({
"module": "remote",
"action": "add_subordinate",
"kind": "request",
"data": {
"custom_name": "test_add1",
"token": prepare_subordinate_token("1122334455667788"),
}
})
assert res == {
"module": "remote",
"action": "add_subordinate",
"kind": "reply",
"data": {"result": False}
}
res = infrastructure.process_message({
"module": "remote",
"action": "del_subordinate",
"kind": "request",
"data": {
"controller_id": "1122334455667788",
}
})
assert res == {
"module": "remote",
"action": "del_subordinate",
"kind": "reply",
"data": {"result": False}
}
res = infrastructure.process_message({
"module": "remote",
"action": "set_subordinate",
"kind": "request",
"data": {
"controller_id": "1122334455667788", "custom_name": "set_xx", "enabled": False,
}
})
assert res == {
"module": "remote",
"action": "set_subordinate",
"kind": "reply",
"data": {"result": False}
}
@pytest.mark.only_message_buses(['mqtt'])
def test_complex_subordinates(uci_configs_init, infrastructure, start_buses, file_root_init):
def in_list(controller_id):
res = infrastructure.process_message({
"module": "remote",
"action": "list_subordinates",
"kind": "request",
})
assert "subordinates" in res["data"]
output = None
for record in res["data"]["subordinates"]:
assert set(record.keys()) == {"custom_name", "controller_id", "enabled"}
if record["controller_id"] == controller_id:
output = record
return output
assert None is in_list("1122334455667788")
token = prepare_subordinate_token("1122334455667788")
filters = [("remote", "add_subordinate")]
notifications = infrastructure.get_notifications(filters=filters)
# add
res = infrastructure.process_message({
"module": "remote",
"action": "add_subordinate",
"kind": "request",
"data": {
"custom_name": "test_add1",
"token": token,
}
})
assert res == {
"module": "remote",
"action": "add_subordinate",
"kind": "reply",
"data": {"result": True, "controller_id": "1122334455667788"}
}
notifications = infrastructure.get_notifications(notifications, filters=filters)
assert notifications[-1] == {
"module": "remote",
"action": "add_subordinate",
"kind": "notification",
"data": {"custom_name": "test_add1", "controller_id": "1122334455667788"}
}
assert in_list("1122334455667788") == {
"controller_id": "1122334455667788", "enabled": True, "custom_name": "test_add1"
}
res = infrastructure.process_message({
"module": "remote",
"action": "add_subordinate",
"kind": "request",
"data": {
"custom_name": "test_add2",
"token": token,
}
})
assert res == {
"module": "remote",
"action": "add_subordinate",
"kind": "reply",
"data": {"result": False}
}
assert in_list("1122334455667788") == {
"controller_id": "1122334455667788", "enabled": True, "custom_name": "test_add1"
}
# set
filters = [("remote", "set_subordinate")]
notifications = infrastructure.get_notifications(filters=filters)
res = infrastructure.process_message({
"module": "remote",
"action": "set_subordinate",
"kind": "request",
"data": {
"controller_id": "1122334455667788", "custom_name": "test_set1", "enabled": False,
}
})
assert res == {
"module": "remote",
"action": "set_subordinate",
"kind": "reply",
"data": {"result": True}
}
notifications = infrastructure.get_notifications(notifications, filters=filters)
assert notifications[-1] == {
"module": "remote",
"action": "set_subordinate",
"kind": "notification",
"data": {"controller_id": "1122334455667788", "custom_name": "test_set1", "enabled": False}
}
assert in_list("1122334455667788") == {
"controller_id": "1122334455667788", "custom_name": "test_set1", "enabled": False
}
res = infrastructure.process_message({
"module": "remote",
"action": "set_subordinate",
"kind": "request",
"data": {
"controller_id": "2222334455667788", "custom_name": "test_set2", "enabled": True,
}
})
assert res == {
"module": "remote",
"action": "set_subordinate",
"kind": "reply",
"data": {"result": False}
}
# del
filters = [("remote", "del_subordinate")]
notifications = infrastructure.get_notifications(filters=filters)
res = infrastructure.process_message({
"module": "remote",
"action": "del_subordinate",
"kind": "request",
"data": {
"controller_id": "1122334455667788",
}
})
assert res == {
"module": "remote",
"action": "del_subordinate",
"kind": "reply",
"data": {"result": True}
}
notifications = infrastructure.get_notifications(notifications, filters=filters)
assert notifications[-1] == {
"module": "remote",
"action": "del_subordinate",
"kind": "notification",
"data": {"controller_id": "1122334455667788"}
}
assert None is in_list("1122334455667788")
res = infrastructure.process_message({
"module": "remote",
"action": "del_subordinate",
"kind": "request",
"data": {
"controller_id": "1122334455667788",
}
})
assert res == {
"module": "remote",
"action": "del_subordinate",
"kind": "reply",
"data": {"result": False}
}
assert None is in_list("1122334455667788")
@pytest.mark.skip("TODO")
@pytest.mark.only_backends(['openwrt'])
@pytest.mark.only_message_buses(['mqtt'])
def test_complex_subordinates_openwrt(uci_configs_init, infrastructure, start_buses, file_root_init):
raise NotImplementedError() # check fosquitto restart
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