add common models definations
This commit is contained in:
+6
-1
@@ -9,7 +9,12 @@ description = "A utility that allows you to manage your computers from a central
|
||||
readme = "README.md"
|
||||
license = { file = "LICENSE" }
|
||||
requires-python = ">=3.11"
|
||||
dependencies = []
|
||||
dependencies = [
|
||||
"fastapi>=0.115.0",
|
||||
"pydantic>=2.10.0",
|
||||
"sqlalchemy>=2.0.0",
|
||||
"uvicorn[standard]>=0.34.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
from nexus_sync.common.models import (
|
||||
AuditLogRecord,
|
||||
ClientInfo,
|
||||
ClientPlatform,
|
||||
ClientRecord,
|
||||
ClientState,
|
||||
Command,
|
||||
CommandKind,
|
||||
CommandRecord,
|
||||
CommandResult,
|
||||
CommandResultRecord,
|
||||
CommandResultStatus,
|
||||
CommandStatus,
|
||||
HeartbeatRequest,
|
||||
HeartbeatResponse,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AuditLogRecord",
|
||||
"ClientInfo",
|
||||
"ClientPlatform",
|
||||
"ClientRecord",
|
||||
"ClientState",
|
||||
"Command",
|
||||
"CommandKind",
|
||||
"CommandRecord",
|
||||
"CommandResult",
|
||||
"CommandResultRecord",
|
||||
"CommandResultStatus",
|
||||
"CommandStatus",
|
||||
"HeartbeatRequest",
|
||||
"HeartbeatResponse",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
from datetime import datetime
|
||||
from enum import StrEnum
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class StrictBaseModel(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class ClientPlatform(StrEnum):
|
||||
LINUX = "linux"
|
||||
DARWIN = "darwin"
|
||||
WINDOWS = "windows"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
class CommandKind(StrEnum):
|
||||
EXEC = "exec"
|
||||
|
||||
|
||||
class CommandResultStatus(StrEnum):
|
||||
SUCCEEDED = "succeeded"
|
||||
FAILED = "failed"
|
||||
TIMED_OUT = "timed_out"
|
||||
REJECTED = "rejected"
|
||||
|
||||
|
||||
class CommandStatus(StrEnum):
|
||||
PENDING = "pending"
|
||||
DELIVERED = "delivered"
|
||||
SUCCEEDED = "succeeded"
|
||||
FAILED = "failed"
|
||||
TIMED_OUT = "timed_out"
|
||||
REJECTED = "rejected"
|
||||
|
||||
|
||||
class ClientInfo(StrictBaseModel):
|
||||
hostname: str
|
||||
platform: ClientPlatform
|
||||
version: str
|
||||
|
||||
|
||||
class ClientState(StrictBaseModel):
|
||||
local_time: datetime
|
||||
uptime_seconds: int | None = Field(default=None, ge=0)
|
||||
|
||||
|
||||
class CommandResult(StrictBaseModel):
|
||||
command_id: str
|
||||
status: CommandResultStatus
|
||||
started_at: datetime | None = None
|
||||
finished_at: datetime | None = None
|
||||
return_code: int | None = None
|
||||
stdout: str = ""
|
||||
stderr: str = ""
|
||||
|
||||
|
||||
class HeartbeatRequest(StrictBaseModel):
|
||||
client_id: str
|
||||
observed_at: datetime
|
||||
client: ClientInfo
|
||||
state: ClientState
|
||||
last_command_result: CommandResult | None = None
|
||||
|
||||
|
||||
class Command(StrictBaseModel):
|
||||
id: str
|
||||
kind: CommandKind
|
||||
name: str
|
||||
args: dict[str, Any] = Field(default_factory=dict)
|
||||
timeout_seconds: int = Field(gt=0)
|
||||
|
||||
|
||||
class HeartbeatResponse(StrictBaseModel):
|
||||
status: Literal["ok"] = "ok"
|
||||
server_time: datetime
|
||||
next_poll_after_seconds: int = Field(ge=0)
|
||||
command: Command | None = None
|
||||
|
||||
|
||||
class ClientRecord(StrictBaseModel):
|
||||
id: str
|
||||
hostname: str
|
||||
platform: ClientPlatform
|
||||
version: str
|
||||
created_at: datetime
|
||||
last_seen_at: datetime
|
||||
is_active: bool = True
|
||||
token_hash: str | None = None
|
||||
|
||||
|
||||
class CommandRecord(StrictBaseModel):
|
||||
id: str
|
||||
client_id: str
|
||||
kind: CommandKind
|
||||
name: str
|
||||
args: dict[str, Any] = Field(default_factory=dict)
|
||||
status: CommandStatus = CommandStatus.PENDING
|
||||
timeout_seconds: int = Field(gt=0)
|
||||
attempts: int = Field(default=0, ge=0)
|
||||
max_attempts: int = Field(default=1, ge=1)
|
||||
created_at: datetime
|
||||
delivered_at: datetime | None = None
|
||||
finished_at: datetime | None = None
|
||||
|
||||
|
||||
class CommandResultRecord(StrictBaseModel):
|
||||
command_id: str
|
||||
client_id: str
|
||||
status: CommandResultStatus
|
||||
started_at: datetime | None = None
|
||||
finished_at: datetime | None = None
|
||||
return_code: int | None = None
|
||||
stdout: str = ""
|
||||
stderr: str = ""
|
||||
received_at: datetime
|
||||
|
||||
|
||||
class AuditLogRecord(StrictBaseModel):
|
||||
id: str
|
||||
actor: str
|
||||
action: str
|
||||
subject_type: str
|
||||
subject_id: str
|
||||
created_at: datetime
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
@@ -0,0 +1,87 @@
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from pydantic import ValidationError
|
||||
|
||||
from nexus_sync.common import (
|
||||
ClientInfo,
|
||||
ClientPlatform,
|
||||
ClientState,
|
||||
Command,
|
||||
CommandKind,
|
||||
CommandResult,
|
||||
CommandResultStatus,
|
||||
HeartbeatRequest,
|
||||
HeartbeatResponse,
|
||||
)
|
||||
|
||||
|
||||
def test_heartbeat_request_accepts_current_mvp_payload() -> None:
|
||||
payload = HeartbeatRequest(
|
||||
client_id="macbook-pro-01",
|
||||
observed_at=datetime(2026, 5, 24, 13, 20, 30, tzinfo=UTC),
|
||||
client=ClientInfo(
|
||||
hostname="macbook-pro.local",
|
||||
platform=ClientPlatform.DARWIN,
|
||||
version="0.1.0",
|
||||
),
|
||||
state=ClientState(
|
||||
local_time=datetime(2026, 5, 24, 13, 20, 30, tzinfo=UTC),
|
||||
uptime_seconds=1200,
|
||||
),
|
||||
last_command_result=None,
|
||||
)
|
||||
|
||||
serialized = payload.model_dump(mode="json")
|
||||
|
||||
assert serialized["client_id"] == "macbook-pro-01"
|
||||
assert serialized["client"]["platform"] == "darwin"
|
||||
assert serialized["last_command_result"] is None
|
||||
|
||||
|
||||
def test_heartbeat_response_accepts_one_structured_command() -> None:
|
||||
response = HeartbeatResponse(
|
||||
server_time=datetime(2026, 5, 24, 13, 20, 30, tzinfo=UTC),
|
||||
next_poll_after_seconds=10,
|
||||
command=Command(
|
||||
id="cmd_01JY3H8V8W8P3FXDR3S2BM7M6B",
|
||||
kind=CommandKind.EXEC,
|
||||
name="network_interfaces",
|
||||
args={},
|
||||
timeout_seconds=30,
|
||||
),
|
||||
)
|
||||
|
||||
serialized = response.model_dump(mode="json")
|
||||
|
||||
assert serialized["status"] == "ok"
|
||||
assert serialized["command"]["kind"] == "exec"
|
||||
assert serialized["command"]["name"] == "network_interfaces"
|
||||
|
||||
|
||||
def test_command_result_supports_rejected_commands() -> None:
|
||||
result = CommandResult(
|
||||
command_id="cmd_01JY3H8V8W8P3FXDR3S2BM7M6B",
|
||||
status=CommandResultStatus.REJECTED,
|
||||
stderr="unknown command preset",
|
||||
)
|
||||
|
||||
serialized = result.model_dump(mode="json")
|
||||
|
||||
assert serialized["status"] == "rejected"
|
||||
assert serialized["return_code"] is None
|
||||
|
||||
|
||||
def test_models_reject_unknown_fields() -> None:
|
||||
try:
|
||||
Command(
|
||||
id="cmd_01JY3H8V8W8P3FXDR3S2BM7M6B",
|
||||
kind=CommandKind.EXEC,
|
||||
name="network_interfaces",
|
||||
args={},
|
||||
timeout_seconds=30,
|
||||
shell="ip a",
|
||||
)
|
||||
except ValidationError as error:
|
||||
assert "shell" in str(error)
|
||||
else:
|
||||
raise AssertionError("Command model accepted an unknown field")
|
||||
Reference in New Issue
Block a user