fix(host-agent): load configured devices
This commit is contained in:
@@ -12,6 +12,7 @@ CLOUD_MAX_TASK_ATTEMPTS=3
|
|||||||
|
|
||||||
HOST_AGENT_HOST_ID=host-local
|
HOST_AGENT_HOST_ID=host-local
|
||||||
HOST_AGENT_TOKEN=change-me-host-token
|
HOST_AGENT_TOKEN=change-me-host-token
|
||||||
|
HOST_AGENT_TASKS_PATH=./tasks
|
||||||
HOST_AGENT_HEARTBEAT_INTERVAL_SECONDS=30
|
HOST_AGENT_HEARTBEAT_INTERVAL_SECONDS=30
|
||||||
HOST_AGENT_POLL_TIMEOUT_SECONDS=20
|
HOST_AGENT_POLL_TIMEOUT_SECONDS=20
|
||||||
HOST_AGENT_RETRY_BACKOFF_SECONDS=1
|
HOST_AGENT_RETRY_BACKOFF_SECONDS=1
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from dataclasses import dataclass
|
|||||||
|
|
||||||
from cloud.internal_api.models import AssignmentModel
|
from cloud.internal_api.models import AssignmentModel
|
||||||
from device.manager import DeviceManager
|
from device.manager import DeviceManager
|
||||||
|
from driver.registry import build_driver_factory
|
||||||
from host_agent.assignment import AssignmentExecutor
|
from host_agent.assignment import AssignmentExecutor
|
||||||
from host_agent.client import HostAgentClient
|
from host_agent.client import HostAgentClient
|
||||||
from host_agent.config import HostAgentConfig, load_host_agent_config
|
from host_agent.config import HostAgentConfig, load_host_agent_config
|
||||||
@@ -13,6 +14,7 @@ from host_agent.execution import create_execution_factories
|
|||||||
from host_agent.heartbeat import HeartbeatSynchronizer
|
from host_agent.heartbeat import HeartbeatSynchronizer
|
||||||
from host_agent.lease import ActiveAssignmentRunner
|
from host_agent.lease import ActiveAssignmentRunner
|
||||||
from host_agent.processor import AssignmentProcessingResult, AssignmentProcessor
|
from host_agent.processor import AssignmentProcessingResult, AssignmentProcessor
|
||||||
|
from storage.device_config import DeviceConfigStore
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -94,9 +96,10 @@ def create_application(
|
|||||||
*,
|
*,
|
||||||
config: HostAgentConfig | None = None,
|
config: HostAgentConfig | None = None,
|
||||||
manager: DeviceManager | None = None,
|
manager: DeviceManager | None = None,
|
||||||
|
device_config_store: DeviceConfigStore | None = None,
|
||||||
) -> HostAgentApplication:
|
) -> HostAgentApplication:
|
||||||
resolved_config = config or load_host_agent_config()
|
resolved_config = config or load_host_agent_config()
|
||||||
resolved_manager = manager or DeviceManager()
|
resolved_manager = manager or _configured_device_manager(device_config_store)
|
||||||
client = HostAgentClient(resolved_config)
|
client = HostAgentClient(resolved_config)
|
||||||
heartbeat = HeartbeatSynchronizer(resolved_manager, client, resolved_config)
|
heartbeat = HeartbeatSynchronizer(resolved_manager, client, resolved_config)
|
||||||
executor = AssignmentExecutor(create_execution_factories(resolved_manager))
|
executor = AssignmentExecutor(create_execution_factories(resolved_manager))
|
||||||
@@ -106,3 +109,22 @@ def create_application(
|
|||||||
heartbeat=heartbeat,
|
heartbeat=heartbeat,
|
||||||
processor=AssignmentProcessor(client, active_runner),
|
processor=AssignmentProcessor(client, active_runner),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _configured_device_manager(
|
||||||
|
config_store: DeviceConfigStore | None,
|
||||||
|
) -> DeviceManager:
|
||||||
|
manager = DeviceManager()
|
||||||
|
store = config_store or DeviceConfigStore()
|
||||||
|
for device in store.list():
|
||||||
|
manager.register_device(
|
||||||
|
device["device_id"],
|
||||||
|
build_driver_factory(
|
||||||
|
device["driver_type"],
|
||||||
|
device["connection_info"],
|
||||||
|
),
|
||||||
|
name=device["name"],
|
||||||
|
driver_type=device["driver_type"],
|
||||||
|
connection_info=device["connection_info"],
|
||||||
|
)
|
||||||
|
return manager
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from cloud.internal_api.models import AssignmentModel
|
|||||||
from device.manager import DeviceManager
|
from device.manager import DeviceManager
|
||||||
from host_agent.app import HostAgentApplication, create_application
|
from host_agent.app import HostAgentApplication, create_application
|
||||||
from host_agent.config import HostAgentConfig
|
from host_agent.config import HostAgentConfig
|
||||||
|
from storage.device_config import DeviceConfigStore
|
||||||
|
|
||||||
|
|
||||||
def _config() -> HostAgentConfig:
|
def _config() -> HostAgentConfig:
|
||||||
@@ -38,6 +39,32 @@ def test_create_application_composes_host_agent_services(tmp_path, monkeypatch)
|
|||||||
asyncio.run(application.client.aclose())
|
asyncio.run(application.client.aclose())
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_application_loads_persisted_device_configuration(
|
||||||
|
tmp_path,
|
||||||
|
monkeypatch,
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
store = DeviceConfigStore(tmp_path / "devices.sqlite3")
|
||||||
|
store.add(
|
||||||
|
device_id="device-a",
|
||||||
|
name="Lab iPhone",
|
||||||
|
driver_type="wda",
|
||||||
|
connection_info={"url": "http://wda.local"},
|
||||||
|
)
|
||||||
|
|
||||||
|
application = create_application(
|
||||||
|
config=_config(),
|
||||||
|
device_config_store=store,
|
||||||
|
)
|
||||||
|
|
||||||
|
devices = application.heartbeat.manager.list_devices()
|
||||||
|
assert [(device.id, device.name, device.driver_type) for device in devices] == [
|
||||||
|
("device-a", "Lab iPhone", "wda")
|
||||||
|
]
|
||||||
|
assert devices[0].connection_info == {"url": "http://wda.local"}
|
||||||
|
asyncio.run(application.client.aclose())
|
||||||
|
|
||||||
|
|
||||||
def test_shutdown_cancels_long_poll_and_sends_final_heartbeat() -> None:
|
def test_shutdown_cancels_long_poll_and_sends_final_heartbeat() -> None:
|
||||||
async def scenario() -> None:
|
async def scenario() -> None:
|
||||||
claim_started = asyncio.Event()
|
claim_started = asyncio.Event()
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ services:
|
|||||||
HOST_AGENT_RETRY_BACKOFF_SECONDS: ${HOST_AGENT_RETRY_BACKOFF_SECONDS:-1}
|
HOST_AGENT_RETRY_BACKOFF_SECONDS: ${HOST_AGENT_RETRY_BACKOFF_SECONDS:-1}
|
||||||
HOST_AGENT_MAX_RETRY_BACKOFF_SECONDS: ${HOST_AGENT_MAX_RETRY_BACKOFF_SECONDS:-30}
|
HOST_AGENT_MAX_RETRY_BACKOFF_SECONDS: ${HOST_AGENT_MAX_RETRY_BACKOFF_SECONDS:-30}
|
||||||
HOST_AGENT_MAX_RETRY_ATTEMPTS: ${HOST_AGENT_MAX_RETRY_ATTEMPTS:-5}
|
HOST_AGENT_MAX_RETRY_ATTEMPTS: ${HOST_AGENT_MAX_RETRY_ATTEMPTS:-5}
|
||||||
|
volumes:
|
||||||
|
- ${HOST_AGENT_TASKS_PATH:-./tasks}:/app/tasks
|
||||||
depends_on:
|
depends_on:
|
||||||
cloud-api:
|
cloud-api:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ def test_compose_defines_database_control_plane_and_outbound_host_agent() -> Non
|
|||||||
"service_healthy"
|
"service_healthy"
|
||||||
)
|
)
|
||||||
assert "ports" not in services["host-agent"]
|
assert "ports" not in services["host-agent"]
|
||||||
|
assert services["host-agent"]["volumes"] == [
|
||||||
|
"${HOST_AGENT_TASKS_PATH:-./tasks}:/app/tasks"
|
||||||
|
]
|
||||||
assert services["host-agent"]["environment"][
|
assert services["host-agent"]["environment"][
|
||||||
"HOST_AGENT_CONTROL_PLANE_URL"
|
"HOST_AGENT_CONTROL_PLANE_URL"
|
||||||
] == "http://cloud-api:8001"
|
] == "http://cloud-api:8001"
|
||||||
|
|||||||
Reference in New Issue
Block a user