diff --git a/README.md b/README.md index 89d5105..d506cd8 100644 --- a/README.md +++ b/README.md @@ -73,12 +73,11 @@ sudo echo "experimental-features = nix-command flakes" > '/etc/nix/nix.conf' - To start the backend server, execute: ```bash - clan webui --reload --no-open --log-level debug --populate + clan webui --reload --no-open --log-level debug --populate --emulate ``` - The server will automatically restart if any Python files change. - The `--populate` flag will automatically populate the database with dummy data - - - To emulate some distributed system behavior run `python3 tests/emulate_fastapi.py` + - The `--emulate` flag will automatically run servers the database with dummy data for the fronted to communicate with (ap, dlg, c1 and c2) 8. **Build the Frontend**: diff --git a/pkgs/clan-cli/clan_cli/webui/__init__.py b/pkgs/clan-cli/clan_cli/webui/__init__.py index 1305e78..3848f9d 100644 --- a/pkgs/clan-cli/clan_cli/webui/__init__.py +++ b/pkgs/clan-cli/clan_cli/webui/__init__.py @@ -28,6 +28,12 @@ def register_parser(parser: argparse.ArgumentParser) -> None: help="Populate the database with dummy data", default=False, ) + parser.add_argument( + "--emulate", + action="store_true", + help="Emulate two entities c1 and c2 + dlg and ap", + default=False, + ) parser.add_argument( "--no-open", action="store_true", help="Don't open the browser", default=False ) diff --git a/pkgs/clan-cli/clan_cli/webui/schemas.py b/pkgs/clan-cli/clan_cli/webui/schemas.py index b3175f0..ea2e3e3 100644 --- a/pkgs/clan-cli/clan_cli/webui/schemas.py +++ b/pkgs/clan-cli/clan_cli/webui/schemas.py @@ -11,6 +11,12 @@ class Status(Enum): UNKNOWN = "unknown" +class Roles(Enum): + PROSUMER = "service_prosumer" + AP = "AP" + DLG = "DLG" + + class Machine(BaseModel): name: str status: Status @@ -25,6 +31,10 @@ class EntityBase(BaseModel): did: str = Field(..., example="did:sov:test:1234") name: str = Field(..., example="C1") ip: str = Field(..., example="127.0.0.1") + network: str = Field(..., example="255.255.0.0") + role: Roles = Field( + ..., example=Roles("service_prosumer") + ) # roles are needed for UI to show the correct view visible: bool = Field(..., example=True) other: dict = Field( ..., diff --git a/pkgs/clan-cli/clan_cli/webui/server.py b/pkgs/clan-cli/clan_cli/webui/server.py index b1ad401..7cd9165 100644 --- a/pkgs/clan-cli/clan_cli/webui/server.py +++ b/pkgs/clan-cli/clan_cli/webui/server.py @@ -126,6 +126,35 @@ def start_server(args: argparse.Namespace) -> None: cmd = ["pytest", "-s", str(test_db_api)] subprocess.run(cmd, check=True) + if args.emulate: + import multiprocessing as mp + + from config import host, port_ap, port_client_base, port_dlg + from emulate_fastapi import app_ap, app_c1, app_c2, app_dlg, get_health + + app_ports = [ + (app_dlg, port_dlg), + (app_ap, port_ap), + (app_c1, port_client_base), + (app_c2, port_client_base + 1), + ] + urls = list() + # start servers as processes (dlg, ap, c1 and c2 for tests) + for app, port in app_ports: + proc = mp.Process( + target=uvicorn.run, + args=(app,), + kwargs={"host": host, "port": port, "log_level": "info"}, + daemon=True, + ) + proc.start() + urls.append(f"http://{host}:{port}") + # check server health + for url in urls: + res = get_health(url=url + "/health") + if res is None: + raise Exception(f"Couldn't reach {url} after starting server") + uvicorn.run( "clan_cli.webui.app:app", host=args.host, diff --git a/pkgs/clan-cli/clan_cli/webui/sql_models.py b/pkgs/clan-cli/clan_cli/webui/sql_models.py index 742b237..d7c4f2a 100644 --- a/pkgs/clan-cli/clan_cli/webui/sql_models.py +++ b/pkgs/clan-cli/clan_cli/webui/sql_models.py @@ -1,6 +1,7 @@ -from sqlalchemy import JSON, Boolean, Column, ForeignKey, Integer, String, Text +from sqlalchemy import JSON, Boolean, Column, Enum, ForeignKey, Integer, String, Text from sqlalchemy.orm import relationship +from .schemas import Roles from .sql_db import Base # Relationsship example @@ -14,12 +15,15 @@ class Entity(Base): did = Column(String, primary_key=True, index=True) name = Column(String, index=True, unique=True) ip = Column(String, index=True) + network = Column(String, index=True) + role = Column(Enum(Roles), index=True, nullable=False) # type: ignore + # role = Column(String, index=True, nullable=False) attached = Column(Boolean, index=True) visible = Column(Boolean, index=True) stop_health_task = Column(Boolean) ## Non queryable body ## - # In here we deposit: Network, Roles, Visible, etc. + # In here we deposit: Not yet defined stuff other = Column(JSON) ## Relations ## diff --git a/pkgs/clan-cli/config.py b/pkgs/clan-cli/config.py new file mode 100644 index 0000000..2874a7d --- /dev/null +++ b/pkgs/clan-cli/config.py @@ -0,0 +1,4 @@ +host = "127.0.0.1" +port_dlg = 6000 +port_ap = 6600 +port_client_base = 7000 diff --git a/pkgs/clan-cli/tests/emulate_fastapi.py b/pkgs/clan-cli/emulate_fastapi.py similarity index 64% rename from pkgs/clan-cli/tests/emulate_fastapi.py rename to pkgs/clan-cli/emulate_fastapi.py index 1033a81..d663c9a 100644 --- a/pkgs/clan-cli/tests/emulate_fastapi.py +++ b/pkgs/clan-cli/emulate_fastapi.py @@ -1,21 +1,60 @@ +import sys import time +import urllib -import uvicorn from fastapi import FastAPI from fastapi.responses import HTMLResponse -app = FastAPI() +app_dlg = FastAPI() +app_ap = FastAPI() +app_c1 = FastAPI() +app_c2 = FastAPI() -# bash tests: curl localhost:8000/ap_list_of_services +# bash tests: curl localhost:6600/ap_list_of_services +# curl localhost:7001/consume_service_from_other_entity -@app.get("/health") -async def healthcheck() -> str: +#### HEALTH + + +@app_c1.get("/health") +async def healthcheck_c1() -> str: return "200 OK" -@app.get("/consume_service_from_other_entity", response_class=HTMLResponse) -async def consume_service_from_other_entity() -> HTMLResponse: +@app_c2.get("/health") +async def healthcheck_c2() -> str: + return "200 OK" + + +@app_dlg.get("/health") +async def healthcheck_dlg() -> str: + return "200 OK" + + +@app_ap.get("/health") +async def healthcheck_ap() -> str: + return "200 OK" + + +def get_health(*, url: str, max_retries: int = 20, delay: float = 0.2) -> str | None: + for attempt in range(max_retries): + try: + with urllib.request.urlopen(url) as response: + return response.read() + except urllib.error.URLError as e: + print(f"Attempt {attempt + 1} failed: {e.reason}", file=sys.stderr) + time.sleep(delay) + return None + + +#### CONSUME SERVICE + +# TODO send_msg??? + + +@app_c1.get("/consume_service_from_other_entity", response_class=HTMLResponse) +async def consume_service_from_other_entity_c1() -> HTMLResponse: html_content = """
@@ -27,7 +66,23 @@ async def consume_service_from_other_entity() -> HTMLResponse: return HTMLResponse(content=html_content, status_code=200) -@app.get("/ap_list_of_services", response_class=HTMLResponse) +@app_c2.get("/consume_service_from_other_entity", response_class=HTMLResponse) +async def consume_service_from_other_entity_c2() -> HTMLResponse: + html_content = """ + + + + + + """ + time.sleep(3) + return HTMLResponse(content=html_content, status_code=200) + + +#### ap_list_of_services + + +@app_ap.get("/ap_list_of_services", response_class=HTMLResponse) async def ap_list_of_services() -> HTMLResponse: html_content = b"""HTTP/1.1 200 OK\r\n\r\n[[ { @@ -114,7 +169,7 @@ async def ap_list_of_services() -> HTMLResponse: return HTMLResponse(content=html_content, status_code=200) -@app.get("/dlg_list_of_did_resolutions", response_class=HTMLResponse) +@app_dlg.get("/dlg_list_of_did_resolutions", response_class=HTMLResponse) async def dlg_list_of_did_resolutions() -> HTMLResponse: html_content = b"""HTTP/1.1 200 OK\r\n\r\n [ @@ -148,6 +203,3 @@ async def dlg_list_of_did_resolutions() -> HTMLResponse: } ]""" return HTMLResponse(content=html_content, status_code=200) - - -uvicorn.run(app, host="localhost", port=8000) diff --git a/pkgs/clan-cli/tests/api.py b/pkgs/clan-cli/tests/api.py index 1c3644d..101f3e3 100644 --- a/pkgs/clan-cli/tests/api.py +++ b/pkgs/clan-cli/tests/api.py @@ -11,6 +11,7 @@ from fastapi.testclient import TestClient from openapi_client import ApiClient, Configuration from ports import PortFunction +import config from clan_cli.webui.app import app @@ -31,10 +32,11 @@ def get_health(*, url: str, max_retries: int = 20, delay: float = 0.2) -> str | # Pytest fixture to run the server in a separate process +# server @pytest.fixture(scope="session") def server_url(unused_tcp_port: PortFunction) -> Generator[str, None, None]: port = unused_tcp_port() - host = "127.0.0.1" + host = config.host proc = Process( target=uvicorn.run, args=(app,), diff --git a/pkgs/clan-cli/tests/openapi_client/__init__.py b/pkgs/clan-cli/tests/openapi_client/__init__.py index c30337b..b3656f9 100644 --- a/pkgs/clan-cli/tests/openapi_client/__init__.py +++ b/pkgs/clan-cli/tests/openapi_client/__init__.py @@ -43,6 +43,7 @@ from openapi_client.models.eventmessage_create import EventmessageCreate from openapi_client.models.http_validation_error import HTTPValidationError from openapi_client.models.machine import Machine from openapi_client.models.resolution import Resolution +from openapi_client.models.roles import Roles from openapi_client.models.service import Service from openapi_client.models.service_create import ServiceCreate from openapi_client.models.status import Status diff --git a/pkgs/clan-cli/tests/openapi_client/docs/Entity.md b/pkgs/clan-cli/tests/openapi_client/docs/Entity.md index fa7efb2..807f6a6 100644 --- a/pkgs/clan-cli/tests/openapi_client/docs/Entity.md +++ b/pkgs/clan-cli/tests/openapi_client/docs/Entity.md @@ -2,15 +2,17 @@ ## Properties -| Name | Type | Description | Notes | -| -------------------- | ---------- | ----------- | ----- | -| **did** | **str** | | -| **name** | **str** | | -| **ip** | **str** | | -| **visible** | **bool** | | -| **other** | **object** | | -| **attached** | **bool** | | -| **stop_health_task** | **bool** | | +| Name | Type | Description | Notes | +| -------------------- | --------------------- | ----------- | ----- | +| **did** | **str** | | +| **name** | **str** | | +| **ip** | **str** | | +| **network** | **str** | | +| **role** | [**Roles**](Roles.md) | | +| **visible** | **bool** | | +| **other** | **object** | | +| **attached** | **bool** | | +| **stop_health_task** | **bool** | | ## Example diff --git a/pkgs/clan-cli/tests/openapi_client/docs/EntityCreate.md b/pkgs/clan-cli/tests/openapi_client/docs/EntityCreate.md index 4fff16a..ea30366 100644 --- a/pkgs/clan-cli/tests/openapi_client/docs/EntityCreate.md +++ b/pkgs/clan-cli/tests/openapi_client/docs/EntityCreate.md @@ -2,13 +2,15 @@ ## Properties -| Name | Type | Description | Notes | -| ----------- | ---------- | ----------- | ----- | -| **did** | **str** | | -| **name** | **str** | | -| **ip** | **str** | | -| **visible** | **bool** | | -| **other** | **object** | | +| Name | Type | Description | Notes | +| ----------- | --------------------- | ----------- | ----- | +| **did** | **str** | | +| **name** | **str** | | +| **ip** | **str** | | +| **network** | **str** | | +| **role** | [**Roles**](Roles.md) | | +| **visible** | **bool** | | +| **other** | **object** | | ## Example diff --git a/pkgs/clan-cli/tests/openapi_client/docs/Roles.md b/pkgs/clan-cli/tests/openapi_client/docs/Roles.md new file mode 100644 index 0000000..994fa4c --- /dev/null +++ b/pkgs/clan-cli/tests/openapi_client/docs/Roles.md @@ -0,0 +1,10 @@ +# Roles + +An enumeration. + +## Properties + +| Name | Type | Description | Notes | +| ---- | ---- | ----------- | ----- | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/pkgs/clan-cli/tests/openapi_client/models/__init__.py b/pkgs/clan-cli/tests/openapi_client/models/__init__.py index 4460d22..f28cdd8 100644 --- a/pkgs/clan-cli/tests/openapi_client/models/__init__.py +++ b/pkgs/clan-cli/tests/openapi_client/models/__init__.py @@ -21,6 +21,7 @@ from openapi_client.models.eventmessage_create import EventmessageCreate from openapi_client.models.http_validation_error import HTTPValidationError from openapi_client.models.machine import Machine from openapi_client.models.resolution import Resolution +from openapi_client.models.roles import Roles from openapi_client.models.service import Service from openapi_client.models.service_create import ServiceCreate from openapi_client.models.status import Status diff --git a/pkgs/clan-cli/tests/openapi_client/models/entity.py b/pkgs/clan-cli/tests/openapi_client/models/entity.py index 0ff4d16..a78e778 100644 --- a/pkgs/clan-cli/tests/openapi_client/models/entity.py +++ b/pkgs/clan-cli/tests/openapi_client/models/entity.py @@ -20,6 +20,7 @@ import json from typing import Any, Dict from pydantic import BaseModel, Field, StrictBool, StrictStr +from openapi_client.models.roles import Roles class Entity(BaseModel): """ @@ -28,11 +29,13 @@ class Entity(BaseModel): did: StrictStr = Field(...) name: StrictStr = Field(...) ip: StrictStr = Field(...) + network: StrictStr = Field(...) + role: Roles = Field(...) visible: StrictBool = Field(...) other: Dict[str, Any] = Field(...) attached: StrictBool = Field(...) stop_health_task: StrictBool = Field(...) - __properties = ["did", "name", "ip", "visible", "other", "attached", "stop_health_task"] + __properties = ["did", "name", "ip", "network", "role", "visible", "other", "attached", "stop_health_task"] class Config: """Pydantic configuration""" @@ -73,6 +76,8 @@ class Entity(BaseModel): "did": obj.get("did"), "name": obj.get("name"), "ip": obj.get("ip"), + "network": obj.get("network"), + "role": obj.get("role"), "visible": obj.get("visible"), "other": obj.get("other"), "attached": obj.get("attached"), diff --git a/pkgs/clan-cli/tests/openapi_client/models/entity_create.py b/pkgs/clan-cli/tests/openapi_client/models/entity_create.py index f9df72b..d7385e1 100644 --- a/pkgs/clan-cli/tests/openapi_client/models/entity_create.py +++ b/pkgs/clan-cli/tests/openapi_client/models/entity_create.py @@ -20,6 +20,7 @@ import json from typing import Any, Dict from pydantic import BaseModel, Field, StrictBool, StrictStr +from openapi_client.models.roles import Roles class EntityCreate(BaseModel): """ @@ -28,9 +29,11 @@ class EntityCreate(BaseModel): did: StrictStr = Field(...) name: StrictStr = Field(...) ip: StrictStr = Field(...) + network: StrictStr = Field(...) + role: Roles = Field(...) visible: StrictBool = Field(...) other: Dict[str, Any] = Field(...) - __properties = ["did", "name", "ip", "visible", "other"] + __properties = ["did", "name", "ip", "network", "role", "visible", "other"] class Config: """Pydantic configuration""" @@ -71,6 +74,8 @@ class EntityCreate(BaseModel): "did": obj.get("did"), "name": obj.get("name"), "ip": obj.get("ip"), + "network": obj.get("network"), + "role": obj.get("role"), "visible": obj.get("visible"), "other": obj.get("other") }) diff --git a/pkgs/clan-cli/tests/openapi_client/models/roles.py b/pkgs/clan-cli/tests/openapi_client/models/roles.py new file mode 100644 index 0000000..189cbd3 --- /dev/null +++ b/pkgs/clan-cli/tests/openapi_client/models/roles.py @@ -0,0 +1,41 @@ +# coding: utf-8 + +""" + FastAPI + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 0.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import json +import pprint +import re # noqa: F401 +from aenum import Enum, no_arg + + + + + +class Roles(str, Enum): + """ + An enumeration. + """ + + """ + allowed enum values + """ + SERVICE_PROSUMER = 'service_prosumer' + AP = 'AP' + DLG = 'DLG' + + @classmethod + def from_json(cls, json_str: str) -> Roles: + """Create an instance of Roles from a JSON string""" + return Roles(json.loads(json_str)) + + diff --git a/pkgs/clan-cli/tests/openapi_client/test/test_eventmessage.py b/pkgs/clan-cli/tests/openapi_client/test/test_eventmessage.py deleted file mode 100644 index 21852d5..0000000 --- a/pkgs/clan-cli/tests/openapi_client/test/test_eventmessage.py +++ /dev/null @@ -1,67 +0,0 @@ -# coding: utf-8 - -""" - FastAPI - - No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - - The version of the OpenAPI document: 0.1.0 - Generated by OpenAPI Generator (https://openapi-generator.tech) - - Do not edit the class manually. -""" # noqa: E501 - - -import unittest -import datetime - -from openapi_client.models.eventmessage import Eventmessage # noqa: E501 - -class TestEventmessage(unittest.TestCase): - """Eventmessage unit test stubs""" - - def setUp(self): - pass - - def tearDown(self): - pass - - def make_instance(self, include_optional) -> Eventmessage: - """Test Eventmessage - include_option is a boolean, when False only required - params are included, when True both required and - optional params are included """ - # uncomment below to create an instance of `Eventmessage` - """ - model = Eventmessage() # noqa: E501 - if include_optional: - return Eventmessage( - id = 123456, - timestamp = 1234123413, - group = 1, - group_id = 12345, - msg_type = 1, - src_did = 'did:sov:test:2234', - des_did = 'did:sov:test:1234', - msg = {optinal=values} - ) - else: - return Eventmessage( - id = 123456, - timestamp = 1234123413, - group = 1, - group_id = 12345, - msg_type = 1, - src_did = 'did:sov:test:2234', - des_did = 'did:sov:test:1234', - msg = {optinal=values}, - ) - """ - - def testEventmessage(self): - """Test Eventmessage""" - # inst_req_only = self.make_instance(include_optional=False) - # inst_req_and_optional = self.make_instance(include_optional=True) - -if __name__ == '__main__': - unittest.main() diff --git a/pkgs/clan-cli/tests/openapi_client/test/test_eventmessage_create.py b/pkgs/clan-cli/tests/openapi_client/test/test_eventmessage_create.py deleted file mode 100644 index 8bc282d..0000000 --- a/pkgs/clan-cli/tests/openapi_client/test/test_eventmessage_create.py +++ /dev/null @@ -1,67 +0,0 @@ -# coding: utf-8 - -""" - FastAPI - - No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - - The version of the OpenAPI document: 0.1.0 - Generated by OpenAPI Generator (https://openapi-generator.tech) - - Do not edit the class manually. -""" # noqa: E501 - - -import unittest -import datetime - -from openapi_client.models.eventmessage_create import EventmessageCreate # noqa: E501 - -class TestEventmessageCreate(unittest.TestCase): - """EventmessageCreate unit test stubs""" - - def setUp(self): - pass - - def tearDown(self): - pass - - def make_instance(self, include_optional) -> EventmessageCreate: - """Test EventmessageCreate - include_option is a boolean, when False only required - params are included, when True both required and - optional params are included """ - # uncomment below to create an instance of `EventmessageCreate` - """ - model = EventmessageCreate() # noqa: E501 - if include_optional: - return EventmessageCreate( - id = 123456, - timestamp = 1234123413, - group = 1, - group_id = 12345, - msg_type = 1, - src_did = 'did:sov:test:2234', - des_did = 'did:sov:test:1234', - msg = {optinal=values} - ) - else: - return EventmessageCreate( - id = 123456, - timestamp = 1234123413, - group = 1, - group_id = 12345, - msg_type = 1, - src_did = 'did:sov:test:2234', - des_did = 'did:sov:test:1234', - msg = {optinal=values}, - ) - """ - - def testEventmessageCreate(self): - """Test EventmessageCreate""" - # inst_req_only = self.make_instance(include_optional=False) - # inst_req_and_optional = self.make_instance(include_optional=True) - -if __name__ == '__main__': - unittest.main() diff --git a/pkgs/clan-cli/tests/openapi_client/test/test_eventmessages_api.py b/pkgs/clan-cli/tests/openapi_client/test/test_eventmessages_api.py deleted file mode 100644 index e2841ab..0000000 --- a/pkgs/clan-cli/tests/openapi_client/test/test_eventmessages_api.py +++ /dev/null @@ -1,45 +0,0 @@ -# coding: utf-8 - -""" - FastAPI - - No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - - The version of the OpenAPI document: 0.1.0 - Generated by OpenAPI Generator (https://openapi-generator.tech) - - Do not edit the class manually. -""" # noqa: E501 - - -import unittest - -from openapi_client.api.eventmessages_api import EventmessagesApi # noqa: E501 - - -class TestEventmessagesApi(unittest.TestCase): - """EventmessagesApi unit test stubs""" - - def setUp(self) -> None: - self.api = EventmessagesApi() # noqa: E501 - - def tearDown(self) -> None: - pass - - def test_create_eventmessage(self) -> None: - """Test case for create_eventmessage - - Create Eventmessage # noqa: E501 - """ - pass - - def test_get_all_eventmessages(self) -> None: - """Test case for get_all_eventmessages - - Get All Eventmessages # noqa: E501 - """ - pass - - -if __name__ == '__main__': - unittest.main() diff --git a/pkgs/clan-cli/tests/test_db_api.py b/pkgs/clan-cli/tests/test_db_api.py index b619729..5cf4324 100644 --- a/pkgs/clan-cli/tests/test_db_api.py +++ b/pkgs/clan-cli/tests/test_db_api.py @@ -13,13 +13,21 @@ from openapi_client.models import ( Eventmessage, EventmessageCreate, Machine, + Roles, ServiceCreate, Status, ) +import config + random.seed(42) +host = config.host +port_dlg = config.port_dlg +port_ap = config.port_ap +port_client_base = config.port_client_base + num_uuids = 100 uuids = [str(uuid.UUID(int=random.getrandbits(128))) for i in range(num_uuids)] @@ -36,11 +44,33 @@ def create_entities(num: int = 10) -> list[EntityCreate]: en = EntityCreate( did=f"did:sov:test:12{i}", name=f"C{i}", - ip=f"127.0.0.1:{7000+i}", + ip=f"{host}:{port_client_base+i}", + network="255.255.0.0", + role=Roles("service_prosumer"), visible=True, other={}, ) res.append(en) + dlg = EntityCreate( + did=f"did:sov:test:{port_dlg}", + name="DLG", + ip=f"{host}:{port_dlg}/health", + network="255.255.0.0", + role=Roles("DLG"), + visible=True, + other={}, + ) + res.append(dlg) + ap = EntityCreate( + did=f"did:sov:test:{port_ap}", + name="AP", + ip=f"{host}:{port_ap}/health", + network="255.255.0.0", + role=Roles("AP"), + visible=True, + other={}, + ) + res.append(ap) return res