diff --git a/.github/workflows/dashboard-ci.yml b/.github/workflows/dashboard-ci.yml index c2f77fa..241721c 100644 --- a/.github/workflows/dashboard-ci.yml +++ b/.github/workflows/dashboard-ci.yml @@ -61,8 +61,8 @@ jobs: - uses: pnpm/action-setup@v3 name: Install pnpm with: - version: 8 - run_install: false + version: 9 + run_install: true - name: Get pnpm store directory shell: bash run: | diff --git a/app/db/__init__.py b/app/db/__init__.py index 13ae45d..52c0119 100644 --- a/app/db/__init__.py +++ b/app/db/__init__.py @@ -11,7 +11,7 @@ from .crud import ( get_admins, get_jwt_secret_key, get_notification_reminder, - assure_node_inbounds, + ensure_node_inbounds, get_system_usage, get_tls_certificate, get_user, diff --git a/app/db/crud.py b/app/db/crud.py index 0cdd947..0396d80 100644 --- a/app/db/crud.py +++ b/app/db/crud.py @@ -21,6 +21,7 @@ from app.db.models import ( Inbound, System, User, + Backend, ) from app.models.admin import AdminCreate, AdminPartialModify from app.models.node import ( @@ -55,7 +56,24 @@ def add_default_hosts(db: Session, inbounds: List[Inbound]): db.commit() -def assure_node_inbounds(db: Session, inbounds: List[Inbound], node_id: int): +def ensure_node_backends(db: Session, backends, node_id: int): + old_backends = db.query(Backend).where(Backend.node_id == node_id) + for backend in old_backends: + db.delete(backend) + backends = [ + Backend( + name=backend.name, + backend_type=backend.type, + version=backend.version, + node_id=node_id, + ) + for backend in backends + ] + db.add_all(backends) + db.flush() + + +def ensure_node_inbounds(db: Session, inbounds: List[Inbound], node_id: int): current_tags = [ i[0] for i in db.execute( @@ -67,10 +85,10 @@ def assure_node_inbounds(db: Session, inbounds: List[Inbound], node_id: int): for tag in current_tags: if tag not in updated_tags: tag_deletions.add(tag) - db_inbounds = db.query(Inbound).where( + removals = db.query(Inbound).where( and_(Inbound.node_id == node_id, Inbound.tag.in_(tag_deletions)) ) - for i in db_inbounds: + for i in removals: db.delete(i) for inb in inbounds: diff --git a/app/db/migrations/versions/35774c98f945_add_node_backends.py b/app/db/migrations/versions/35774c98f945_add_node_backends.py new file mode 100644 index 0000000..007f83a --- /dev/null +++ b/app/db/migrations/versions/35774c98f945_add_node_backends.py @@ -0,0 +1,43 @@ +"""add node backends + +Revision ID: 35774c98f945 +Revises: c4fb5f6423a8 +Create Date: 2024-08-01 08:29:56.436001 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "35774c98f945" +down_revision = "c4fb5f6423a8" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "backends", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(length=64), nullable=False), + sa.Column("node_id", sa.Integer(), nullable=True), + sa.Column("backend_type", sa.String(length=32), nullable=False), + sa.Column("version", sa.String(length=32), nullable=True), + sa.Column("running", sa.Boolean(), nullable=False), + sa.ForeignKeyConstraint( + ["node_id"], + ["nodes.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_backends_node_id"), "backends", ["node_id"]) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f("ix_backends_node_id"), table_name="backends") + op.drop_table("backends") + # ### end Alembic commands ### diff --git a/app/db/models.py b/app/db/models.py index d0058de..3b65530 100644 --- a/app/db/models.py +++ b/app/db/models.py @@ -274,6 +274,18 @@ class User(Base): return self.admin.username +class Backend(Base): + __tablename__ = "backends" + + id = Column(Integer, primary_key=True) + name = Column(String(64), nullable=False) + node_id = Column(Integer, ForeignKey("nodes.id"), index=True) + node = relationship("Node", back_populates="backends") + backend_type = Column(String(32), nullable=False) + version = Column(String(32)) + running = Column(Boolean, default=True, nullable=False) + + class Inbound(Base): __tablename__ = "inbounds" __table_args__ = (UniqueConstraint("node_id", "tag"),) @@ -375,6 +387,9 @@ class Node(Base): inbounds = relationship( "Inbound", back_populates="node", cascade="all, delete" ) + backends = relationship( + "Backend", back_populates="node", cascade="all, delete" + ) status = Column( Enum(NodeStatus), nullable=False, default=NodeStatus.unhealthy ) diff --git a/app/marznode/base.py b/app/marznode/base.py index 68a0509..7772ac8 100644 --- a/app/marznode/base.py +++ b/app/marznode/base.py @@ -11,8 +11,16 @@ class MarzNodeBase(ABC): async def fetch_users_stats(self): """get user stats from the node""" - async def is_alive(self) -> bool: - """ - checks if node is up and running and connected - :return: True if the node is responsive, otherwise False - """ + async def get_logs(self, name: str, include_buffer: bool): + pass + + async def restart_backend( + self, name: str, config: str, config_format: int + ): + pass + + async def get_backend_config(self, name: str) -> tuple[str, str]: + pass + + async def get_backend_stats(self, name: str): + pass diff --git a/app/marznode/database.py b/app/marznode/database.py index 66fff55..6179105 100644 --- a/app/marznode/database.py +++ b/app/marznode/database.py @@ -15,9 +15,13 @@ class MarzNodeDB: users[rel[0]]["inbounds"].append(rel[3].tag) return list(users.values()) - def store_inbounds(self, inbounds): + def store_backends(self, backends): + inbounds = [ + inbound for backend in backends for inbound in backend.inbounds + ] with GetDB() as db: - crud.assure_node_inbounds(db, inbounds, self.id) + crud.ensure_node_backends(db, backends, self.id) + crud.ensure_node_inbounds(db, inbounds, self.id) def set_status(self, status: NodeStatus, message: str | None = None): with GetDB() as db: diff --git a/app/marznode/grpcio.py b/app/marznode/grpcio.py index b4db6e0..a01afe9 100644 --- a/app/marznode/grpcio.py +++ b/app/marznode/grpcio.py @@ -14,8 +14,11 @@ from .marznode_pb2 import ( Empty, User, Inbound, - XrayLogsRequest, - XrayConfig, + BackendLogsRequest, + RestartBackendRequest, + BackendConfig, + Backend, + BackendStats, ) from .marznode_pb2_grpc import MarzServiceStub from ..models.node import NodeStatus @@ -125,26 +128,37 @@ class MarzNodeGRPCIO(MarzNodeBase, MarzNodeDB): response = await self._stub.FetchUsersStats(Empty()) return response.users_stats - async def _fetch_inbounds(self) -> list: - response = await self._stub.FetchInbounds(Empty()) - return response.inbounds + async def _fetch_backends(self) -> list: + response = await self._stub.FetchBackends(Empty()) + return response.backends async def _sync(self): - inbounds = await self._fetch_inbounds() - self.store_inbounds(inbounds) + backends = await self._fetch_backends() + self.store_backends(backends) users = self.list_users() await self._repopulate_users(users) self.synced = True - async def get_logs(self, include_buffer=True): - async for response in self._stub.StreamXrayLogs( - XrayLogsRequest(include_buffer=include_buffer) + async def get_logs(self, name: str = "xray", include_buffer=True): + async for response in self._stub.StreamBackendLogs( + BackendLogsRequest( + backend_name=name, include_buffer=include_buffer + ) ): yield response.line - async def restart_xray(self, config: str): + async def restart_backend( + self, name: str, config: str, config_format: int + ): try: - await self._stub.RestartXray(XrayConfig(configuration=config)) + await self._stub.RestartBackend( + RestartBackendRequest( + backend_name=name, + config=BackendConfig( + configuration=config, config_format=config_format + ), + ) + ) await self._sync() except RpcError: self.synced = False @@ -153,6 +167,12 @@ class MarzNodeGRPCIO(MarzNodeBase, MarzNodeDB): else: self.set_status(NodeStatus.healthy) - async def get_xray_config(self): - response = await self._stub.FetchXrayConfig(Empty()) - return response.configuration + async def get_backend_config(self, name: str = "xray"): + response = await self._stub.FetchBackendConfig(Backend(name=name)) + return response.configuration, response.config_format + + async def get_backend_stats(self, name: str): + response: BackendStats = await self._stub.GetBackendStats( + Backend(name=name) + ) + return response diff --git a/app/marznode/grpclib.py b/app/marznode/grpclib.py index b1e0ccb..758ee78 100644 --- a/app/marznode/grpclib.py +++ b/app/marznode/grpclib.py @@ -17,8 +17,11 @@ from .marznode_pb2 import ( Empty, User, Inbound, - XrayLogsRequest, - XrayConfig, + BackendConfig, + BackendLogsRequest, + Backend, + RestartBackendRequest, + BackendStats, ) from ..models.node import NodeStatus @@ -133,29 +136,40 @@ class MarzNodeGRPCLIB(MarzNodeBase, MarzNodeDB): response = await self._stub.FetchUsersStats(Empty()) return response.users_stats - async def _fetch_inbounds(self) -> list: - response = await self._stub.FetchInbounds(Empty()) - return response.inbounds + async def _fetch_backends(self) -> list: + response = await self._stub.FetchBackends(Empty()) + return response.backends async def _sync(self): - inbounds = await self._fetch_inbounds() - self.store_inbounds(inbounds) + backends = await self._fetch_backends() + self.store_backends(backends) users = self.list_users() await self._repopulate_users(users) self.synced = True - async def get_logs(self, include_buffer=True): - async with self._stub.StreamXrayLogs.open() as stm: + async def get_logs(self, name: str = "xray", include_buffer=True): + async with self._stub.StreamBackendLogs.open() as stm: await stm.send_message( - XrayLogsRequest(include_buffer=include_buffer) + BackendLogsRequest( + backend_name=name, include_buffer=include_buffer + ) ) while True: response = await stm.recv_message() yield response.line - async def restart_xray(self, config: str): + async def restart_backend( + self, name: str, config: str, config_format: int + ): try: - await self._stub.RestartXray(XrayConfig(configuration=config)) + await self._stub.RestartBackend( + RestartBackendRequest( + backend_name=name, + config=BackendConfig( + configuration=config, config_format=config_format + ), + ) + ) await self._sync() except: self.synced = False @@ -164,6 +178,14 @@ class MarzNodeGRPCLIB(MarzNodeBase, MarzNodeDB): else: self.set_status(NodeStatus.healthy) - async def get_xray_config(self): - response = await self._stub.FetchXrayConfig(Empty()) - return response.configuration + async def get_backend_config(self, name: str): + response: BackendConfig = await self._stub.FetchBackendConfig( + Backend(name=name) + ) + return response.configuration, response.config_format + + async def get_backend_stats(self, name: str): + response: BackendStats = await self._stub.GetBackendStats( + Backend(name=name) + ) + return response diff --git a/app/marznode/marznode.proto b/app/marznode/marznode.proto index f7b1f64..9ed9332 100644 --- a/app/marznode/marznode.proto +++ b/app/marznode/marznode.proto @@ -4,15 +4,28 @@ package marznode; message Empty {} +enum ConfigFormat { + PLAIN = 0; + JSON = 1; + YAML = 2; +} + +message Backend { + string name = 1; + optional string type = 2; + optional string version = 3; + repeated Inbound inbounds = 4; +} + +message BackendsResponse { + repeated Backend backends = 1; +} + message Inbound { string tag = 1; optional string config = 2; } -message InboundsResponse { - repeated Inbound inbounds = 1; -} - message User { uint32 id = 1; string username = 2; @@ -40,20 +53,32 @@ message LogLine { string line = 1; } -message XrayConfig { +message BackendConfig { string configuration = 1; + ConfigFormat config_format = 2; +} + +message BackendLogsRequest { + string backend_name = 1; + bool include_buffer = 2; +} + +message RestartBackendRequest { + string backend_name = 1; + optional BackendConfig config = 2; } -message XrayLogsRequest { - bool include_buffer = 1; +message BackendStats { + bool running = 1; } service MarzService { rpc SyncUsers(stream UserData) returns (Empty); rpc RepopulateUsers(UsersData) returns (Empty); - rpc FetchInbounds(Empty) returns (InboundsResponse); + rpc FetchBackends(Empty) returns (BackendsResponse); rpc FetchUsersStats(Empty) returns (UsersStats); - rpc FetchXrayConfig(Empty) returns (XrayConfig); - rpc RestartXray(XrayConfig) returns (InboundsResponse); - rpc StreamXrayLogs(XrayLogsRequest) returns(stream LogLine); + rpc FetchBackendConfig(Backend) returns (BackendConfig); + rpc RestartBackend(RestartBackendRequest) returns (Empty); + rpc StreamBackendLogs(BackendLogsRequest) returns (stream LogLine); + rpc GetBackendStats(Backend) returns (BackendStats); } diff --git a/app/marznode/marznode_grpc.py b/app/marznode/marznode_grpc.py index 3492736..6deea56 100644 --- a/app/marznode/marznode_grpc.py +++ b/app/marznode/marznode_grpc.py @@ -23,7 +23,7 @@ class MarzServiceBase(abc.ABC): pass @abc.abstractmethod - async def FetchInbounds(self, stream: 'grpclib.server.Stream[app.marznode.marznode_pb2.Empty, app.marznode.marznode_pb2.InboundsResponse]') -> None: + async def FetchBackends(self, stream: 'grpclib.server.Stream[app.marznode.marznode_pb2.Empty, app.marznode.marznode_pb2.BackendsResponse]') -> None: pass @abc.abstractmethod @@ -31,15 +31,19 @@ class MarzServiceBase(abc.ABC): pass @abc.abstractmethod - async def FetchXrayConfig(self, stream: 'grpclib.server.Stream[app.marznode.marznode_pb2.Empty, app.marznode.marznode_pb2.XrayConfig]') -> None: + async def FetchBackendConfig(self, stream: 'grpclib.server.Stream[app.marznode.marznode_pb2.Backend, app.marznode.marznode_pb2.BackendConfig]') -> None: pass @abc.abstractmethod - async def RestartXray(self, stream: 'grpclib.server.Stream[app.marznode.marznode_pb2.XrayConfig, app.marznode.marznode_pb2.InboundsResponse]') -> None: + async def RestartBackend(self, stream: 'grpclib.server.Stream[app.marznode.marznode_pb2.RestartBackendRequest, app.marznode.marznode_pb2.Empty]') -> None: pass @abc.abstractmethod - async def StreamXrayLogs(self, stream: 'grpclib.server.Stream[app.marznode.marznode_pb2.XrayLogsRequest, app.marznode.marznode_pb2.LogLine]') -> None: + async def StreamBackendLogs(self, stream: 'grpclib.server.Stream[app.marznode.marznode_pb2.BackendLogsRequest, app.marznode.marznode_pb2.LogLine]') -> None: + pass + + @abc.abstractmethod + async def GetBackendStats(self, stream: 'grpclib.server.Stream[app.marznode.marznode_pb2.Backend, app.marznode.marznode_pb2.BackendStats]') -> None: pass def __mapping__(self) -> typing.Dict[str, grpclib.const.Handler]: @@ -56,11 +60,11 @@ class MarzServiceBase(abc.ABC): app.marznode.marznode_pb2.UsersData, app.marznode.marznode_pb2.Empty, ), - '/marznode.MarzService/FetchInbounds': grpclib.const.Handler( - self.FetchInbounds, + '/marznode.MarzService/FetchBackends': grpclib.const.Handler( + self.FetchBackends, grpclib.const.Cardinality.UNARY_UNARY, app.marznode.marznode_pb2.Empty, - app.marznode.marznode_pb2.InboundsResponse, + app.marznode.marznode_pb2.BackendsResponse, ), '/marznode.MarzService/FetchUsersStats': grpclib.const.Handler( self.FetchUsersStats, @@ -68,24 +72,30 @@ class MarzServiceBase(abc.ABC): app.marznode.marznode_pb2.Empty, app.marznode.marznode_pb2.UsersStats, ), - '/marznode.MarzService/FetchXrayConfig': grpclib.const.Handler( - self.FetchXrayConfig, + '/marznode.MarzService/FetchBackendConfig': grpclib.const.Handler( + self.FetchBackendConfig, grpclib.const.Cardinality.UNARY_UNARY, - app.marznode.marznode_pb2.Empty, - app.marznode.marznode_pb2.XrayConfig, + app.marznode.marznode_pb2.Backend, + app.marznode.marznode_pb2.BackendConfig, ), - '/marznode.MarzService/RestartXray': grpclib.const.Handler( - self.RestartXray, + '/marznode.MarzService/RestartBackend': grpclib.const.Handler( + self.RestartBackend, grpclib.const.Cardinality.UNARY_UNARY, - app.marznode.marznode_pb2.XrayConfig, - app.marznode.marznode_pb2.InboundsResponse, + app.marznode.marznode_pb2.RestartBackendRequest, + app.marznode.marznode_pb2.Empty, ), - '/marznode.MarzService/StreamXrayLogs': grpclib.const.Handler( - self.StreamXrayLogs, + '/marznode.MarzService/StreamBackendLogs': grpclib.const.Handler( + self.StreamBackendLogs, grpclib.const.Cardinality.UNARY_STREAM, - app.marznode.marznode_pb2.XrayLogsRequest, + app.marznode.marznode_pb2.BackendLogsRequest, app.marznode.marznode_pb2.LogLine, ), + '/marznode.MarzService/GetBackendStats': grpclib.const.Handler( + self.GetBackendStats, + grpclib.const.Cardinality.UNARY_UNARY, + app.marznode.marznode_pb2.Backend, + app.marznode.marznode_pb2.BackendStats, + ), } @@ -104,11 +114,11 @@ class MarzServiceStub: app.marznode.marznode_pb2.UsersData, app.marznode.marznode_pb2.Empty, ) - self.FetchInbounds = grpclib.client.UnaryUnaryMethod( + self.FetchBackends = grpclib.client.UnaryUnaryMethod( channel, - '/marznode.MarzService/FetchInbounds', + '/marznode.MarzService/FetchBackends', app.marznode.marznode_pb2.Empty, - app.marznode.marznode_pb2.InboundsResponse, + app.marznode.marznode_pb2.BackendsResponse, ) self.FetchUsersStats = grpclib.client.UnaryUnaryMethod( channel, @@ -116,21 +126,27 @@ class MarzServiceStub: app.marznode.marznode_pb2.Empty, app.marznode.marznode_pb2.UsersStats, ) - self.FetchXrayConfig = grpclib.client.UnaryUnaryMethod( + self.FetchBackendConfig = grpclib.client.UnaryUnaryMethod( channel, - '/marznode.MarzService/FetchXrayConfig', - app.marznode.marznode_pb2.Empty, - app.marznode.marznode_pb2.XrayConfig, + '/marznode.MarzService/FetchBackendConfig', + app.marznode.marznode_pb2.Backend, + app.marznode.marznode_pb2.BackendConfig, ) - self.RestartXray = grpclib.client.UnaryUnaryMethod( + self.RestartBackend = grpclib.client.UnaryUnaryMethod( channel, - '/marznode.MarzService/RestartXray', - app.marznode.marznode_pb2.XrayConfig, - app.marznode.marznode_pb2.InboundsResponse, + '/marznode.MarzService/RestartBackend', + app.marznode.marznode_pb2.RestartBackendRequest, + app.marznode.marznode_pb2.Empty, ) - self.StreamXrayLogs = grpclib.client.UnaryStreamMethod( + self.StreamBackendLogs = grpclib.client.UnaryStreamMethod( channel, - '/marznode.MarzService/StreamXrayLogs', - app.marznode.marznode_pb2.XrayLogsRequest, + '/marznode.MarzService/StreamBackendLogs', + app.marznode.marznode_pb2.BackendLogsRequest, app.marznode.marznode_pb2.LogLine, ) + self.GetBackendStats = grpclib.client.UnaryUnaryMethod( + channel, + '/marznode.MarzService/GetBackendStats', + app.marznode.marznode_pb2.Backend, + app.marznode.marznode_pb2.BackendStats, + ) diff --git a/app/marznode/marznode_pb2.py b/app/marznode/marznode_pb2.py index c0f1798..7ca57ed 100644 --- a/app/marznode/marznode_pb2.py +++ b/app/marznode/marznode_pb2.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: app/marznode/marznode.proto -# Protobuf Python Version: 4.25.1 +# Protobuf Python Version: 5.26.1 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool @@ -14,35 +14,43 @@ _sym_db = _symbol_database.Default() -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1b\x61pp/marznode/marznode.proto\x12\x08marznode\"\x07\n\x05\x45mpty\"6\n\x07Inbound\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12\x13\n\x06\x63onfig\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\t\n\x07_config\"7\n\x10InboundsResponse\x12#\n\x08inbounds\x18\x01 \x03(\x0b\x32\x11.marznode.Inbound\"1\n\x04User\x12\n\n\x02id\x18\x01 \x01(\r\x12\x10\n\x08username\x18\x02 \x01(\t\x12\x0b\n\x03key\x18\x03 \x01(\t\"M\n\x08UserData\x12\x1c\n\x04user\x18\x01 \x01(\x0b\x32\x0e.marznode.User\x12#\n\x08inbounds\x18\x02 \x03(\x0b\x32\x11.marznode.Inbound\"3\n\tUsersData\x12&\n\nusers_data\x18\x01 \x03(\x0b\x32\x12.marznode.UserData\"j\n\nUsersStats\x12\x33\n\x0busers_stats\x18\x01 \x03(\x0b\x32\x1e.marznode.UsersStats.UserStats\x1a\'\n\tUserStats\x12\x0b\n\x03uid\x18\x01 \x01(\r\x12\r\n\x05usage\x18\x02 \x01(\x04\"\x17\n\x07LogLine\x12\x0c\n\x04line\x18\x01 \x01(\t\"#\n\nXrayConfig\x12\x15\n\rconfiguration\x18\x01 \x01(\t\")\n\x0fXrayLogsRequest\x12\x16\n\x0einclude_buffer\x18\x01 \x01(\x08\x32\xaf\x03\n\x0bMarzService\x12\x32\n\tSyncUsers\x12\x12.marznode.UserData\x1a\x0f.marznode.Empty(\x01\x12\x37\n\x0fRepopulateUsers\x12\x13.marznode.UsersData\x1a\x0f.marznode.Empty\x12<\n\rFetchInbounds\x12\x0f.marznode.Empty\x1a\x1a.marznode.InboundsResponse\x12\x38\n\x0f\x46\x65tchUsersStats\x12\x0f.marznode.Empty\x1a\x14.marznode.UsersStats\x12\x38\n\x0f\x46\x65tchXrayConfig\x12\x0f.marznode.Empty\x1a\x14.marznode.XrayConfig\x12?\n\x0bRestartXray\x12\x14.marznode.XrayConfig\x1a\x1a.marznode.InboundsResponse\x12@\n\x0eStreamXrayLogs\x12\x19.marznode.XrayLogsRequest\x1a\x11.marznode.LogLine0\x01\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1b\x61pp/marznode/marznode.proto\x12\x08marznode\"\x07\n\x05\x45mpty\"z\n\x07\x42\x61\x63kend\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x11\n\x04type\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x14\n\x07version\x18\x03 \x01(\tH\x01\x88\x01\x01\x12#\n\x08inbounds\x18\x04 \x03(\x0b\x32\x11.marznode.InboundB\x07\n\x05_typeB\n\n\x08_version\"7\n\x10\x42\x61\x63kendsResponse\x12#\n\x08\x62\x61\x63kends\x18\x01 \x03(\x0b\x32\x11.marznode.Backend\"6\n\x07Inbound\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12\x13\n\x06\x63onfig\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\t\n\x07_config\"1\n\x04User\x12\n\n\x02id\x18\x01 \x01(\r\x12\x10\n\x08username\x18\x02 \x01(\t\x12\x0b\n\x03key\x18\x03 \x01(\t\"M\n\x08UserData\x12\x1c\n\x04user\x18\x01 \x01(\x0b\x32\x0e.marznode.User\x12#\n\x08inbounds\x18\x02 \x03(\x0b\x32\x11.marznode.Inbound\"3\n\tUsersData\x12&\n\nusers_data\x18\x01 \x03(\x0b\x32\x12.marznode.UserData\"j\n\nUsersStats\x12\x33\n\x0busers_stats\x18\x01 \x03(\x0b\x32\x1e.marznode.UsersStats.UserStats\x1a\'\n\tUserStats\x12\x0b\n\x03uid\x18\x01 \x01(\r\x12\r\n\x05usage\x18\x02 \x01(\x04\"\x17\n\x07LogLine\x12\x0c\n\x04line\x18\x01 \x01(\t\"U\n\rBackendConfig\x12\x15\n\rconfiguration\x18\x01 \x01(\t\x12-\n\rconfig_format\x18\x02 \x01(\x0e\x32\x16.marznode.ConfigFormat\"B\n\x12\x42\x61\x63kendLogsRequest\x12\x14\n\x0c\x62\x61\x63kend_name\x18\x01 \x01(\t\x12\x16\n\x0einclude_buffer\x18\x02 \x01(\x08\"f\n\x15RestartBackendRequest\x12\x14\n\x0c\x62\x61\x63kend_name\x18\x01 \x01(\t\x12,\n\x06\x63onfig\x18\x02 \x01(\x0b\x32\x17.marznode.BackendConfigH\x00\x88\x01\x01\x42\t\n\x07_config\"\x1f\n\x0c\x42\x61\x63kendStats\x12\x0f\n\x07running\x18\x01 \x01(\x08*-\n\x0c\x43onfigFormat\x12\t\n\x05PLAIN\x10\x00\x12\x08\n\x04JSON\x10\x01\x12\x08\n\x04YAML\x10\x02\x32\xfe\x03\n\x0bMarzService\x12\x32\n\tSyncUsers\x12\x12.marznode.UserData\x1a\x0f.marznode.Empty(\x01\x12\x37\n\x0fRepopulateUsers\x12\x13.marznode.UsersData\x1a\x0f.marznode.Empty\x12<\n\rFetchBackends\x12\x0f.marznode.Empty\x1a\x1a.marznode.BackendsResponse\x12\x38\n\x0f\x46\x65tchUsersStats\x12\x0f.marznode.Empty\x1a\x14.marznode.UsersStats\x12@\n\x12\x46\x65tchBackendConfig\x12\x11.marznode.Backend\x1a\x17.marznode.BackendConfig\x12\x42\n\x0eRestartBackend\x12\x1f.marznode.RestartBackendRequest\x1a\x0f.marznode.Empty\x12\x46\n\x11StreamBackendLogs\x12\x1c.marznode.BackendLogsRequest\x1a\x11.marznode.LogLine0\x01\x12<\n\x0fGetBackendStats\x12\x11.marznode.Backend\x1a\x16.marznode.BackendStatsb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'app.marznode.marznode_pb2', _globals) -if _descriptor._USE_C_DESCRIPTORS == False: - DESCRIPTOR._options = None +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_CONFIGFORMAT']._serialized_start=895 + _globals['_CONFIGFORMAT']._serialized_end=940 _globals['_EMPTY']._serialized_start=41 _globals['_EMPTY']._serialized_end=48 - _globals['_INBOUND']._serialized_start=50 - _globals['_INBOUND']._serialized_end=104 - _globals['_INBOUNDSRESPONSE']._serialized_start=106 - _globals['_INBOUNDSRESPONSE']._serialized_end=161 - _globals['_USER']._serialized_start=163 - _globals['_USER']._serialized_end=212 - _globals['_USERDATA']._serialized_start=214 - _globals['_USERDATA']._serialized_end=291 - _globals['_USERSDATA']._serialized_start=293 - _globals['_USERSDATA']._serialized_end=344 - _globals['_USERSSTATS']._serialized_start=346 - _globals['_USERSSTATS']._serialized_end=452 - _globals['_USERSSTATS_USERSTATS']._serialized_start=413 - _globals['_USERSSTATS_USERSTATS']._serialized_end=452 - _globals['_LOGLINE']._serialized_start=454 - _globals['_LOGLINE']._serialized_end=477 - _globals['_XRAYCONFIG']._serialized_start=479 - _globals['_XRAYCONFIG']._serialized_end=514 - _globals['_XRAYLOGSREQUEST']._serialized_start=516 - _globals['_XRAYLOGSREQUEST']._serialized_end=557 - _globals['_MARZSERVICE']._serialized_start=560 - _globals['_MARZSERVICE']._serialized_end=991 + _globals['_BACKEND']._serialized_start=50 + _globals['_BACKEND']._serialized_end=172 + _globals['_BACKENDSRESPONSE']._serialized_start=174 + _globals['_BACKENDSRESPONSE']._serialized_end=229 + _globals['_INBOUND']._serialized_start=231 + _globals['_INBOUND']._serialized_end=285 + _globals['_USER']._serialized_start=287 + _globals['_USER']._serialized_end=336 + _globals['_USERDATA']._serialized_start=338 + _globals['_USERDATA']._serialized_end=415 + _globals['_USERSDATA']._serialized_start=417 + _globals['_USERSDATA']._serialized_end=468 + _globals['_USERSSTATS']._serialized_start=470 + _globals['_USERSSTATS']._serialized_end=576 + _globals['_USERSSTATS_USERSTATS']._serialized_start=537 + _globals['_USERSSTATS_USERSTATS']._serialized_end=576 + _globals['_LOGLINE']._serialized_start=578 + _globals['_LOGLINE']._serialized_end=601 + _globals['_BACKENDCONFIG']._serialized_start=603 + _globals['_BACKENDCONFIG']._serialized_end=688 + _globals['_BACKENDLOGSREQUEST']._serialized_start=690 + _globals['_BACKENDLOGSREQUEST']._serialized_end=756 + _globals['_RESTARTBACKENDREQUEST']._serialized_start=758 + _globals['_RESTARTBACKENDREQUEST']._serialized_end=860 + _globals['_BACKENDSTATS']._serialized_start=862 + _globals['_BACKENDSTATS']._serialized_end=893 + _globals['_MARZSERVICE']._serialized_start=943 + _globals['_MARZSERVICE']._serialized_end=1453 # @@protoc_insertion_point(module_scope) diff --git a/app/marznode/marznode_pb2.pyi b/app/marznode/marznode_pb2.pyi index 5a3d03c..3348be7 100644 --- a/app/marznode/marznode_pb2.pyi +++ b/app/marznode/marznode_pb2.pyi @@ -1,14 +1,42 @@ from google.protobuf.internal import containers as _containers +from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union DESCRIPTOR: _descriptor.FileDescriptor +class ConfigFormat(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + PLAIN: _ClassVar[ConfigFormat] + JSON: _ClassVar[ConfigFormat] + YAML: _ClassVar[ConfigFormat] +PLAIN: ConfigFormat +JSON: ConfigFormat +YAML: ConfigFormat + class Empty(_message.Message): __slots__ = () def __init__(self) -> None: ... +class Backend(_message.Message): + __slots__ = ("name", "type", "version", "inbounds") + NAME_FIELD_NUMBER: _ClassVar[int] + TYPE_FIELD_NUMBER: _ClassVar[int] + VERSION_FIELD_NUMBER: _ClassVar[int] + INBOUNDS_FIELD_NUMBER: _ClassVar[int] + name: str + type: str + version: str + inbounds: _containers.RepeatedCompositeFieldContainer[Inbound] + def __init__(self, name: _Optional[str] = ..., type: _Optional[str] = ..., version: _Optional[str] = ..., inbounds: _Optional[_Iterable[_Union[Inbound, _Mapping]]] = ...) -> None: ... + +class BackendsResponse(_message.Message): + __slots__ = ("backends",) + BACKENDS_FIELD_NUMBER: _ClassVar[int] + backends: _containers.RepeatedCompositeFieldContainer[Backend] + def __init__(self, backends: _Optional[_Iterable[_Union[Backend, _Mapping]]] = ...) -> None: ... + class Inbound(_message.Message): __slots__ = ("tag", "config") TAG_FIELD_NUMBER: _ClassVar[int] @@ -17,12 +45,6 @@ class Inbound(_message.Message): config: str def __init__(self, tag: _Optional[str] = ..., config: _Optional[str] = ...) -> None: ... -class InboundsResponse(_message.Message): - __slots__ = ("inbounds",) - INBOUNDS_FIELD_NUMBER: _ClassVar[int] - inbounds: _containers.RepeatedCompositeFieldContainer[Inbound] - def __init__(self, inbounds: _Optional[_Iterable[_Union[Inbound, _Mapping]]] = ...) -> None: ... - class User(_message.Message): __slots__ = ("id", "username", "key") ID_FIELD_NUMBER: _ClassVar[int] @@ -66,14 +88,32 @@ class LogLine(_message.Message): line: str def __init__(self, line: _Optional[str] = ...) -> None: ... -class XrayConfig(_message.Message): - __slots__ = ("configuration",) +class BackendConfig(_message.Message): + __slots__ = ("configuration", "config_format") CONFIGURATION_FIELD_NUMBER: _ClassVar[int] + CONFIG_FORMAT_FIELD_NUMBER: _ClassVar[int] configuration: str - def __init__(self, configuration: _Optional[str] = ...) -> None: ... + config_format: ConfigFormat + def __init__(self, configuration: _Optional[str] = ..., config_format: _Optional[_Union[ConfigFormat, str]] = ...) -> None: ... -class XrayLogsRequest(_message.Message): - __slots__ = ("include_buffer",) +class BackendLogsRequest(_message.Message): + __slots__ = ("backend_name", "include_buffer") + BACKEND_NAME_FIELD_NUMBER: _ClassVar[int] INCLUDE_BUFFER_FIELD_NUMBER: _ClassVar[int] + backend_name: str include_buffer: bool - def __init__(self, include_buffer: bool = ...) -> None: ... + def __init__(self, backend_name: _Optional[str] = ..., include_buffer: bool = ...) -> None: ... + +class RestartBackendRequest(_message.Message): + __slots__ = ("backend_name", "config") + BACKEND_NAME_FIELD_NUMBER: _ClassVar[int] + CONFIG_FIELD_NUMBER: _ClassVar[int] + backend_name: str + config: BackendConfig + def __init__(self, backend_name: _Optional[str] = ..., config: _Optional[_Union[BackendConfig, _Mapping]] = ...) -> None: ... + +class BackendStats(_message.Message): + __slots__ = ("running",) + RUNNING_FIELD_NUMBER: _ClassVar[int] + running: bool + def __init__(self, running: bool = ...) -> None: ... diff --git a/app/marznode/marznode_pb2_grpc.py b/app/marznode/marznode_pb2_grpc.py index c444abd..61f747f 100644 --- a/app/marznode/marznode_pb2_grpc.py +++ b/app/marznode/marznode_pb2_grpc.py @@ -1,9 +1,34 @@ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! """Client and server classes corresponding to protobuf-defined services.""" import grpc +import warnings from app.marznode import marznode_pb2 as app_dot_marznode_dot_marznode__pb2 +GRPC_GENERATED_VERSION = '1.65.4' +GRPC_VERSION = grpc.__version__ +EXPECTED_ERROR_RELEASE = '1.66.0' +SCHEDULED_RELEASE_DATE = 'August 6, 2024' +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + warnings.warn( + f'The grpc package installed is at version {GRPC_VERSION},' + + f' but the generated code in app/marznode/marznode_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + + f' This warning will become an error in {EXPECTED_ERROR_RELEASE},' + + f' scheduled for release on {SCHEDULED_RELEASE_DATE}.', + RuntimeWarning + ) + class MarzServiceStub(object): """Missing associated documentation comment in .proto file.""" @@ -18,37 +43,42 @@ class MarzServiceStub(object): '/marznode.MarzService/SyncUsers', request_serializer=app_dot_marznode_dot_marznode__pb2.UserData.SerializeToString, response_deserializer=app_dot_marznode_dot_marznode__pb2.Empty.FromString, - ) + _registered_method=True) self.RepopulateUsers = channel.unary_unary( '/marznode.MarzService/RepopulateUsers', request_serializer=app_dot_marznode_dot_marznode__pb2.UsersData.SerializeToString, response_deserializer=app_dot_marznode_dot_marznode__pb2.Empty.FromString, - ) - self.FetchInbounds = channel.unary_unary( - '/marznode.MarzService/FetchInbounds', + _registered_method=True) + self.FetchBackends = channel.unary_unary( + '/marznode.MarzService/FetchBackends', request_serializer=app_dot_marznode_dot_marznode__pb2.Empty.SerializeToString, - response_deserializer=app_dot_marznode_dot_marznode__pb2.InboundsResponse.FromString, - ) + response_deserializer=app_dot_marznode_dot_marznode__pb2.BackendsResponse.FromString, + _registered_method=True) self.FetchUsersStats = channel.unary_unary( '/marznode.MarzService/FetchUsersStats', request_serializer=app_dot_marznode_dot_marznode__pb2.Empty.SerializeToString, response_deserializer=app_dot_marznode_dot_marznode__pb2.UsersStats.FromString, - ) - self.FetchXrayConfig = channel.unary_unary( - '/marznode.MarzService/FetchXrayConfig', - request_serializer=app_dot_marznode_dot_marznode__pb2.Empty.SerializeToString, - response_deserializer=app_dot_marznode_dot_marznode__pb2.XrayConfig.FromString, - ) - self.RestartXray = channel.unary_unary( - '/marznode.MarzService/RestartXray', - request_serializer=app_dot_marznode_dot_marznode__pb2.XrayConfig.SerializeToString, - response_deserializer=app_dot_marznode_dot_marznode__pb2.InboundsResponse.FromString, - ) - self.StreamXrayLogs = channel.unary_stream( - '/marznode.MarzService/StreamXrayLogs', - request_serializer=app_dot_marznode_dot_marznode__pb2.XrayLogsRequest.SerializeToString, + _registered_method=True) + self.FetchBackendConfig = channel.unary_unary( + '/marznode.MarzService/FetchBackendConfig', + request_serializer=app_dot_marznode_dot_marznode__pb2.Backend.SerializeToString, + response_deserializer=app_dot_marznode_dot_marznode__pb2.BackendConfig.FromString, + _registered_method=True) + self.RestartBackend = channel.unary_unary( + '/marznode.MarzService/RestartBackend', + request_serializer=app_dot_marznode_dot_marznode__pb2.RestartBackendRequest.SerializeToString, + response_deserializer=app_dot_marznode_dot_marznode__pb2.Empty.FromString, + _registered_method=True) + self.StreamBackendLogs = channel.unary_stream( + '/marznode.MarzService/StreamBackendLogs', + request_serializer=app_dot_marznode_dot_marznode__pb2.BackendLogsRequest.SerializeToString, response_deserializer=app_dot_marznode_dot_marznode__pb2.LogLine.FromString, - ) + _registered_method=True) + self.GetBackendStats = channel.unary_unary( + '/marznode.MarzService/GetBackendStats', + request_serializer=app_dot_marznode_dot_marznode__pb2.Backend.SerializeToString, + response_deserializer=app_dot_marznode_dot_marznode__pb2.BackendStats.FromString, + _registered_method=True) class MarzServiceServicer(object): @@ -66,7 +96,7 @@ class MarzServiceServicer(object): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') - def FetchInbounds(self, request, context): + def FetchBackends(self, request, context): """Missing associated documentation comment in .proto file.""" context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details('Method not implemented!') @@ -78,19 +108,25 @@ class MarzServiceServicer(object): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') - def FetchXrayConfig(self, request, context): + def FetchBackendConfig(self, request, context): """Missing associated documentation comment in .proto file.""" context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') - def RestartXray(self, request, context): + def RestartBackend(self, request, context): """Missing associated documentation comment in .proto file.""" context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') - def StreamXrayLogs(self, request, context): + def StreamBackendLogs(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetBackendStats(self, request, context): """Missing associated documentation comment in .proto file.""" context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details('Method not implemented!') @@ -109,35 +145,41 @@ def add_MarzServiceServicer_to_server(servicer, server): request_deserializer=app_dot_marznode_dot_marznode__pb2.UsersData.FromString, response_serializer=app_dot_marznode_dot_marznode__pb2.Empty.SerializeToString, ), - 'FetchInbounds': grpc.unary_unary_rpc_method_handler( - servicer.FetchInbounds, + 'FetchBackends': grpc.unary_unary_rpc_method_handler( + servicer.FetchBackends, request_deserializer=app_dot_marznode_dot_marznode__pb2.Empty.FromString, - response_serializer=app_dot_marznode_dot_marznode__pb2.InboundsResponse.SerializeToString, + response_serializer=app_dot_marznode_dot_marznode__pb2.BackendsResponse.SerializeToString, ), 'FetchUsersStats': grpc.unary_unary_rpc_method_handler( servicer.FetchUsersStats, request_deserializer=app_dot_marznode_dot_marznode__pb2.Empty.FromString, response_serializer=app_dot_marznode_dot_marznode__pb2.UsersStats.SerializeToString, ), - 'FetchXrayConfig': grpc.unary_unary_rpc_method_handler( - servicer.FetchXrayConfig, - request_deserializer=app_dot_marznode_dot_marznode__pb2.Empty.FromString, - response_serializer=app_dot_marznode_dot_marznode__pb2.XrayConfig.SerializeToString, + 'FetchBackendConfig': grpc.unary_unary_rpc_method_handler( + servicer.FetchBackendConfig, + request_deserializer=app_dot_marznode_dot_marznode__pb2.Backend.FromString, + response_serializer=app_dot_marznode_dot_marznode__pb2.BackendConfig.SerializeToString, ), - 'RestartXray': grpc.unary_unary_rpc_method_handler( - servicer.RestartXray, - request_deserializer=app_dot_marznode_dot_marznode__pb2.XrayConfig.FromString, - response_serializer=app_dot_marznode_dot_marznode__pb2.InboundsResponse.SerializeToString, + 'RestartBackend': grpc.unary_unary_rpc_method_handler( + servicer.RestartBackend, + request_deserializer=app_dot_marznode_dot_marznode__pb2.RestartBackendRequest.FromString, + response_serializer=app_dot_marznode_dot_marznode__pb2.Empty.SerializeToString, ), - 'StreamXrayLogs': grpc.unary_stream_rpc_method_handler( - servicer.StreamXrayLogs, - request_deserializer=app_dot_marznode_dot_marznode__pb2.XrayLogsRequest.FromString, + 'StreamBackendLogs': grpc.unary_stream_rpc_method_handler( + servicer.StreamBackendLogs, + request_deserializer=app_dot_marznode_dot_marznode__pb2.BackendLogsRequest.FromString, response_serializer=app_dot_marznode_dot_marznode__pb2.LogLine.SerializeToString, ), + 'GetBackendStats': grpc.unary_unary_rpc_method_handler( + servicer.GetBackendStats, + request_deserializer=app_dot_marznode_dot_marznode__pb2.Backend.FromString, + response_serializer=app_dot_marznode_dot_marznode__pb2.BackendStats.SerializeToString, + ), } generic_handler = grpc.method_handlers_generic_handler( 'marznode.MarzService', rpc_method_handlers) server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('marznode.MarzService', rpc_method_handlers) # This class is part of an EXPERIMENTAL API. @@ -155,11 +197,21 @@ class MarzService(object): wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.stream_unary(request_iterator, target, '/marznode.MarzService/SyncUsers', + return grpc.experimental.stream_unary( + request_iterator, + target, + '/marznode.MarzService/SyncUsers', app_dot_marznode_dot_marznode__pb2.UserData.SerializeToString, app_dot_marznode_dot_marznode__pb2.Empty.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def RepopulateUsers(request, @@ -172,14 +224,24 @@ class MarzService(object): wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/marznode.MarzService/RepopulateUsers', + return grpc.experimental.unary_unary( + request, + target, + '/marznode.MarzService/RepopulateUsers', app_dot_marznode_dot_marznode__pb2.UsersData.SerializeToString, app_dot_marznode_dot_marznode__pb2.Empty.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod - def FetchInbounds(request, + def FetchBackends(request, target, options=(), channel_credentials=None, @@ -189,11 +251,21 @@ class MarzService(object): wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/marznode.MarzService/FetchInbounds', + return grpc.experimental.unary_unary( + request, + target, + '/marznode.MarzService/FetchBackends', app_dot_marznode_dot_marznode__pb2.Empty.SerializeToString, - app_dot_marznode_dot_marznode__pb2.InboundsResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + app_dot_marznode_dot_marznode__pb2.BackendsResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def FetchUsersStats(request, @@ -206,14 +278,24 @@ class MarzService(object): wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/marznode.MarzService/FetchUsersStats', + return grpc.experimental.unary_unary( + request, + target, + '/marznode.MarzService/FetchUsersStats', app_dot_marznode_dot_marznode__pb2.Empty.SerializeToString, app_dot_marznode_dot_marznode__pb2.UsersStats.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod - def FetchXrayConfig(request, + def FetchBackendConfig(request, target, options=(), channel_credentials=None, @@ -223,14 +305,24 @@ class MarzService(object): wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/marznode.MarzService/FetchXrayConfig', - app_dot_marznode_dot_marznode__pb2.Empty.SerializeToString, - app_dot_marznode_dot_marznode__pb2.XrayConfig.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + return grpc.experimental.unary_unary( + request, + target, + '/marznode.MarzService/FetchBackendConfig', + app_dot_marznode_dot_marznode__pb2.Backend.SerializeToString, + app_dot_marznode_dot_marznode__pb2.BackendConfig.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod - def RestartXray(request, + def RestartBackend(request, target, options=(), channel_credentials=None, @@ -240,14 +332,24 @@ class MarzService(object): wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/marznode.MarzService/RestartXray', - app_dot_marznode_dot_marznode__pb2.XrayConfig.SerializeToString, - app_dot_marznode_dot_marznode__pb2.InboundsResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + return grpc.experimental.unary_unary( + request, + target, + '/marznode.MarzService/RestartBackend', + app_dot_marznode_dot_marznode__pb2.RestartBackendRequest.SerializeToString, + app_dot_marznode_dot_marznode__pb2.Empty.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod - def StreamXrayLogs(request, + def StreamBackendLogs(request, target, options=(), channel_credentials=None, @@ -257,8 +359,45 @@ class MarzService(object): wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_stream(request, target, '/marznode.MarzService/StreamXrayLogs', - app_dot_marznode_dot_marznode__pb2.XrayLogsRequest.SerializeToString, + return grpc.experimental.unary_stream( + request, + target, + '/marznode.MarzService/StreamBackendLogs', + app_dot_marznode_dot_marznode__pb2.BackendLogsRequest.SerializeToString, app_dot_marznode_dot_marznode__pb2.LogLine.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetBackendStats(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/marznode.MarzService/GetBackendStats', + app_dot_marznode_dot_marznode__pb2.Backend.SerializeToString, + app_dot_marznode_dot_marznode__pb2.BackendStats.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/app/models/node.py b/app/models/node.py index b10804d..07b6504 100644 --- a/app/models/node.py +++ b/app/models/node.py @@ -3,6 +3,28 @@ from enum import Enum from pydantic import ConfigDict, BaseModel, Field +class BackendConfigFormat(Enum): + PLAIN = 0 + JSON = 1 + YAML = 2 + + +class BackendConfig(BaseModel): + config: str + format: BackendConfigFormat + + +class BackendStats(BaseModel): + running: bool + + +class Backend(BaseModel): + name: str + backend_type: str + version: str | None + running: bool + + class NodeStatus(str, Enum): healthy = "healthy" unhealthy = "unhealthy" @@ -70,6 +92,7 @@ class NodeResponse(Node): message: str | None = None model_config = ConfigDict(from_attributes=True) inbound_ids: list[int] | None = None + backends: list[Backend] class NodeUsageResponse(BaseModel): diff --git a/app/routes/node.py b/app/routes/node.py index bdaa9f7..ea1f9a9 100644 --- a/app/routes/node.py +++ b/app/routes/node.py @@ -1,4 +1,3 @@ -import json import logging from typing import Annotated @@ -26,6 +25,8 @@ from app.models.node import ( NodeSettings, NodeStatus, NodesUsageResponse, + BackendConfig, + BackendStats, ) logger = logging.getLogger(__name__) @@ -98,9 +99,13 @@ def get_node(node_id: int, db: DBDep, admin: SudoAdminDep): return db_node -@router.websocket("/{node_id}/logs") +@router.websocket("/{node_id}/{backend}/logs") async def node_logs( - node_id: int, websocket: WebSocket, db: DBDep, include_buffer: bool = True + node_id: int, + backend: str, + websocket: WebSocket, + db: DBDep, + include_buffer: bool = True, ): token = websocket.query_params.get("token") or websocket.headers.get( "Authorization", "" @@ -113,13 +118,10 @@ async def node_logs( if not marznode.nodes.get(node_id): return await websocket.close(reason="Node not found", code=4404) - # if not await marznode.nodes[node_id].is_healthy: - # return await websocket.close(reason="Node is not connected", code=4400) - await websocket.accept() try: async for line in marznode.nodes[node_id].get_logs( - include_buffer=include_buffer + name=backend, include_buffer=include_buffer ): try: await websocket.send_text(line) @@ -170,28 +172,52 @@ async def reconnect_node(node_id: int, db: DBDep, admin: SudoAdminDep): return {} -@router.get("/{node_id}/xray_config") -async def get_node_xray_config(node_id: int, db: DBDep, admin: SudoAdminDep): +@router.get("/{node_id}/{backend}/stats", response_model=BackendStats) +async def get_backend_stats( + node_id: int, backend: str, db: DBDep, admin: SudoAdminDep +): if not (node := marznode.nodes.get(node_id)): raise HTTPException(status_code=404, detail="Node not found") try: - config = await node.get_xray_config() - except: + stats = await node.get_backend_stats(backend) + except Exception: + raise HTTPException(502) + else: + return BackendStats(running=stats.running) + + +@router.get("/{node_id}/{backend}/config", response_model=BackendConfig) +async def get_node_xray_config( + node_id: int, backend: str, db: DBDep, admin: SudoAdminDep +): + if not (node := marznode.nodes.get(node_id)): + raise HTTPException(status_code=404, detail="Node not found") + + try: + config, config_format = await node.get_backend_config(name=backend) + except Exception: raise HTTPException(status_code=502, detail="Node isn't responsive") else: - return json.loads(config) + return {"config": config, "format": config_format} -@router.put("/{node_id}/xray_config") +@router.put("/{node_id}/{backend}/config") async def alter_node_xray_config( - node_id: int, db: DBDep, admin: SudoAdminDep, body: Annotated[dict, Body()] + node_id: int, + backend: str, + db: DBDep, + admin: SudoAdminDep, + config: Annotated[BackendConfig, Body()], ): if not (node := marznode.nodes.get(node_id)): raise HTTPException(status_code=404, detail="Node not found") - xray_config = json.dumps(body) try: - await node.restart_xray(xray_config) + await node.restart_backend( + name=backend, + config=config.config, + config_format=config.format.value, + ) except: raise HTTPException(status_code=502, detail="Node isn't responsive") return {} diff --git a/dashboard/public/locales/en.json b/dashboard/public/locales/en.json index 262c627..97aea22 100644 --- a/dashboard/public/locales/en.json +++ b/dashboard/public/locales/en.json @@ -72,6 +72,8 @@ "normal": "Normal", "sudo": "Sudo", "owner": "Owner", + "running": "Running", + "down": "Down", "support-us": { "title": "Support Us", "desc": "Marzneshin is free and open-source software (FOSS), join us and show your support by donating.", diff --git a/dashboard/src/components/settings-dialog/index.tsx b/dashboard/src/components/settings-dialog/index.tsx index f406642..a864338 100644 --- a/dashboard/src/components/settings-dialog/index.tsx +++ b/dashboard/src/components/settings-dialog/index.tsx @@ -1,32 +1,41 @@ -import { FC, PropsWithChildren } from "react"; +import { type FC, type PropsWithChildren, useEffect } from "react"; import { - ScrollArea, - Sheet, - SheetContent, - SheetHeader, - SheetTitle, + ScrollArea, + Sheet, + SheetContent, + SheetHeader, + SheetTitle, } from "@marzneshin/components"; import { useTranslation } from "react-i18next"; export interface SettingsDialogProps { - onOpenChange: (state: boolean) => void; - open: boolean; + onOpenChange: (state: boolean) => void; + open: boolean; + onClose?: () => void; } export const SettingsDialog: FC = ({ - open, onOpenChange, children + open, + onOpenChange, + children, + onClose = () => null, }) => { - const { t } = useTranslation(); - return ( - - - - {t("settings")} - - - {children} - - - - ) -} + const { t } = useTranslation(); + + useEffect(() => { + if (!open) onClose(); + }, [open, onClose]); + + return ( + + + + {t("settings")} + + + {children} + + + + ); +}; diff --git a/dashboard/src/features/admins/components/dialogs/settings/index.tsx b/dashboard/src/features/admins/components/dialogs/settings/index.tsx index 0685275..60b31f8 100644 --- a/dashboard/src/features/admins/components/dialogs/settings/index.tsx +++ b/dashboard/src/features/admins/components/dialogs/settings/index.tsx @@ -1,73 +1,69 @@ import { - // Button, - Tabs, - TabsContent, - TabsList, - TabsTrigger, - Awaiting, - SettingsInfoSkeleton, - SettingsDialogProps, - SettingsDialog, + // Button, + Tabs, + TabsContent, + TabsList, + TabsTrigger, + Awaiting, + SettingsInfoSkeleton, + SettingsDialogProps, + SettingsDialog, } from "@marzneshin/components"; -import { - type AdminType -} from "@marzneshin/features/admins"; -import { type FC, useEffect } from "react"; +import { type AdminType } from "@marzneshin/features/admins"; +import { type FC } from "react"; import { AdminInfoTable } from "./admin-info"; import { useTranslation } from "react-i18next"; interface AdminsSettingsDialogProps extends SettingsDialogProps { - entity: AdminType | null; - onClose: () => void; - isPending: boolean; + entity: AdminType | null; + onClose: () => void; + isPending: boolean; } export const AdminsSettingsDialog: FC = ({ - onOpenChange, - open, - entity, - onClose, - isPending, + onOpenChange, + open, + entity, + onClose, + isPending, }) => { - const { t } = useTranslation(); - - useEffect(() => { - if (!open) onClose(); - }, [open, onClose]); + const { t } = useTranslation(); - return ( - - - - - {t("admin_info")} - - - {t("services")} - - - {t("users")} - - - - - - - {/* */} - - - Users - - - ) : (
Not Found
)} - Skeleton={} - isFetching={isPending} - /> -
- ); + return ( + + + + + {t("admin_info")} + + + {t("services")} + + + {t("users")} + + + + + + + {/* */} + + Users + + ) : ( +
Not Found
+ ) + } + Skeleton={} + isFetching={isPending} + /> +
+ ); }; diff --git a/dashboard/src/features/hosts/dialogs/settings/index.tsx b/dashboard/src/features/hosts/dialogs/settings/index.tsx index 946d31f..ef31776 100644 --- a/dashboard/src/features/hosts/dialogs/settings/index.tsx +++ b/dashboard/src/features/hosts/dialogs/settings/index.tsx @@ -1,41 +1,42 @@ -import { type FC, useEffect } from "react"; +import { type FC } from "react"; import { - SettingsDialogProps, - SettingsDialog, - Table, - TableBody, - TableRowWithCell, -} from "@marzneshin/components" -import { useTranslation } from "react-i18next" -import { HostType } from "../.." + SettingsDialogProps, + SettingsDialog, + Table, + TableBody, + TableRowWithCell, +} from "@marzneshin/components"; +import { useTranslation } from "react-i18next"; +import { HostType } from "../.."; interface HostsSettingsDialogProps extends SettingsDialogProps { - onOpenChange: (state: boolean) => void - onClose: () => void - open: boolean - entity: HostType + onOpenChange: (state: boolean) => void; + onClose: () => void; + open: boolean; + entity: HostType; } -export const HostSettingsDialog: FC = ({ onOpenChange, open, entity, onClose }) => { - const { t } = useTranslation(); +export const HostSettingsDialog: FC = ({ + onOpenChange, + open, + entity, + onClose, +}) => { + const { t } = useTranslation(); - useEffect(() => { - if (!open) onClose(); - }, [open, onClose]); - - return ( - - - - - - - - - - - -
-
- ) -} + return ( + + + + + + + + + + + +
+
+ ); +}; diff --git a/dashboard/src/features/nodes/dialogs/settings/index.tsx b/dashboard/src/features/nodes/dialogs/settings/index.tsx index de5c617..5ceaefd 100644 --- a/dashboard/src/features/nodes/dialogs/settings/index.tsx +++ b/dashboard/src/features/nodes/dialogs/settings/index.tsx @@ -1,65 +1,63 @@ import { - Card, - CardContent, - SettingsDialogProps, - SettingsDialog, - Tabs, - TabsContent, - TabsList, - TabsTrigger, + type SettingsDialogProps, + SettingsDialog, + Tabs, + TabsContent, + TabsList, + TabsTrigger, } from "@marzneshin/components"; -import { type FC, useEffect } from "react"; +import type { FC } from "react"; import { useTranslation } from "react-i18next"; -import type { NodeType } from "../.."; -import { LogContainer } from "../../log"; +import type { NodeBackendType, NodeType } from "../.."; import { NodesDetailTable } from "../../tables/detail-table"; -import { NodeConfigEditor } from "./node-config-editor"; +import { NodeBackendSetting } from "./node-backend-setting"; interface NodesSettingsDialogProps extends SettingsDialogProps { - entity: NodeType; - onClose: () => void; + entity: NodeType; + onClose: () => void; } export const NodesSettingsDialog: FC = ({ - onOpenChange, - open, - entity, - onClose, + onOpenChange, + open, + entity, + onClose, }) => { - const { t } = useTranslation(); + const { t } = useTranslation(); - useEffect(() => { - if (!open) onClose(); - }, [open, onClose]); - - return ( - -
-

- {t("page.nodes.settings.detail")} -

- -
- - - - {t("logs")} - - - {t("config")} - - - - - - - - - - - - - -
- ); + return ( + +
+

+ {t("page.nodes.settings.detail")} +

+ +
+ + + {...entity.backends.map((backend: NodeBackendType) => ( + + {backend.name} + + ))} + + {...entity.backends.map((backend: NodeBackendType) => ( + + + + ))} + +
+ ); }; diff --git a/dashboard/src/features/nodes/dialogs/settings/node-backend-setting.tsx b/dashboard/src/features/nodes/dialogs/settings/node-backend-setting.tsx new file mode 100644 index 0000000..c773f2f --- /dev/null +++ b/dashboard/src/features/nodes/dialogs/settings/node-backend-setting.tsx @@ -0,0 +1,66 @@ +import { + Badge, + Card, + CardContent, + CardHeader, + CardTitle, + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@marzneshin/components"; +import { NodeConfigEditor } from "./node-config-editor"; +import { LogContainer } from "../../log"; +import { useTranslation } from "react-i18next"; +import { type NodeType, useBackendStatsQuery } from "../.."; +import type { FC } from "react"; + +export const NodeBackendSetting: FC<{ node: NodeType; backend: string }> = ({ + node, + backend, +}) => { + const { t } = useTranslation(); + const nodeBackend = node.backends.find((item) => item.name === backend); + const { data: backendStats } = useBackendStatsQuery(node, backend); + return ( + + + {backend} + {nodeBackend?.version && ( + v{nodeBackend.version} + )} + {backendStats === null ? ( + + {t("unkown")} + + ) : backendStats.running ? ( + + {t("running")} + + ) : ( + + {t("down")} + + )} + + + + + + {t("logs")} + + + {t("config")} + + + + + + + + + + + + ); +}; diff --git a/dashboard/src/features/nodes/dialogs/settings/node-config-editor.tsx b/dashboard/src/features/nodes/dialogs/settings/node-config-editor.tsx index 29e743f..f10260c 100644 --- a/dashboard/src/features/nodes/dialogs/settings/node-config-editor.tsx +++ b/dashboard/src/features/nodes/dialogs/settings/node-config-editor.tsx @@ -1,41 +1,44 @@ - -import Editor from '@monaco-editor/react'; +import Editor from "@monaco-editor/react"; import { useTheme } from "@marzneshin/features/theme-switch"; import { useNodesSettings } from "./use-nodes-settings"; -import { Button } from '@marzneshin/components'; -import { useTranslation } from 'react-i18next'; -import { NodeType } from '../..'; - -export const NodeConfigEditor = ({ entity }: { entity: NodeType }) => { - const { t } = useTranslation() - const { theme } = useTheme() - const { - payloadValidity, - data, - handleConfigSave, - handleConfigChange, - handleEditorValidation - } = useNodesSettings(entity) +import { Button } from "@marzneshin/components"; +import { useTranslation } from "react-i18next"; +import type { NodeType } from "../.."; - return (<> - { + const { t } = useTranslation(); + const { theme } = useTheme(); + const { + payloadValidity, + language, + config, + handleConfigSave, + handleConfigChange, + handleEditorValidation, + } = useNodesSettings(entity, backend); - /> - + return ( + <> + + - ) -} + ); +}; diff --git a/dashboard/src/features/nodes/dialogs/settings/use-nodes-settings.ts b/dashboard/src/features/nodes/dialogs/settings/use-nodes-settings.ts index 96d5994..530a1dc 100644 --- a/dashboard/src/features/nodes/dialogs/settings/use-nodes-settings.ts +++ b/dashboard/src/features/nodes/dialogs/settings/use-nodes-settings.ts @@ -1,30 +1,61 @@ import { useState } from "react"; -import { NodeType, useNodesSettingsMutation, useNodesSettingsQuery } from "../.."; +import YAML from "yaml"; +import { + type NodeType, + NodeBackendSettingConfigFormat, + useNodesSettingsMutation, + useNodesSettingsQuery, +} from "../.."; -export const useNodesSettings = (entity: NodeType) => { - const { data } = useNodesSettingsQuery(entity); - const [config, setConfig] = useState(data) - const [payloadValidity, setPayloadValidity] = useState(true) - const mutate = useNodesSettingsMutation(); +const parseConfig = (data: { + config: string; + format: NodeBackendSettingConfigFormat; +}) => { + return { + [NodeBackendSettingConfigFormat.PLAIN]: () => data.config, + [NodeBackendSettingConfigFormat.JSON]: () => + JSON.stringify(JSON.parse(data.config), null, "\t"), + [NodeBackendSettingConfigFormat.YAML]: () => + YAML.stringify(YAML.parse(data.config), null, "\t"), + }[data.format](); +}; - const handleEditorValidation = (markers: any[]) => { - setPayloadValidity(!markers.length) - } +export const useNodesSettings = (entity: NodeType, backend: string) => { + const { data } = useNodesSettingsQuery(entity, backend); + const [config, setConfig] = useState(data.config); + const [payloadValidity, setPayloadValidity] = useState(true); + const mutate = useNodesSettingsMutation(); + const language = { + [NodeBackendSettingConfigFormat.PLAIN]: "text", + [NodeBackendSettingConfigFormat.JSON]: "json", + [NodeBackendSettingConfigFormat.YAML]: "yaml", + }[data.format]; - const handleConfigSave = () => { - mutate.mutate({ node: entity, config }) - } + const handleEditorValidation = (markers: any[]) => { + setPayloadValidity(!markers.length); + }; - const handleConfigChange = (newConfig: string | undefined) => { - if (newConfig) { - try { - const parsedConfig = JSON.parse(newConfig); - setConfig(parsedConfig); - } catch (error) { - setPayloadValidity(false) - } - } - }; + const handleConfigSave = () => { + mutate.mutate({ node: entity, backend, config, format: data.format }); + }; + + const handleConfigChange = (newConfig: string | undefined) => { + if (newConfig) { + try { + setConfig(parseConfig({ config: newConfig, format: data.format })); + setPayloadValidity(true); + } catch (error) { + setPayloadValidity(false); + } + } + }; - return { payloadValidity, data, handleConfigSave, handleConfigChange, handleEditorValidation } -} + return { + payloadValidity, + language, + config: parseConfig(data), + handleConfigSave, + handleConfigChange, + handleEditorValidation, + }; +}; diff --git a/dashboard/src/features/nodes/log/container.tsx b/dashboard/src/features/nodes/log/container.tsx index 90a7a41..cf323f5 100644 --- a/dashboard/src/features/nodes/log/container.tsx +++ b/dashboard/src/features/nodes/log/container.tsx @@ -1,57 +1,55 @@ import { - Badge, - Card, - CardContent, - CardHeader, - ScrollArea, - Separator, + Badge, + Card, + CardContent, + CardHeader, + ScrollArea, + Separator, } from "@marzneshin/components"; import type { NodeType } from ".."; import { useNodesLog } from ".."; -import { cn } from "@marzneshin/utils"; import type { FC } from "react"; import { useTranslation } from "react-i18next"; interface LogContainerProps { - node: NodeType; + node: NodeType; + backend: string; } -export const LogContainer: FC = ({ node }) => { - const { logs, status, logsDiv } = useNodesLog(node); - const { t } = useTranslation(); +export const LogContainer: FC = ({ node, backend }) => { + const { logs, status, logsDiv } = useNodesLog(node, backend); + const { t } = useTranslation(); - return ( - - - -

- {t("page.nodes.settings.log-container.title")} -

-
- - {status} - -
-
- - {logs.map((log: string, index: number) => ( -
-

- {log} -

- {index !== logs.length - 1 && ( - - )} -
- ))} -
-
-
- ); + return ( + + + +

{`${backend} ${t("logs")}`}

+
+ + {status} + +
+
+ + {logs.map((log: string, index: number) => ( +
+

+ {log} +

+ {index !== logs.length - 1 && ( + + )} +
+ ))} +
+
+
+ ); }; diff --git a/dashboard/src/features/nodes/services/backend-stats.query.ts b/dashboard/src/features/nodes/services/backend-stats.query.ts new file mode 100644 index 0000000..bcd5ec8 --- /dev/null +++ b/dashboard/src/features/nodes/services/backend-stats.query.ts @@ -0,0 +1,35 @@ +import type { NodeType } from "@marzneshin/features/nodes"; +import { useQuery } from "@tanstack/react-query"; +import { fetch } from "@marzneshin/utils"; + +type NodeBackendStatsQueryKey = [string, number, string, string]; + +interface NodeBackendStatsQuery { + queryKey: NodeBackendStatsQueryKey; +} + +export async function fetchNodeBackendStats({ + queryKey, +}: NodeBackendStatsQuery): Promise<{ + running: boolean; +} | null> { + return fetch(`/nodes/${Number(queryKey[1])}/${queryKey[2]}/stats`) + .then((stats) => stats) + .catch(() => null); +} + +export const useBackendStatsQuery = (node: NodeType, backend: string) => { + const queryKey: NodeBackendStatsQueryKey = [ + "nodes", + node.id, + backend, + "stats", + ]; + + return useQuery({ + queryKey, + queryFn: fetchNodeBackendStats, + refetchInterval: 1000 * 5, + initialData: null, + }); +}; diff --git a/dashboard/src/features/nodes/services/index.ts b/dashboard/src/features/nodes/services/index.ts index 912979a..ad40cfd 100644 --- a/dashboard/src/features/nodes/services/index.ts +++ b/dashboard/src/features/nodes/services/index.ts @@ -1,8 +1,9 @@ -export * from './nodes.query' -export * from './node.query' -export * from './logs.socket' -export * from './settings.query' -export * from './settings.mutate' -export * from './update-node.mutate' -export * from './create-node.mutate' -export * from './delete-node.mutate' +export * from "./nodes.query"; +export * from "./node.query"; +export * from "./logs.socket"; +export * from "./settings.query"; +export * from "./backend-stats.query"; +export * from "./settings.mutate"; +export * from "./update-node.mutate"; +export * from "./create-node.mutate"; +export * from "./delete-node.mutate"; diff --git a/dashboard/src/features/nodes/services/logs.socket.ts b/dashboard/src/features/nodes/services/logs.socket.ts index be80cfc..6412995 100644 --- a/dashboard/src/features/nodes/services/logs.socket.ts +++ b/dashboard/src/features/nodes/services/logs.socket.ts @@ -4,75 +4,77 @@ import { useAuth } from "@marzneshin/features/auth"; import { joinPaths } from "@tanstack/react-router"; import { useCallback, useEffect, useRef, useState } from "react"; -export const MAX_NUMBER_OF_LOGS = 500 +export const MAX_NUMBER_OF_LOGS = 500; export const getStatus = (status: string) => { - return { - [ReadyState.CONNECTING]: 'connecting', - [ReadyState.OPEN]: 'connected', - [ReadyState.CLOSING]: 'closed', - [ReadyState.CLOSED]: 'closed', - [ReadyState.UNINSTANTIATED]: 'closed', - }[status]; + return { + [ReadyState.CONNECTING]: "connecting", + [ReadyState.OPEN]: "connected", + [ReadyState.CLOSING]: "closed", + [ReadyState.CLOSED]: "closed", + [ReadyState.UNINSTANTIATED]: "closed", + }[status]; }; -export const getWebsocketUrl = (nodeId: number) => { - try { - const baseURL = new URL( - import.meta.env.VITE_BASE_API.startsWith('/') - ? window.location.origin + import.meta.env.VITE_BASE_API - : import.meta.env.VITE_BASE_API - ); - - return ( - (baseURL.protocol === 'https:' ? 'wss://' : 'ws://') + - joinPaths([baseURL.host + baseURL.pathname, `/nodes/${nodeId}/logs`]) + - '?interval=1&token=' + - useAuth.getState().getAuthToken() - ); - } catch (e) { - console.error('Unable to generate websocket url'); - console.error(e); - return null; - } +export const getWebsocketUrl = (nodeId: number, backend: string) => { + try { + const baseURL = new URL( + import.meta.env.VITE_BASE_API.startsWith("/") + ? window.location.origin + import.meta.env.VITE_BASE_API + : import.meta.env.VITE_BASE_API, + ); + const protocol = baseURL.protocol === "https:" ? "wss://" : "ws://"; + return `${protocol}${joinPaths([baseURL.host + baseURL.pathname, `/nodes/${nodeId}/${backend}/logs`])}?interval=1&token=${useAuth.getState().getAuthToken()}`; + } catch (e) { + console.error("Unable to generate websocket url"); + console.error(e); + return null; + } }; +export const useNodesLog = (node: NodeType, backend: string) => { + const [logs, setLogs] = useState([]); + const logsDiv = useRef(null); + const scrollShouldStayOnEnd = useRef(true); -export const useNodesLog = (node: NodeType) => { - const [logs, setLogs] = useState([]); - const logsDiv = useRef(null); - const scrollShouldStayOnEnd = useRef(true); + const updateLogs = useCallback( + (callback: (prevLogs: string[]) => string[]) => { + setLogs((prevLogs) => { + const newLogs = callback(prevLogs); + return newLogs.length > MAX_NUMBER_OF_LOGS + ? newLogs.slice(-MAX_NUMBER_OF_LOGS) + : newLogs; + }); + }, + [], + ); - const updateLogs = useCallback((callback: (prevLogs: string[]) => string[]) => { - setLogs(prevLogs => { - const newLogs = callback(prevLogs); - return newLogs.length > MAX_NUMBER_OF_LOGS ? newLogs.slice(-MAX_NUMBER_OF_LOGS) : newLogs; - }); - }, []); + const { readyState } = useWebSocket( + node?.id ? getWebsocketUrl(node.id, backend) : "", + { + onMessage: (e: any) => { + updateLogs((prevLogs) => [...prevLogs, e.data]); + }, + shouldReconnect: () => true, + reconnectAttempts: 10, + reconnectInterval: 1000, + }, + ); - const { readyState } = useWebSocket(node?.id ? getWebsocketUrl(node.id) : '', { - onMessage: (e: any) => { - updateLogs(prevLogs => [...prevLogs, e.data]); - }, - shouldReconnect: () => true, - reconnectAttempts: 10, - reconnectInterval: 1000, - }); - - useEffect(() => { - if (logsDiv.current && scrollShouldStayOnEnd.current) { - logsDiv.current.scrollTop = logsDiv.current.scrollHeight; - } - }, [logs]); + useEffect(() => { + if (logsDiv.current && scrollShouldStayOnEnd.current) { + logsDiv.current.scrollTop = logsDiv.current.scrollHeight; + } + }, [logs]); - useEffect(() => { - return () => { - // Cleanup - setLogs([]); - }; - }, []); + useEffect(() => { + return () => { + // Cleanup + setLogs([]); + }; + }, []); - const status = getStatus(readyState.toString()); + const status = getStatus(readyState.toString()); - return { status, readyState, logs, logsDiv }; + return { status, readyState, logs, logsDiv }; }; diff --git a/dashboard/src/features/nodes/services/nodes.query.ts b/dashboard/src/features/nodes/services/nodes.query.ts index 7b3d2eb..c752589 100644 --- a/dashboard/src/features/nodes/services/nodes.query.ts +++ b/dashboard/src/features/nodes/services/nodes.query.ts @@ -1,13 +1,17 @@ -import { NodeType } from "@marzneshin/features/nodes"; +import type { NodeType } from "@marzneshin/features/nodes"; import { useQuery } from "@tanstack/react-query"; import { fetch } from "@marzneshin/utils"; -import { +import type { EntityQueryKeyType, - UseEntityQueryProps + UseEntityQueryProps, } from "@marzneshin/features/entity-table"; -export async function fetchNodes({ queryKey }: EntityQueryKeyType): Promise<{ entity: NodeType[], pageCount: number }> { - return fetch(`/nodes?page=${queryKey[1]}&size=${queryKey[2]}&name=${queryKey[3]}`).then((result) => { +export async function fetchNodes({ + queryKey, +}: EntityQueryKeyType): Promise<{ entity: NodeType[]; pageCount: number }> { + return fetch( + `/nodes?page=${queryKey[1]}&size=${queryKey[2]}&name=${queryKey[3]}`, + ).then((result) => { return { entity: result.items, pageCount: result.pages, @@ -17,11 +21,10 @@ export async function fetchNodes({ queryKey }: EntityQueryKeyType): Promise<{ en export const NodesQueryFetchKey = "nodes"; - export const useNodesQuery = ({ page, size, search }: UseEntityQueryProps) => { return useQuery({ queryKey: [NodesQueryFetchKey, page, size, search ? search : ""], queryFn: fetchNodes, initialData: { entity: [], pageCount: 0 }, - }) -} + }); +}; diff --git a/dashboard/src/features/nodes/services/settings.mutate.ts b/dashboard/src/features/nodes/services/settings.mutate.ts index 9d95cd9..db60da3 100644 --- a/dashboard/src/features/nodes/services/settings.mutate.ts +++ b/dashboard/src/features/nodes/services/settings.mutate.ts @@ -1,40 +1,48 @@ -import { NodeType } from "@marzneshin/features/nodes"; +import type { NodeType } from "@marzneshin/features/nodes"; import { useMutation } from "@tanstack/react-query"; import { fetch, queryClient } from "@marzneshin/utils"; import { toast } from "sonner"; import i18n from "@marzneshin/features/i18n"; -import { NodesSettingsQueryFetchKey } from "./settings.query"; +import { + type NodeBackendSettingConfigFormat, + NodesSettingsQueryFetchKey, +} from "./settings.query"; interface UpdateNodesSettings { - node: NodeType - config: string + node: NodeType; + backend: string; + config: string; + format: NodeBackendSettingConfigFormat; } -export async function fetchUpdateNodesSettings({ node, config }: UpdateNodesSettings): Promise { - return fetch(`/nodes/${node.id}/xray_config`, { method: 'put', body: config }).then((node) => { +export async function fetchUpdateNodesSettings({ + node, + backend, + config, + format, +}: UpdateNodesSettings): Promise { + return fetch(`/nodes/${node.id}/${backend}/config`, { + method: "put", + body: { config, format }, + }).then((node) => { return node; }); } const handleError = (error: Error, value: UpdateNodesSettings) => { - toast.error( - i18n.t('events.update.error', { name: value.node.name }), - { - description: error.message - }) -} + toast.error(i18n.t("events.update.error", { name: value.node.name }), { + description: error.message, + }); +}; const handleSuccess = (value: NodeType) => { - toast.success( - i18n.t('events.update.success.title', { name: value.name }), - { - description: i18n.t('events.update.success.desc') - }) - queryClient.invalidateQueries({ queryKey: [NodesSettingsQueryFetchKey] }) -} - + toast.success(i18n.t("events.update.success.title", { name: value.name }), { + description: i18n.t("events.update.success.desc"), + }); + queryClient.invalidateQueries({ queryKey: [NodesSettingsQueryFetchKey] }); +}; -const NodesSettingsUpdateFetchKey = ["nodes", "xray_config"]; +const NodesSettingsUpdateFetchKey = ["nodes", "config"]; export const useNodesSettingsMutation = () => { return useMutation({ @@ -42,5 +50,5 @@ export const useNodesSettingsMutation = () => { mutationFn: fetchUpdateNodesSettings, onError: handleError, onSuccess: handleSuccess, - }) -} + }); +}; diff --git a/dashboard/src/features/nodes/services/settings.query.ts b/dashboard/src/features/nodes/services/settings.query.ts index af92e2b..b1b8f70 100644 --- a/dashboard/src/features/nodes/services/settings.query.ts +++ b/dashboard/src/features/nodes/services/settings.query.ts @@ -1,29 +1,45 @@ -import { NodeType } from "@marzneshin/features/nodes"; +import type { NodeType } from "@marzneshin/features/nodes"; import { useQuery } from "@tanstack/react-query"; import { fetch } from "@marzneshin/utils"; -type NodesSettingsQueryKey = [string, number | null, string] +type NodesSettingsQueryKey = [string, number, string, string]; -export async function fetchNodesSettings({ queryKey }: { queryKey: NodesSettingsQueryKey }): Promise { - return fetch(`/nodes/${Number(queryKey[1])}/xray_config`).then((config) => { - return config; - }); +export enum NodeBackendSettingConfigFormat { + PLAIN = 0, + JSON = 1, + YAML = 2, } -export const NodesSettingsQueryFetchKey = "nodes"; +interface NodesSettingsQuery { + queryKey: NodesSettingsQueryKey; +} -export const useNodesSettingsQuery = (node: NodeType | null) => { - const nodeId: number | null = node?.id ?? null - const queryKey: NodesSettingsQueryKey = - [NodesSettingsQueryFetchKey, nodeId, "xray_config"]; +export async function fetchNodesSettings({ + queryKey, +}: NodesSettingsQuery): Promise<{ + config: string; + format: NodeBackendSettingConfigFormat; +}> { + return fetch(`/nodes/${Number(queryKey[1])}/${queryKey[2]}/config`).then( + (config) => { + return config; + }, + ); +} + +export const NodesSettingsQueryFetchKey = "nodes"; - const queryFn = nodeId !== null - ? ({ queryKey }: { queryKey: [string, number | null, string] }) => fetchNodesSettings({ queryKey }) - : () => Promise.resolve([]); +export const useNodesSettingsQuery = (node: NodeType, backend: string) => { + const queryKey: NodesSettingsQueryKey = [ + NodesSettingsQueryFetchKey, + node.id, + backend, + "config", + ]; return useQuery({ queryKey, - queryFn, - initialData: [], + queryFn: fetchNodesSettings, + initialData: { config: "", format: NodeBackendSettingConfigFormat.PLAIN }, }); }; diff --git a/dashboard/src/features/nodes/types/index.ts b/dashboard/src/features/nodes/types/index.ts index ff65f64..8b51e98 100644 --- a/dashboard/src/features/nodes/types/index.ts +++ b/dashboard/src/features/nodes/types/index.ts @@ -3,43 +3,53 @@ import { PowerOff, Zap, ZapOff } from "lucide-react"; import { z } from "zod"; export const NodesStatus = { - healthy: { - label: "healthy", - icon: Zap, - }, - unhealthy: { - label: "unhealthy", - icon: ZapOff, - }, - disabled: { - label: "disabled", - icon: PowerOff, - }, - none: { - label: "none", - icon: null, - }, + healthy: { + label: "healthy", + icon: Zap, + }, + unhealthy: { + label: "unhealthy", + icon: ZapOff, + }, + disabled: { + label: "disabled", + icon: PowerOff, + }, + none: { + label: "none", + icon: null, + }, } as Record; export const NodeSchema = z.object({ - name: z.string().min(1), - address: z.string().min(1), - port: z - .number() - .min(1) - .or(z.string().transform((v) => Number.parseFloat(v))), - id: z.number().nullable().optional(), - status: z.enum([ - NodesStatus.healthy.label, - NodesStatus.unhealthy.label, - "none", - NodesStatus.disabled.label, - ]), - usage_coefficient: z - .number() - .default(1.0) - .or(z.string().transform((v) => Number.parseFloat(v))), - connection_backend: z.enum(["grpclib", "grpcio"]).default("grpclib"), + name: z.string().min(1), + address: z.string().min(1), + port: z + .number() + .min(1) + .or(z.string().transform((v) => Number.parseFloat(v))), + id: z.number().nullable().optional(), + status: z.enum([ + NodesStatus.healthy.label, + NodesStatus.unhealthy.label, + "none", + NodesStatus.disabled.label, + ]), + usage_coefficient: z + .number() + .default(1.0) + .or(z.string().transform((v) => Number.parseFloat(v))), + connection_backend: z.enum(["grpclib", "grpcio"]).default("grpclib"), }); -export type NodeType = z.infer & { id: number }; +export type NodeBackendType = { + name: string; + backend_type: string; + version: string; + running: boolean; +}; + +export type NodeType = z.infer & { + id: number; + backends: NodeBackendType[]; +}; diff --git a/dashboard/src/features/services/components/dialogs/settings.dialog.tsx b/dashboard/src/features/services/components/dialogs/settings.dialog.tsx index 7daa68c..adaa77c 100644 --- a/dashboard/src/features/services/components/dialogs/settings.dialog.tsx +++ b/dashboard/src/features/services/components/dialogs/settings.dialog.tsx @@ -1,47 +1,50 @@ import { - SettingsDialogProps, - SettingsDialog, - Tabs, - TabsContent, - TabsList, - TabsTrigger, -} from "@marzneshin/components" -import { type FC, useEffect } from "react" -import { useTranslation } from "react-i18next" + SettingsDialogProps, + SettingsDialog, + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@marzneshin/components"; +import { type FC } from "react"; +import { useTranslation } from "react-i18next"; import { - ServicesUsersTable, - ServiceInboundsTable, - ServiceType + ServicesUsersTable, + ServiceInboundsTable, + ServiceType, } from "@marzneshin/features/services"; interface ServicecSettingsDialogProps extends SettingsDialogProps { - entity: ServiceType - onClose: () => void; + entity: ServiceType; + onClose: () => void; } export const ServiceSettingsDialog: FC = ({ - onOpenChange, open, entity, onClose + onOpenChange, + open, + entity, + onClose, }) => { - const { t } = useTranslation(); + const { t } = useTranslation(); - useEffect(() => { - if (!open) onClose(); - }, [open, onClose]); - - return ( - - - - {t('inbounds')} - {t('users')} - - - - - - - - - - ) -} + return ( + + + + + {t("inbounds")} + + + {t("users")} + + + + + + + + + + + ); +}; diff --git a/dashboard/src/features/users/components/dialogs/settings/index.tsx b/dashboard/src/features/users/components/dialogs/settings/index.tsx index 9835530..50f7aaf 100644 --- a/dashboard/src/features/users/components/dialogs/settings/index.tsx +++ b/dashboard/src/features/users/components/dialogs/settings/index.tsx @@ -1,91 +1,94 @@ import { - Button, - ScrollArea, - Tabs, - TabsContent, - TabsList, - TabsTrigger, - Awaiting, - SettingsInfoSkeleton, - SettingsDialogProps, - SettingsDialog, + Button, + ScrollArea, + Tabs, + TabsContent, + TabsList, + TabsTrigger, + Awaiting, + SettingsInfoSkeleton, + SettingsDialogProps, + SettingsDialog, } from "@marzneshin/components"; import { - UserServicesTable, - useUserUsageResetCmd, - type UserType + UserServicesTable, + useUserUsageResetCmd, + type UserType, } from "@marzneshin/features/users"; -import { type FC, useEffect } from "react"; +import { type FC } from "react"; import { useTranslation } from "react-i18next"; import { - UserStatusEnableButton, - SubscriptionActions, - QRCodeSection, - UserInfoTable + UserStatusEnableButton, + SubscriptionActions, + QRCodeSection, + UserInfoTable, } from "./user-info"; interface UsersSettingsDialogProps extends SettingsDialogProps { - entity: UserType | null; - onClose: () => void; - isPending: boolean; + entity: UserType | null; + onClose: () => void; + isPending: boolean; } export const UsersSettingsDialog: FC = ({ - onOpenChange, - open, - entity, - onClose, - isPending, + onOpenChange, + open, + entity, + onClose, + isPending, }) => { - const { t } = useTranslation(); - const { mutate: resetUsage } = useUserUsageResetCmd(); + const { t } = useTranslation(); + const { mutate: resetUsage } = useUserUsageResetCmd(); - useEffect(() => { - if (!open) onClose(); - }, [open, onClose]); - - return ( - - - - - - {t("user_info")} - - - {t("services")} - - - {t("subscription")} - - - - -
- - -
-
- - - - - - - -
- - ) : (
Not Found
)} - Skeleton={} - isFetching={isPending} - /> -
- ); + return ( + + + + + + {t("user_info")} + + + {t("services")} + + + {t("subscription")} + + + + +
+ + +
+
+ + + + + + + +
+ + ) : ( +
Not Found
+ ) + } + Skeleton={} + isFetching={isPending} + /> +
+ ); }; diff --git a/package.json b/package.json new file mode 100644 index 0000000..33c4fee --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "yaml": "^2.5.0" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..fc98ac4 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,24 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + yaml: + specifier: ^2.5.0 + version: 2.5.0 + +packages: + + yaml@2.5.0: + resolution: {integrity: sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==} + engines: {node: '>= 14'} + hasBin: true + +snapshots: + + yaml@2.5.0: {} diff --git a/requirements.txt b/requirements.txt index 0ea3c7d..4a59446 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,7 +21,7 @@ websockets==12.0 PyYAML==6.0.1 aiohttp==3.9.4 xxhash==3.4.1 -grpcio==1.60.0 +grpcio==1.65.2 psycopg==3.1.18 v2share==0.1.0b12 PyJWT~=2.8.0 \ No newline at end of file