mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-04 06:21:15 +02:00
Merge branch '3.1' into image-manager-out-of-sync
This commit is contained in:
129 files changed
+944
-863
No files matched your search
@@ -14,7 +14,10 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import Column, String, Boolean, DateTime, ForeignKey, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from .base import BaseTable, GUID
|
||||
|
||||
@@ -22,8 +25,10 @@ from .base import BaseTable, GUID
|
||||
class ApiKey(BaseTable):
|
||||
__tablename__ = "api_keys"
|
||||
|
||||
api_key_id = Column(GUID, primary_key=True)
|
||||
user_id = Column(GUID, ForeignKey("users.user_id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
api_key_id: Mapped[uuid.UUID] = mapped_column(GUID, primary_key=True)
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(
|
||||
GUID, ForeignKey("users.user_id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
name = Column(String(128), nullable=False)
|
||||
key_hash = Column(String(128), nullable=False)
|
||||
key_prefix = Column(String(8), nullable=False)
|
||||
|
||||
@@ -16,12 +16,14 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from sqlalchemy import Column, DateTime, func, inspect
|
||||
from sqlalchemy.types import TypeDecorator, CHAR, VARCHAR
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
@@ -105,7 +107,9 @@ class BaseTable(Base):
|
||||
__abstract__ = True
|
||||
|
||||
created_at = Column(DateTime, server_default=func.current_timestamp())
|
||||
updated_at = Column(DateTime, server_default=func.current_timestamp(), onupdate=func.current_timestamp())
|
||||
updated_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime, server_default=func.current_timestamp(), onupdate=func.current_timestamp()
|
||||
)
|
||||
|
||||
__mapper_args__ = {"eager_defaults": True}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from sqlalchemy import Table, Column, String, ForeignKey, BigInteger, Integer, DateTime, JSON, Boolean
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from .base import Base, BaseTable, GUID
|
||||
|
||||
@@ -32,9 +32,9 @@ image_template_map = Table(
|
||||
class Image(BaseTable):
|
||||
__tablename__ = "images"
|
||||
|
||||
image_id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
image_id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
filename = Column(String, index=True)
|
||||
path = Column(String, unique=True)
|
||||
path: Mapped[str] = mapped_column(String, unique=True, nullable=True)
|
||||
image_type = Column(String)
|
||||
image_size = Column(BigInteger)
|
||||
checksum = Column(String, index=True)
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from sqlalchemy import Column, Boolean, ForeignKey, CheckConstraint, Index, Integer, String, text, JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from .base import BaseTable, generate_uuid, GUID
|
||||
|
||||
@@ -36,11 +36,13 @@ class LLMModelConfig(BaseTable):
|
||||
config_id = Column(GUID, primary_key=True, default=generate_uuid)
|
||||
name = Column(String(100), nullable=False) # Configuration name (table-level for indexing)
|
||||
model_type = Column(String(50), nullable=False) # Model type: text, vision, stt, tts, multimodal, etc.
|
||||
config = Column(JSON, nullable=False) # Config fields: provider, base_url, model, temperature, api_key, etc.
|
||||
config: Mapped[dict] = mapped_column(
|
||||
JSON, nullable=False
|
||||
) # Config fields: provider, base_url, model, temperature, api_key, etc.
|
||||
user_id = Column(GUID, ForeignKey("users.user_id", ondelete="CASCADE"), nullable=True)
|
||||
group_id = Column(GUID, ForeignKey("user_groups.user_group_id", ondelete="CASCADE"), nullable=True)
|
||||
is_default = Column(Boolean, default=False, nullable=False)
|
||||
version = Column(Integer, default=0, nullable=False) # Optimistic locking version
|
||||
version: Mapped[int] = mapped_column(Integer, default=0, nullable=False) # Optimistic locking version
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", backref="llm_model_configs")
|
||||
|
||||
@@ -15,8 +15,10 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import Table, Column, String, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from .base import Base, BaseTable, generate_uuid, GUID
|
||||
|
||||
@@ -36,7 +38,7 @@ resource_pool_map = Table(
|
||||
class Resource(BaseTable):
|
||||
__tablename__ = "resources"
|
||||
|
||||
resource_id = Column(GUID, primary_key=True)
|
||||
resource_id: Mapped[uuid.UUID] = mapped_column(GUID, primary_key=True)
|
||||
name = Column(String, unique=True, index=True)
|
||||
resource_type = Column(String)
|
||||
resource_pools = relationship("ResourcePool", secondary=resource_pool_map, back_populates="resources")
|
||||
@@ -45,6 +47,6 @@ class Resource(BaseTable):
|
||||
class ResourcePool(BaseTable):
|
||||
__tablename__ = "resource_pools"
|
||||
|
||||
resource_pool_id = Column(GUID, primary_key=True, default=generate_uuid)
|
||||
resource_pool_id: Mapped[uuid.UUID] = mapped_column(GUID, primary_key=True, default=generate_uuid)
|
||||
name = Column(String, unique=True, index=True)
|
||||
resources = relationship("Resource", secondary=resource_pool_map, back_populates="resource_pools")
|
||||
@@ -15,8 +15,10 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import Column, String, Boolean, event
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from .base import BaseTable, generate_uuid, GUID
|
||||
from .privileges import privilege_role_map
|
||||
@@ -29,7 +31,7 @@ log = logging.getLogger(__name__)
|
||||
class Role(BaseTable):
|
||||
__tablename__ = "roles"
|
||||
|
||||
role_id = Column(GUID, primary_key=True, default=generate_uuid)
|
||||
role_id: Mapped[uuid.UUID] = mapped_column(GUID, primary_key=True, default=generate_uuid)
|
||||
name = Column(String, unique=True, index=True)
|
||||
description = Column(String)
|
||||
is_builtin = Column(Boolean, default=False)
|
||||
|
||||
@@ -16,19 +16,26 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import Boolean, Column, String, Integer, Float, ForeignKey, JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from .base import BaseTable, generate_uuid, GUID
|
||||
from .images import image_template_map
|
||||
|
||||
|
||||
def template_id_column() -> Mapped[uuid.UUID]:
|
||||
return mapped_column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||
|
||||
|
||||
class Template(BaseTable):
|
||||
__tablename__ = "templates"
|
||||
|
||||
template_id = Column(GUID, primary_key=True, default=generate_uuid)
|
||||
template_id: Mapped[uuid.UUID] = mapped_column(GUID, primary_key=True, default=generate_uuid)
|
||||
name = Column(String, index=True)
|
||||
version = Column(String)
|
||||
version: Mapped[Optional[str]] = mapped_column(String)
|
||||
category = Column(String)
|
||||
default_name_format = Column(String)
|
||||
symbol = Column(String)
|
||||
@@ -36,7 +43,7 @@ class Template(BaseTable):
|
||||
usage = Column(String)
|
||||
netmiko_device_type = Column(String)
|
||||
appliance_metadata = Column(JSON)
|
||||
template_type = Column(String)
|
||||
template_type: Mapped[str] = mapped_column(String, nullable=True)
|
||||
tags = Column(JSON)
|
||||
compute_id = Column(String)
|
||||
images = relationship("Image", secondary=image_template_map, back_populates="templates")
|
||||
@@ -50,7 +57,7 @@ class Template(BaseTable):
|
||||
class CloudTemplate(Template):
|
||||
__tablename__ = "cloud_templates"
|
||||
|
||||
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||
template_id: Mapped[uuid.UUID] = template_id_column()
|
||||
ports_mapping = Column(JSON)
|
||||
remote_console_host = Column(String)
|
||||
remote_console_port = Column(Integer)
|
||||
@@ -63,7 +70,7 @@ class CloudTemplate(Template):
|
||||
class DockerTemplate(Template):
|
||||
__tablename__ = "docker_templates"
|
||||
|
||||
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||
template_id: Mapped[uuid.UUID] = template_id_column()
|
||||
image = Column(String)
|
||||
adapters = Column(Integer)
|
||||
mac_address = Column(String)
|
||||
@@ -88,10 +95,10 @@ class DockerTemplate(Template):
|
||||
class DynamipsTemplate(Template):
|
||||
__tablename__ = "dynamips_templates"
|
||||
|
||||
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||
platform = Column(String)
|
||||
template_id: Mapped[uuid.UUID] = template_id_column()
|
||||
platform: Mapped[str] = mapped_column(String, nullable=True)
|
||||
chassis = Column(String)
|
||||
image = Column(String)
|
||||
image: Mapped[str] = mapped_column(String, nullable=True)
|
||||
exec_area = Column(Integer)
|
||||
mmap = Column(Boolean)
|
||||
mac_addr = Column(String)
|
||||
@@ -130,7 +137,7 @@ class DynamipsTemplate(Template):
|
||||
class EthernetHubTemplate(Template):
|
||||
__tablename__ = "ethernet_hub_templates"
|
||||
|
||||
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||
template_id: Mapped[uuid.UUID] = template_id_column()
|
||||
ports_mapping = Column(JSON)
|
||||
|
||||
__mapper_args__ = {"polymorphic_identity": "ethernet_hub", "polymorphic_load": "selectin"}
|
||||
@@ -139,7 +146,7 @@ class EthernetHubTemplate(Template):
|
||||
class EthernetSwitchTemplate(Template):
|
||||
__tablename__ = "ethernet_switch_templates"
|
||||
|
||||
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||
template_id: Mapped[uuid.UUID] = template_id_column()
|
||||
ports_mapping = Column(JSON)
|
||||
console_type = Column(String)
|
||||
|
||||
@@ -149,8 +156,8 @@ class EthernetSwitchTemplate(Template):
|
||||
class IOUTemplate(Template):
|
||||
__tablename__ = "iou_templates"
|
||||
|
||||
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||
path = Column(String)
|
||||
template_id: Mapped[uuid.UUID] = template_id_column()
|
||||
path: Mapped[str] = mapped_column(String, nullable=True)
|
||||
ethernet_adapters = Column(Integer)
|
||||
serial_adapters = Column(Integer)
|
||||
ram = Column(Integer)
|
||||
@@ -168,7 +175,7 @@ class IOUTemplate(Template):
|
||||
class QemuTemplate(Template):
|
||||
__tablename__ = "qemu_templates"
|
||||
|
||||
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||
template_id: Mapped[uuid.UUID] = template_id_column()
|
||||
qemu_path = Column(String)
|
||||
platform = Column(String)
|
||||
linked_clone = Column(Boolean)
|
||||
@@ -214,7 +221,7 @@ class QemuTemplate(Template):
|
||||
class VirtualBoxTemplate(Template):
|
||||
__tablename__ = "virtualbox_templates"
|
||||
|
||||
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||
template_id: Mapped[uuid.UUID] = template_id_column()
|
||||
vmname = Column(String)
|
||||
ram = Column(Integer)
|
||||
linked_clone = Column(Boolean)
|
||||
@@ -236,7 +243,7 @@ class VirtualBoxTemplate(Template):
|
||||
class VMwareTemplate(Template):
|
||||
__tablename__ = "vmware_templates"
|
||||
|
||||
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||
template_id: Mapped[uuid.UUID] = template_id_column()
|
||||
vmx_path = Column(String)
|
||||
linked_clone = Column(Boolean)
|
||||
first_port_name = Column(String)
|
||||
@@ -257,7 +264,7 @@ class VMwareTemplate(Template):
|
||||
class VPCSTemplate(Template):
|
||||
__tablename__ = "vpcs_templates"
|
||||
|
||||
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||
template_id: Mapped[uuid.UUID] = template_id_column()
|
||||
base_script_file = Column(String)
|
||||
console_type = Column(String)
|
||||
console_auto_start = Column(Boolean, default=False)
|
||||
|
||||
@@ -15,8 +15,11 @@
|
||||
# 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 datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import Table, Boolean, Column, Integer, String, DateTime, ForeignKey, event
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from .base import Base, BaseTable, generate_uuid, GUID
|
||||
|
||||
@@ -24,6 +27,7 @@ from gns3server.config import Config
|
||||
from gns3server.services import auth_service
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -43,8 +47,8 @@ class User(BaseTable):
|
||||
email = Column(String, unique=True, index=True)
|
||||
full_name = Column(String)
|
||||
hashed_password = Column(String)
|
||||
last_login = Column(DateTime)
|
||||
token_version = Column(Integer, default=0, nullable=False, server_default="0")
|
||||
last_login: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||||
token_version: Mapped[int] = mapped_column(Integer, default=0, nullable=False, server_default="0")
|
||||
is_active = Column(Boolean, default=True)
|
||||
is_superadmin = Column(Boolean, default=False)
|
||||
groups = relationship("UserGroup", secondary=user_group_map, back_populates="users")
|
||||
@@ -72,7 +76,7 @@ def create_default_super_admin(target, connection, **kw):
|
||||
class UserGroup(BaseTable):
|
||||
__tablename__ = "user_groups"
|
||||
|
||||
user_group_id = Column(GUID, primary_key=True, default=generate_uuid)
|
||||
user_group_id: Mapped[uuid.UUID] = mapped_column(GUID, primary_key=True, default=generate_uuid)
|
||||
name = Column(String, unique=True, index=True)
|
||||
is_builtin = Column(Boolean, default=False)
|
||||
users = relationship("User", secondary=user_group_map, back_populates="groups")
|
||||
|
||||
@@ -15,9 +15,10 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from uuid import UUID
|
||||
from typing import Optional, List
|
||||
from typing import Optional, List, cast
|
||||
from datetime import datetime, timezone
|
||||
from sqlalchemy import select, update, delete, func
|
||||
from sqlalchemy.engine import CursorResult
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
@@ -67,13 +68,13 @@ class ApiKeysRepository(BaseRepository):
|
||||
query = update(models.ApiKey).where(models.ApiKey.api_key_id == api_key_id).values(revoked=True)
|
||||
result = await self._db_session.execute(query)
|
||||
await self._db_session.commit()
|
||||
return result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
|
||||
async def restore_api_key(self, api_key_id: UUID) -> bool:
|
||||
query = update(models.ApiKey).where(models.ApiKey.api_key_id == api_key_id).values(revoked=False)
|
||||
result = await self._db_session.execute(query)
|
||||
await self._db_session.commit()
|
||||
return result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
|
||||
async def update_last_used(self, api_key_id: UUID) -> None:
|
||||
query = update(models.ApiKey).where(models.ApiKey.api_key_id == api_key_id).values(last_used_at=func.now())
|
||||
@@ -84,4 +85,4 @@ class ApiKeysRepository(BaseRepository):
|
||||
query = delete(models.ApiKey).where(models.ApiKey.api_key_id == api_key_id)
|
||||
result = await self._db_session.execute(query)
|
||||
await self._db_session.commit()
|
||||
return result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
@@ -16,9 +16,11 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from uuid import UUID
|
||||
from typing import Optional, List
|
||||
from typing import Optional, List, Union
|
||||
from sqlalchemy import select, update, delete
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.engine import CursorResult
|
||||
from typing import cast
|
||||
|
||||
from .base import BaseRepository
|
||||
|
||||
@@ -31,7 +33,7 @@ class ComputesRepository(BaseRepository):
|
||||
|
||||
super().__init__(db_session)
|
||||
|
||||
async def get_compute(self, compute_id: UUID) -> Optional[models.Compute]:
|
||||
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)
|
||||
@@ -47,7 +49,7 @@ class ComputesRepository(BaseRepository):
|
||||
|
||||
query = select(models.Compute)
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def create_compute(self, compute_create: schemas.ComputeCreate) -> models.Compute:
|
||||
|
||||
@@ -58,14 +60,16 @@ class ComputesRepository(BaseRepository):
|
||||
host=compute_create.host,
|
||||
port=compute_create.port,
|
||||
user=compute_create.user,
|
||||
password=compute_create.password.get_secret_value(),
|
||||
password=compute_create.password.get_secret_value() if compute_create.password else None,
|
||||
)
|
||||
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: UUID, compute_update: schemas.ComputeUpdate) -> Optional[models.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:
|
||||
@@ -80,9 +84,9 @@ class ComputesRepository(BaseRepository):
|
||||
await self._db_session.refresh(compute_db) # force refresh of updated_at value
|
||||
return compute_db
|
||||
|
||||
async def delete_compute(self, compute_id: UUID) -> bool:
|
||||
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 result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
@@ -19,8 +19,9 @@ import os
|
||||
import asyncio
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from typing import Optional, List, Callable
|
||||
from typing import Optional, List, Callable, cast
|
||||
from sqlalchemy import select, delete, update
|
||||
from sqlalchemy.engine import CursorResult
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from gns3server.utils.image_inventory import fingerprint, image_lock, normalized_path
|
||||
@@ -56,7 +57,7 @@ class ImagesRepository(BaseRepository):
|
||||
result = await self._db_session.execute(query.execution_options(populate_existing=refresh))
|
||||
return result.scalars().one_or_none()
|
||||
|
||||
async def get_image_by_checksum(self, checksum: str, image_dir: str = None) -> Optional[models.Image]:
|
||||
async def get_image_by_checksum(self, checksum: str, image_dir: Optional[str] = None) -> Optional[models.Image]:
|
||||
"""
|
||||
Get an image by its checksum.
|
||||
"""
|
||||
@@ -95,9 +96,9 @@ class ImagesRepository(BaseRepository):
|
||||
if availability:
|
||||
query = query.where(models.Image.availability == availability)
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_image_templates(self, image_id: int) -> Optional[List[models.Template]]:
|
||||
async def get_image_templates(self, image_id: int) -> List[models.Template]:
|
||||
"""
|
||||
Get all templates that an image belongs to.
|
||||
"""
|
||||
@@ -105,7 +106,7 @@ class ImagesRepository(BaseRepository):
|
||||
query = select(models.Template).join(models.Template.images).filter(models.Image.image_id == image_id)
|
||||
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def add_image(
|
||||
self, image_name, image_type, image_size, path, checksum, checksum_algorithm, file_fingerprint=None
|
||||
@@ -135,7 +136,7 @@ class ImagesRepository(BaseRepository):
|
||||
await self._db_session.refresh(db_image)
|
||||
return db_image
|
||||
|
||||
async def update_image(self, image_path: str, checksum: str, checksum_algorithm: str) -> models.Image:
|
||||
async def update_image(self, image_path: str, checksum: str, checksum_algorithm: str) -> Optional[models.Image]:
|
||||
"""
|
||||
Update an image.
|
||||
"""
|
||||
@@ -243,9 +244,9 @@ class ImagesRepository(BaseRepository):
|
||||
query = delete(models.Image).where(models.Image.filename == image_name)
|
||||
result = await self._db_session.execute(query)
|
||||
await self._db_session.commit()
|
||||
return result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
|
||||
async def prune_images(self, skip_images: list[str] = None, is_in_use: Optional[Callable] = None) -> int:
|
||||
async def prune_images(self, skip_images: Optional[list[str]] = None, is_in_use: Optional[Callable] = None) -> int:
|
||||
"""
|
||||
Prune images not attached to any template.
|
||||
"""
|
||||
|
||||
@@ -16,8 +16,9 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from uuid import UUID
|
||||
from typing import Optional, List, Dict, Any
|
||||
from typing import Optional, List, Dict, Any, cast
|
||||
from sqlalchemy import select, update, delete, and_
|
||||
from sqlalchemy.engine import CursorResult
|
||||
from datetime import datetime
|
||||
|
||||
import logging
|
||||
@@ -63,7 +64,7 @@ class LLMModelConfigsRepository(BaseRepository):
|
||||
.order_by(models.LLMModelConfig.created_at)
|
||||
)
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_user_default_config(self, user_id: UUID) -> Optional[models.LLMModelConfig]:
|
||||
"""Get a user's default LLM model configuration."""
|
||||
@@ -182,7 +183,7 @@ class LLMModelConfigsRepository(BaseRepository):
|
||||
)
|
||||
result = await self._db_session.execute(query)
|
||||
await self._db_session.commit()
|
||||
return result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
|
||||
async def set_user_default_config(self, user_id: UUID, config_id: UUID) -> bool:
|
||||
"""Set a user's default LLM model configuration."""
|
||||
@@ -202,7 +203,7 @@ class LLMModelConfigsRepository(BaseRepository):
|
||||
)
|
||||
result = await self._db_session.execute(query)
|
||||
await self._db_session.commit()
|
||||
return result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
|
||||
# Group configuration methods
|
||||
|
||||
@@ -222,7 +223,7 @@ class LLMModelConfigsRepository(BaseRepository):
|
||||
.order_by(models.LLMModelConfig.created_at)
|
||||
)
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_group_default_config(self, group_id: UUID) -> Optional[models.LLMModelConfig]:
|
||||
"""Get a group's default LLM model configuration."""
|
||||
@@ -341,7 +342,7 @@ class LLMModelConfigsRepository(BaseRepository):
|
||||
)
|
||||
result = await self._db_session.execute(query)
|
||||
await self._db_session.commit()
|
||||
return result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
|
||||
async def set_group_default_config(self, group_id: UUID, config_id: UUID) -> bool:
|
||||
"""Set a group's default LLM model configuration."""
|
||||
@@ -361,7 +362,7 @@ class LLMModelConfigsRepository(BaseRepository):
|
||||
)
|
||||
result = await self._db_session.execute(query)
|
||||
await self._db_session.commit()
|
||||
return result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
|
||||
# Inheritance methods
|
||||
|
||||
@@ -441,21 +442,21 @@ class LLMModelConfigsRepository(BaseRepository):
|
||||
)
|
||||
|
||||
# Select default_config with proper priority:
|
||||
# 1. User's config marked with is_default: true
|
||||
# 2. Group's config marked with is_default: true
|
||||
# 3. First config in the list (user configs come first)
|
||||
for config in configs_with_source:
|
||||
if config["is_default"] and config["source"] == "user":
|
||||
default_config = config
|
||||
# 1. User's entry marked with is_default: true
|
||||
# 2. Group's entry marked with is_default: true
|
||||
# 3. First entry in the list (user configs come first)
|
||||
for entry in configs_with_source:
|
||||
if entry["is_default"] and entry["source"] == "user":
|
||||
default_config = entry
|
||||
break
|
||||
|
||||
if default_config is None:
|
||||
for config in configs_with_source:
|
||||
if config["is_default"] and config["source"] == "group":
|
||||
default_config = config
|
||||
for entry in configs_with_source:
|
||||
if entry["is_default"] and entry["source"] == "group":
|
||||
default_config = entry
|
||||
break
|
||||
|
||||
# Fallback to first config if no default is marked
|
||||
# Fallback to first entry if no default is marked
|
||||
if default_config is None and configs_with_source:
|
||||
default_config = configs_with_source[0]
|
||||
|
||||
|
||||
@@ -16,8 +16,9 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from uuid import UUID
|
||||
from typing import Optional, List, Union
|
||||
from typing import Optional, List, Union, cast
|
||||
from sqlalchemy import select, update, delete
|
||||
from sqlalchemy.engine import CursorResult
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
@@ -52,7 +53,7 @@ class ResourcePoolsRepository(BaseRepository):
|
||||
|
||||
query = select(models.Resource)
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def create_resource(self, resource: schemas.ResourceCreate) -> models.Resource:
|
||||
"""
|
||||
@@ -75,7 +76,7 @@ class ResourcePoolsRepository(BaseRepository):
|
||||
query = delete(models.Resource).where(models.Resource.resource_id == resource_id)
|
||||
result = await self._db_session.execute(query)
|
||||
await self._db_session.commit()
|
||||
return result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
|
||||
async def get_resource_memberships(self, resource_id: UUID) -> List[models.ResourcePool]:
|
||||
"""
|
||||
@@ -89,7 +90,7 @@ class ResourcePoolsRepository(BaseRepository):
|
||||
)
|
||||
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_resource_pool(self, resource_pool_id: UUID) -> Optional[models.ResourcePool]:
|
||||
"""
|
||||
@@ -116,7 +117,7 @@ class ResourcePoolsRepository(BaseRepository):
|
||||
|
||||
query = select(models.ResourcePool)
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def create_resource_pool(self, resource_pool: schemas.ResourcePoolCreate) -> models.ResourcePool:
|
||||
"""
|
||||
@@ -166,7 +167,7 @@ class ResourcePoolsRepository(BaseRepository):
|
||||
query = delete(models.ResourcePool).where(models.ResourcePool.resource_pool_id == resource_pool_id)
|
||||
result = await self._db_session.execute(query)
|
||||
await self._db_session.commit()
|
||||
return result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
|
||||
async def add_resource_to_pool(
|
||||
self, resource_pool_id: UUID, resource: models.Resource
|
||||
@@ -225,4 +226,4 @@ class ResourcePoolsRepository(BaseRepository):
|
||||
)
|
||||
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
@@ -17,8 +17,9 @@
|
||||
|
||||
from uuid import UUID
|
||||
from urllib.parse import urlparse
|
||||
from typing import Optional, List, Union
|
||||
from typing import Optional, List, Union, cast
|
||||
from sqlalchemy import select, update, delete
|
||||
from sqlalchemy.engine import CursorResult
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
@@ -62,9 +63,9 @@ class RbacRepository(BaseRepository):
|
||||
|
||||
query = select(models.Role).options(selectinload(models.Role.privileges))
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def create_role(self, role_create: schemas.RoleCreate) -> models.Role:
|
||||
async def create_role(self, role_create: schemas.RoleCreate) -> Optional[models.Role]:
|
||||
"""
|
||||
Create a new role.
|
||||
"""
|
||||
@@ -100,7 +101,7 @@ class RbacRepository(BaseRepository):
|
||||
query = delete(models.Role).where(models.Role.role_id == role_id)
|
||||
result = await self._db_session.execute(query)
|
||||
await self._db_session.commit()
|
||||
return result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
|
||||
async def add_privilege_to_role(self, role_id: UUID, privilege: models.Privilege) -> Union[None, models.Role]:
|
||||
"""
|
||||
@@ -149,7 +150,7 @@ class RbacRepository(BaseRepository):
|
||||
query = select(models.Privilege).join(models.Privilege.roles).filter(models.Role.role_id == role_id)
|
||||
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_privilege(self, privilege_id: UUID) -> Optional[models.Privilege]:
|
||||
"""
|
||||
@@ -176,7 +177,7 @@ class RbacRepository(BaseRepository):
|
||||
|
||||
query = select(models.Privilege)
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_ace(self, ace_id: UUID) -> Optional[models.ACE]:
|
||||
"""
|
||||
@@ -203,7 +204,7 @@ class RbacRepository(BaseRepository):
|
||||
|
||||
query = select(models.ACE)
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_aces_for_path(self, path: str) -> List[models.ACE]:
|
||||
"""
|
||||
@@ -218,7 +219,7 @@ class RbacRepository(BaseRepository):
|
||||
.options(selectinload(models.ACE.user), selectinload(models.ACE.group), selectinload(models.ACE.role))
|
||||
)
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def check_ace_exists(self, path: str) -> bool:
|
||||
"""
|
||||
@@ -264,7 +265,7 @@ class RbacRepository(BaseRepository):
|
||||
query = delete(models.ACE).where(models.ACE.ace_id == ace_id)
|
||||
result = await self._db_session.execute(query)
|
||||
await self._db_session.commit()
|
||||
return result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
|
||||
async def delete_all_ace_starting_with_path(self, path: str) -> None:
|
||||
"""
|
||||
@@ -273,7 +274,7 @@ class RbacRepository(BaseRepository):
|
||||
|
||||
query = delete(models.ACE).where(models.ACE.path.startswith(path)).execution_options(synchronize_session=False)
|
||||
result = await self._db_session.execute(query)
|
||||
log.debug(f"{result.rowcount} ACE(s) have been deleted")
|
||||
log.debug(f"{cast(CursorResult, result).rowcount} ACE(s) have been deleted")
|
||||
|
||||
@staticmethod
|
||||
def _check_path_with_aces(path: str, aces) -> bool:
|
||||
@@ -297,7 +298,7 @@ class RbacRepository(BaseRepository):
|
||||
return True # only allow if the path is the original path or the ACE is set to propagate
|
||||
return False
|
||||
|
||||
async def _get_resources_in_pools(self, aces, path: str = None) -> List[models.Resource]:
|
||||
async def _get_resources_in_pools(self, aces, path: Optional[str] = None) -> List[models.Resource]:
|
||||
"""
|
||||
Get all resources in pools.
|
||||
"""
|
||||
@@ -392,7 +393,7 @@ class RbacRepository(BaseRepository):
|
||||
all_resources = result.scalars().all()
|
||||
|
||||
# Precompute pool_id -> set of project_ids
|
||||
pool_to_projects = {}
|
||||
pool_to_projects: dict[str, set[str]] = {}
|
||||
for r in all_resources:
|
||||
if r.resource_type == "project":
|
||||
for pool in r.resource_pools:
|
||||
|
||||
@@ -16,11 +16,13 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import uuid
|
||||
import logging
|
||||
|
||||
from uuid import UUID
|
||||
from typing import List, Union, Optional
|
||||
from typing import List, Union, Optional, cast
|
||||
from sqlalchemy import select, delete
|
||||
from sqlalchemy.engine import CursorResult
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
from sqlalchemy.orm.session import make_transient
|
||||
@@ -66,7 +68,7 @@ class TemplatesRepository(BaseRepository):
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().first()
|
||||
|
||||
async def get_template_by_name_and_version(self, name: str, version: str) -> Union[None, models.Template]:
|
||||
async def get_template_by_name_and_version(self, name: str, version: Optional[str]) -> Union[None, models.Template]:
|
||||
|
||||
query = (
|
||||
select(models.Template)
|
||||
@@ -91,7 +93,7 @@ class TemplatesRepository(BaseRepository):
|
||||
|
||||
query = select(models.Template).options(selectinload(models.Template.images))
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def create_template(self, template_type: str, template_settings: dict) -> models.Template:
|
||||
|
||||
@@ -102,7 +104,7 @@ class TemplatesRepository(BaseRepository):
|
||||
await self._db_session.refresh(db_template)
|
||||
return db_template
|
||||
|
||||
async def update_template(self, db_template: models.Template, template_settings: dict) -> schemas.Template:
|
||||
async def update_template(self, db_template: models.Template, template_settings: dict) -> models.Template:
|
||||
|
||||
# update the fields directly because update() query couldn't work
|
||||
for key, value in template_settings.items():
|
||||
@@ -116,9 +118,9 @@ class TemplatesRepository(BaseRepository):
|
||||
query = delete(models.Template).where(models.Template.template_id == template_id)
|
||||
result = await self._db_session.execute(query)
|
||||
await self._db_session.commit()
|
||||
return result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
|
||||
async def duplicate_template(self, template_id: UUID) -> Optional[schemas.Template]:
|
||||
async def duplicate_template(self, template_id: UUID) -> Optional[models.Template]:
|
||||
|
||||
query = (
|
||||
select(models.Template)
|
||||
@@ -130,7 +132,7 @@ class TemplatesRepository(BaseRepository):
|
||||
# duplicate db object with new primary key (template_id)
|
||||
self._db_session.expunge(db_template)
|
||||
make_transient(db_template)
|
||||
db_template.template_id = None
|
||||
db_template.template_id = uuid.uuid4()
|
||||
self._db_session.add(db_template)
|
||||
await self._db_session.commit()
|
||||
await self._db_session.refresh(db_template)
|
||||
@@ -219,4 +221,4 @@ class TemplatesRepository(BaseRepository):
|
||||
|
||||
query = select(models.Image).join(models.Image.templates).filter(models.Template.template_id == template_id)
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
@@ -16,8 +16,9 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from uuid import UUID
|
||||
from typing import Optional, List, Union
|
||||
from typing import Optional, List, Union, cast
|
||||
from sqlalchemy import select, update, delete, func
|
||||
from sqlalchemy.engine import CursorResult
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
@@ -72,7 +73,7 @@ class UsersRepository(BaseRepository):
|
||||
|
||||
query = select(models.User)
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def create_user(self, user: schemas.UserCreate) -> models.User:
|
||||
"""
|
||||
@@ -88,7 +89,9 @@ class UsersRepository(BaseRepository):
|
||||
await self._db_session.refresh(db_user)
|
||||
return db_user
|
||||
|
||||
async def update_user(self, user_id: UUID, user_update: schemas.UserUpdate) -> Optional[models.User]:
|
||||
async def update_user(
|
||||
self, user_id: UUID, user_update: Union[schemas.UserUpdate, schemas.LoggedInUserUpdate]
|
||||
) -> Optional[models.User]:
|
||||
"""
|
||||
Update a user.
|
||||
"""
|
||||
@@ -129,7 +132,7 @@ class UsersRepository(BaseRepository):
|
||||
query = delete(models.User).where(models.User.user_id == user_id)
|
||||
result = await self._db_session.execute(query)
|
||||
await self._db_session.commit()
|
||||
return result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
|
||||
async def authenticate_user(self, username: str, password: str) -> Optional[models.User]:
|
||||
"""
|
||||
@@ -168,7 +171,7 @@ class UsersRepository(BaseRepository):
|
||||
query = select(models.UserGroup).join(models.UserGroup.users).filter(models.User.user_id == user_id)
|
||||
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_user_group(self, user_group_id: UUID) -> Optional[models.UserGroup]:
|
||||
"""
|
||||
@@ -195,7 +198,7 @@ class UsersRepository(BaseRepository):
|
||||
|
||||
query = select(models.UserGroup)
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def create_user_group(self, user_group: schemas.UserGroupCreate) -> models.UserGroup:
|
||||
"""
|
||||
@@ -233,7 +236,7 @@ class UsersRepository(BaseRepository):
|
||||
query = delete(models.UserGroup).where(models.UserGroup.user_group_id == user_group_id)
|
||||
result = await self._db_session.execute(query)
|
||||
await self._db_session.commit()
|
||||
return result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
|
||||
async def add_member_to_user_group(self, user_group_id: UUID, user: models.User) -> Union[None, models.UserGroup]:
|
||||
"""
|
||||
@@ -285,4 +288,4 @@ class UsersRepository(BaseRepository):
|
||||
query = select(models.User).join(models.User.groups).filter(models.UserGroup.user_group_id == user_group_id)
|
||||
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
@@ -177,7 +177,7 @@ async def disconnect_from_db(app: FastAPI) -> None:
|
||||
log.info(f"Disconnected from database")
|
||||
|
||||
|
||||
async def get_computes(app: FastAPI) -> List[dict]:
|
||||
async def get_computes(app: FastAPI) -> List[schemas.Compute]:
|
||||
|
||||
computes = []
|
||||
async with AsyncSession(app.state._db_engine) as db_session:
|
||||
@@ -199,8 +199,8 @@ async def update_disk_checksums(updated_disks: List[str]) -> None:
|
||||
|
||||
for path in updated_disks:
|
||||
async with image_lock(path):
|
||||
async with AsyncSession(app.state._db_engine, expire_on_commit=False) as session:
|
||||
repository = ImagesRepository(session)
|
||||
async with AsyncSession(app.state._db_engine, expire_on_commit=False) as db_session:
|
||||
repository = ImagesRepository(db_session)
|
||||
image = await repository.get_image(path)
|
||||
if image:
|
||||
info = await read_image_info(path, image.image_type, allow_raw_image=True)
|
||||
@@ -234,7 +234,7 @@ async def get_user_llm_config_full(user_id: str, app: FastAPI) -> Optional[dict]
|
||||
from gns3server.utils.encryption import decrypt, is_encrypted
|
||||
|
||||
try:
|
||||
user_uuid = UUID(user_id) if isinstance(user_id, str) else user_id
|
||||
user_uuid = UUID(user_id)
|
||||
|
||||
async with AsyncSession(app.state._db_engine, expire_on_commit=False) as session:
|
||||
repo = LLMModelConfigsRepository(session)
|
||||
|
||||
Reference in new issue
Block a user