YueGuobin 6734d2d666
feat: add auto_connect flag and disconnect endpoint for computes
Computes can now be flagged auto_connect in the database. The controller
connects to flagged computes in the background at startup (reusing the
one-second delayed fire-and-forget mechanism), and enabling the flag via
PUT /computes/{id} triggers a background connection without the request
ever waiting for it. Disabling the flag does not drop an existing
connection.

Add POST /computes/{id}/disconnect to tear a compute down manually: the
notification stream and HTTP session are closed and every reconnection
path (retry backoff, notification stream reconnect, lazy connection in
http_query) is blocked until connect() is called again or the controller
restarts with auto_connect enabled. Disconnecting a compute used by an
opened project is rejected with 409 unless force=true; forcing only
drops the controller-side control channel, nodes keep running unmanaged.
The local compute cannot be disconnected.

Also make test_compute_list create its own compute (it relied on rows
from earlier tests and failed under reordering), and stop scheduling the
delayed add_compute connection during tests: the timer leaked real
connection attempts into other tests' event loops.
2026-10-15 22:50:22 +08:00

97 lines
3.7 KiB
Python

#!/usr/bin/env python
#
# Copyright (C) 2021 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from typing import List, Optional, Union, cast
from uuid import UUID
from sqlalchemy import delete, select, update
from sqlalchemy.engine import CursorResult
from sqlalchemy.ext.asyncio import AsyncSession
import gns3server.db.models as models
from gns3server import schemas
from .base import BaseRepository
class ComputesRepository(BaseRepository):
def __init__(self, db_session: AsyncSession) -> None:
super().__init__(db_session)
async def get_compute(self, compute_id: Union[str, UUID]) -> Optional[models.Compute]:
query = select(models.Compute).where(models.Compute.compute_id == compute_id)
result = await self._db_session.execute(query)
return result.scalars().first()
async def get_compute_by_name(self, name: str) -> Optional[models.Compute]:
query = select(models.Compute).where(models.Compute.name == name)
result = await self._db_session.execute(query)
return result.scalars().first()
async def get_computes(self) -> List[models.Compute]:
query = select(models.Compute)
result = await self._db_session.execute(query)
return list(result.scalars().all())
async def create_compute(self, compute_create: schemas.ComputeCreate) -> models.Compute:
db_compute = models.Compute(
compute_id=compute_create.compute_id,
name=compute_create.name,
protocol=compute_create.protocol,
host=compute_create.host,
port=compute_create.port,
user=compute_create.user,
password=compute_create.password.get_secret_value() if compute_create.password else None,
auto_connect=bool(compute_create.auto_connect),
)
self._db_session.add(db_compute)
await self._db_session.commit()
await self._db_session.refresh(db_compute)
return db_compute
async def update_compute(
self, compute_id: Union[str, UUID], compute_update: schemas.ComputeUpdate
) -> Optional[models.Compute]:
update_values = compute_update.model_dump(exclude_unset=True)
if compute_update.password is not None:
update_values["password"] = compute_update.password.get_secret_value()
if update_values.get("auto_connect") is None:
# the column is NOT NULL: treat an explicit null as "leave unchanged"
update_values.pop("auto_connect", None)
query = update(models.Compute).where(models.Compute.compute_id == compute_id).values(update_values)
await self._db_session.execute(query)
await self._db_session.commit()
compute_db = await self.get_compute(compute_id)
if compute_db:
await self._db_session.refresh(compute_db) # force refresh of updated_at value
return compute_db
async def delete_compute(self, compute_id: Union[str, UUID]) -> bool:
query = delete(models.Compute).where(models.Compute.compute_id == compute_id)
result = await self._db_session.execute(query)
await self._db_session.commit()
return cast(CursorResult, result).rowcount > 0