refactor: ruff autofixes

This commit is contained in:
Mark Paronyan committed 2026-09-27 15:55:10 +03:00
1 parent 2ac637c662
commit a0450b6bce
455 files changed
+12322 -14762

No files matched your search

+3 -4
View File
@@ -26,7 +26,6 @@ log = logging.getLogger(__name__)
class ACE(BaseTable):
__tablename__ = "acl"
ace_id = Column(GUID, primary_key=True, default=generate_uuid)
@@ -34,11 +33,11 @@ class ACE(BaseTable):
path = Column(String)
propagate = Column(Boolean, default=True)
allowed = Column(Boolean, default=True)
user_id = Column(GUID, ForeignKey('users.user_id', ondelete="CASCADE"))
user_id = Column(GUID, ForeignKey("users.user_id", ondelete="CASCADE"))
user = relationship("User", back_populates="acl_entries")
group_id = Column(GUID, ForeignKey('user_groups.user_group_id', ondelete="CASCADE"))
group_id = Column(GUID, ForeignKey("user_groups.user_group_id", ondelete="CASCADE"))
group = relationship("UserGroup", back_populates="acl_entries")
role_id = Column(GUID, ForeignKey('roles.role_id', ondelete="CASCADE"))
role_id = Column(GUID, ForeignKey("roles.role_id", ondelete="CASCADE"))
role = relationship("Role", back_populates="acl_entries")
__table_args__ = (
-1
View File
@@ -20,7 +20,6 @@ from .base import BaseTable, GUID
class ApiKey(BaseTable):
__tablename__ = "api_keys"
api_key_id = Column(GUID, primary_key=True)
+2 -4
View File
@@ -84,7 +84,7 @@ class ListType(TypeDecorator):
impl = VARCHAR
cache_ok = True
def __init__(self, separator=',', *args, **kwargs):
def __init__(self, separator=",", *args, **kwargs):
self._separator = separator
super().__init__(*args, **kwargs)
@@ -92,8 +92,7 @@ class ListType(TypeDecorator):
def process_bind_param(self, value, dialect):
if value is not None:
if any(self._separator in str(item) for item in value):
raise ListException(f"List values cannot contain '{self._separator}'"
f"Please use a different separator.")
raise ListException(f"List values cannot contain '{self._separator}'Please use a different separator.")
return self._separator.join(map(str, value))
def process_result_value(self, value, dialect):
@@ -104,7 +103,6 @@ class ListType(TypeDecorator):
class BaseTable(Base):
__abstract__ = True
created_at = Column(DateTime, server_default=func.current_timestamp())
-1
View File
@@ -21,7 +21,6 @@ from .base import BaseTable, GUID
class Compute(BaseTable):
__tablename__ = "computes"
compute_id = Column(GUID, primary_key=True)
+1 -2
View File
@@ -25,12 +25,11 @@ image_template_map = Table(
"image_template_map",
Base.metadata,
Column("image_id", Integer, ForeignKey("images.image_id", ondelete="CASCADE")),
Column("template_id", GUID, ForeignKey("templates.template_id", ondelete="CASCADE"))
Column("template_id", GUID, ForeignKey("templates.template_id", ondelete="CASCADE")),
)
class Image(BaseTable):
__tablename__ = "images"
image_id = Column(Integer, primary_key=True, autoincrement=True)
+15 -8
View File
@@ -50,21 +50,28 @@ class LLMModelConfig(BaseTable):
__table_args__ = (
# Ensure a config belongs to either a user or a group, not both
CheckConstraint(
"(user_id IS NOT NULL AND group_id IS NULL) OR "
"(user_id IS NULL AND group_id IS NOT NULL)",
name="single_owner_check"
"(user_id IS NOT NULL AND group_id IS NULL) OR (user_id IS NULL AND group_id IS NOT NULL)",
name="single_owner_check",
),
# Validate model_type values
CheckConstraint(
"model_type IN ('text', 'vision', 'stt', 'tts', 'multimodal', 'embedding', 'reranking', 'other')",
name="valid_model_type_check"
name="valid_model_type_check",
),
# Each user can have at most one default config (partial unique index, PostgreSQL only)
Index("unique_user_default", "user_id", unique=True,
postgresql_where=text("is_default = TRUE AND user_id IS NOT NULL")),
Index(
"unique_user_default",
"user_id",
unique=True,
postgresql_where=text("is_default = TRUE AND user_id IS NOT NULL"),
),
# Each group can have at most one default config (partial unique index, PostgreSQL only)
Index("unique_group_default", "group_id", unique=True,
postgresql_where=text("is_default = TRUE AND group_id IS NOT NULL")),
Index(
"unique_group_default",
"group_id",
unique=True,
postgresql_where=text("is_default = TRUE AND group_id IS NOT NULL"),
),
# Indexes for efficient queries
Index("idx_llm_model_configs_user_id", "user_id"),
Index("idx_llm_model_configs_group_id", "group_id"),
+1 -3
View File
@@ -29,12 +29,11 @@ resource_pool_map = Table(
"resource_pool_map",
Base.metadata,
Column("resource_id", GUID, ForeignKey("resources.resource_id", ondelete="CASCADE")),
Column("resource_pool_id", GUID, ForeignKey("resource_pools.resource_pool_id", ondelete="CASCADE"))
Column("resource_pool_id", GUID, ForeignKey("resource_pools.resource_pool_id", ondelete="CASCADE")),
)
class Resource(BaseTable):
__tablename__ = "resources"
resource_id = Column(GUID, primary_key=True)
@@ -44,7 +43,6 @@ class Resource(BaseTable):
class ResourcePool(BaseTable):
__tablename__ = "resource_pools"
resource_pool_id = Column(GUID, primary_key=True, default=generate_uuid)
+60 -216
View File
@@ -29,12 +29,11 @@ privilege_role_map = Table(
"privilege_role_map",
Base.metadata,
Column("privilege_id", GUID, ForeignKey("privileges.privilege_id", ondelete="CASCADE")),
Column("role_id", GUID, ForeignKey("roles.role_id", ondelete="CASCADE"))
Column("role_id", GUID, ForeignKey("roles.role_id", ondelete="CASCADE")),
)
class Privilege(BaseTable):
__tablename__ = "privileges"
privilege_id = Column(GUID, primary_key=True, default=generate_uuid)
@@ -43,210 +42,60 @@ class Privilege(BaseTable):
roles = relationship("Role", secondary=privilege_role_map, back_populates="privileges")
@event.listens_for(Privilege.__table__, 'after_create')
@event.listens_for(Privilege.__table__, "after_create")
def create_default_roles(target, connection, **kw):
default_privileges = [
{
"description": "Create or delete a user",
"name": "User.Allocate"
},
{
"description": "View a user",
"name": "User.Audit"
},
{
"description": "Update a user",
"name": "User.Modify"
},
{
"description": "Create or delete a group",
"name": "Group.Allocate"
},
{
"description": "View a group",
"name": "Group.Audit"
},
{
"description": "Update a group",
"name": "Group.Modify"
},
{
"description": "Create or delete a role",
"name": "Role.Allocate"
},
{
"description": "View a role",
"name": "Role.Audit"
},
{
"description": "Update a role",
"name": "Role.Modify"
},
{
"description": "Create or delete an ACE",
"name": "ACE.Allocate"
},
{
"description": "View an ACE",
"name": "ACE.Audit"
},
{
"description": "Update an ACE",
"name": "ACE.Modify"
},
{
"description": "Create or delete a resource pool",
"name": "Pool.Allocate"
},
{
"description": "View a resource pool",
"name": "Pool.Audit"
},
{
"description": "Update a resource pool",
"name": "Pool.Modify"
},
{
"description": "Create or delete a template",
"name": "Template.Allocate"
},
{
"description": "View a template",
"name": "Template.Audit"
},
{
"description": "Update a template",
"name": "Template.Modify"
},
{
"description": "Create or delete a project",
"name": "Project.Allocate"
},
{
"description": "View a project",
"name": "Project.Audit"
},
{
"description": "Update a project",
"name": "Project.Modify"
},
{
"description": "Create or delete project snapshots",
"name": "Snapshot.Allocate"
},
{
"description": "Restore a snapshot",
"name": "Snapshot.Restore"
},
{
"description": "View a snapshot",
"name": "Snapshot.Audit"
},
{
"description": "Create or delete a node",
"name": "Node.Allocate"
},
{
"description": "View a node",
"name": "Node.Audit"
},
{
"description": "Update a node",
"name": "Node.Modify"
},
{
"description": "Console access to a node",
"name": "Node.Console"
},
{
"description": "Power management for a node",
"name": "Node.PowerMgmt"
},
{
"description": "Create or delete a link",
"name": "Link.Allocate"
},
{
"description": "View a link",
"name": "Link.Audit"
},
{
"description": "Update a link",
"name": "Link.Modify"
},
{
"description": "Capture packets on a link",
"name": "Link.Capture"
},
{
"description": "Create or delete a drawing",
"name": "Drawing.Allocate"
},
{
"description": "View a drawing",
"name": "Drawing.Audit"
},
{
"description": "Update a drawing",
"name": "Drawing.Modify"
},
{
"description": "Create or delete a symbol",
"name": "Symbol.Allocate"
},
{
"description": "View a symbol",
"name": "Symbol.Audit"
},
{
"description": "Create or delete an image",
"name": "Image.Allocate"
},
{
"description": "View an image",
"name": "Image.Audit"
},
{
"description": "Create or delete a compute",
"name": "Compute.Allocate"
},
{
"description": "Update a compute",
"name": "Compute.Modify"
},
{
"description": "View a compute",
"name": "Compute.Audit"
},
{
"description": "Install an appliance",
"name": "Appliance.Allocate"
},
{
"description": "View an appliance",
"name": "Appliance.Audit"
},
{
"description": "Create or delete an LLM model configuration",
"name": "LLMConfig.Allocate"
},
{
"description": "View an LLM model configuration",
"name": "LLMConfig.Audit"
},
{
"description": "Update an LLM model configuration",
"name": "LLMConfig.Modify"
},
{
"description": "View server settings",
"name": "Server.Audit"
},
{
"description": "Update server settings",
"name": "Server.Modify"
}
{"description": "Create or delete a user", "name": "User.Allocate"},
{"description": "View a user", "name": "User.Audit"},
{"description": "Update a user", "name": "User.Modify"},
{"description": "Create or delete a group", "name": "Group.Allocate"},
{"description": "View a group", "name": "Group.Audit"},
{"description": "Update a group", "name": "Group.Modify"},
{"description": "Create or delete a role", "name": "Role.Allocate"},
{"description": "View a role", "name": "Role.Audit"},
{"description": "Update a role", "name": "Role.Modify"},
{"description": "Create or delete an ACE", "name": "ACE.Allocate"},
{"description": "View an ACE", "name": "ACE.Audit"},
{"description": "Update an ACE", "name": "ACE.Modify"},
{"description": "Create or delete a resource pool", "name": "Pool.Allocate"},
{"description": "View a resource pool", "name": "Pool.Audit"},
{"description": "Update a resource pool", "name": "Pool.Modify"},
{"description": "Create or delete a template", "name": "Template.Allocate"},
{"description": "View a template", "name": "Template.Audit"},
{"description": "Update a template", "name": "Template.Modify"},
{"description": "Create or delete a project", "name": "Project.Allocate"},
{"description": "View a project", "name": "Project.Audit"},
{"description": "Update a project", "name": "Project.Modify"},
{"description": "Create or delete project snapshots", "name": "Snapshot.Allocate"},
{"description": "Restore a snapshot", "name": "Snapshot.Restore"},
{"description": "View a snapshot", "name": "Snapshot.Audit"},
{"description": "Create or delete a node", "name": "Node.Allocate"},
{"description": "View a node", "name": "Node.Audit"},
{"description": "Update a node", "name": "Node.Modify"},
{"description": "Console access to a node", "name": "Node.Console"},
{"description": "Power management for a node", "name": "Node.PowerMgmt"},
{"description": "Create or delete a link", "name": "Link.Allocate"},
{"description": "View a link", "name": "Link.Audit"},
{"description": "Update a link", "name": "Link.Modify"},
{"description": "Capture packets on a link", "name": "Link.Capture"},
{"description": "Create or delete a drawing", "name": "Drawing.Allocate"},
{"description": "View a drawing", "name": "Drawing.Audit"},
{"description": "Update a drawing", "name": "Drawing.Modify"},
{"description": "Create or delete a symbol", "name": "Symbol.Allocate"},
{"description": "View a symbol", "name": "Symbol.Audit"},
{"description": "Create or delete an image", "name": "Image.Allocate"},
{"description": "View an image", "name": "Image.Audit"},
{"description": "Create or delete a compute", "name": "Compute.Allocate"},
{"description": "Update a compute", "name": "Compute.Modify"},
{"description": "View a compute", "name": "Compute.Audit"},
{"description": "Install an appliance", "name": "Appliance.Allocate"},
{"description": "View an appliance", "name": "Appliance.Audit"},
{"description": "Create or delete an LLM model configuration", "name": "LLMConfig.Allocate"},
{"description": "View an LLM model configuration", "name": "LLMConfig.Audit"},
{"description": "Update an LLM model configuration", "name": "LLMConfig.Modify"},
{"description": "View server settings", "name": "Server.Audit"},
{"description": "Update server settings", "name": "Server.Modify"},
]
stmt = target.insert().values(default_privileges)
@@ -258,6 +107,7 @@ def create_default_roles(target, connection, **kw):
def add_privileges_to_role(target, connection, role, privileges):
from .roles import Role
roles_table = Role.__table__
privileges_table = Privilege.__table__
@@ -272,10 +122,11 @@ def add_privileges_to_role(target, connection, role, privileges):
connection.execute(stmt)
@event.listens_for(privilege_role_map, 'after_create')
@event.listens_for(privilege_role_map, "after_create")
def add_privileges_to_default_roles(target, connection, **kw):
from .roles import Role
roles_table = Role.__table__
stmt = roles_table.select().where(roles_table.c.name == "Administrator")
result = connection.execute(stmt)
@@ -317,7 +168,7 @@ def add_privileges_to_default_roles(target, connection, **kw):
"Appliance.Allocate",
"Appliance.Audit",
"LLMConfig.Audit",
"LLMConfig.Modify"
"LLMConfig.Modify",
)
add_privileges_to_role(target, connection, "User", user_privileges)
@@ -333,7 +184,7 @@ def add_privileges_to_default_roles(target, connection, **kw):
"Symbol.Audit",
"Image.Audit",
"Compute.Audit",
"Appliance.Audit"
"Appliance.Audit",
)
add_privileges_to_role(target, connection, "Auditor", auditor_privileges)
@@ -348,7 +199,7 @@ def add_privileges_to_default_roles(target, connection, **kw):
"Image.Allocate",
"Image.Audit",
"Appliance.Allocate",
"Appliance.Audit"
"Appliance.Audit",
)
add_privileges_to_role(target, connection, "Template manager", template_manager_privileges)
@@ -360,20 +211,13 @@ def add_privileges_to_default_roles(target, connection, **kw):
"User.Modify",
"Group.Allocate",
"Group.Audit",
"Group.Modify"
"Group.Modify",
)
add_privileges_to_role(target, connection, "User manager", user_manager_privileges)
# add required privileges to the "ACL manager" role
acl_manager_privileges = (
"Role.Allocate",
"Role.Audit",
"Role.Modify",
"ACE.Allocate",
"ACE.Audit",
"ACE.Modify"
)
acl_manager_privileges = ("Role.Allocate", "Role.Audit", "Role.Modify", "ACE.Allocate", "ACE.Audit", "ACE.Modify")
add_privileges_to_role(target, connection, "ACL manager", acl_manager_privileges)
+2 -3
View File
@@ -27,7 +27,6 @@ log = logging.getLogger(__name__)
class Role(BaseTable):
__tablename__ = "roles"
role_id = Column(GUID, primary_key=True, default=generate_uuid)
@@ -38,7 +37,7 @@ class Role(BaseTable):
acl_entries = relationship("ACE")
@event.listens_for(Role.__table__, 'after_create')
@event.listens_for(Role.__table__, "after_create")
def create_default_roles(target, connection, **kw):
default_roles = [
@@ -48,7 +47,7 @@ def create_default_roles(target, connection, **kw):
{"name": "Template manager", "description": "Role to manage templates", "is_builtin": True},
{"name": "User manager", "description": "Role to manage users and groups", "is_builtin": True},
{"name": "ACL manager", "description": "Role to manage other roles and the ACL", "is_builtin": True},
{"name": "No Access", "description": "Role with no privileges (used to forbid access)", "is_builtin": True}
{"name": "No Access", "description": "Role with no privileges (used to forbid access)", "is_builtin": True},
]
stmt = target.insert().values(default_roles)
-11
View File
@@ -24,7 +24,6 @@ from .images import image_template_map
class Template(BaseTable):
__tablename__ = "templates"
template_id = Column(GUID, primary_key=True, default=generate_uuid)
@@ -49,7 +48,6 @@ class Template(BaseTable):
class CloudTemplate(Template):
__tablename__ = "cloud_templates"
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
@@ -63,7 +61,6 @@ class CloudTemplate(Template):
class DockerTemplate(Template):
__tablename__ = "docker_templates"
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
@@ -89,7 +86,6 @@ class DockerTemplate(Template):
class DynamipsTemplate(Template):
__tablename__ = "dynamips_templates"
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
@@ -132,7 +128,6 @@ class DynamipsTemplate(Template):
class EthernetHubTemplate(Template):
__tablename__ = "ethernet_hub_templates"
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
@@ -142,7 +137,6 @@ class EthernetHubTemplate(Template):
class EthernetSwitchTemplate(Template):
__tablename__ = "ethernet_switch_templates"
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
@@ -153,7 +147,6 @@ class EthernetSwitchTemplate(Template):
class IOUTemplate(Template):
__tablename__ = "iou_templates"
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
@@ -173,7 +166,6 @@ class IOUTemplate(Template):
class QemuTemplate(Template):
__tablename__ = "qemu_templates"
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
@@ -220,7 +212,6 @@ class QemuTemplate(Template):
class VirtualBoxTemplate(Template):
__tablename__ = "virtualbox_templates"
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
@@ -243,7 +234,6 @@ class VirtualBoxTemplate(Template):
class VMwareTemplate(Template):
__tablename__ = "vmware_templates"
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
@@ -265,7 +255,6 @@ class VMwareTemplate(Template):
class VPCSTemplate(Template):
__tablename__ = "vpcs_templates"
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
+5 -10
View File
@@ -31,12 +31,11 @@ user_group_map = Table(
"user_group_map",
Base.metadata,
Column("user_id", GUID, ForeignKey("users.user_id", ondelete="CASCADE")),
Column("user_group_id", GUID, ForeignKey("user_groups.user_group_id", ondelete="CASCADE"))
Column("user_group_id", GUID, ForeignKey("user_groups.user_group_id", ondelete="CASCADE")),
)
class User(BaseTable):
__tablename__ = "users"
user_id = Column(GUID, primary_key=True, default=generate_uuid)
@@ -52,7 +51,7 @@ class User(BaseTable):
acl_entries = relationship("ACE")
@event.listens_for(User.__table__, 'after_create')
@event.listens_for(User.__table__, "after_create")
def create_default_super_admin(target, connection, **kw):
config = Config.instance().settings
@@ -63,7 +62,7 @@ def create_default_super_admin(target, connection, **kw):
username=default_admin_username,
full_name="Super Administrator",
hashed_password=hashed_password,
is_superadmin=True
is_superadmin=True,
)
connection.execute(stmt)
connection.commit()
@@ -71,7 +70,6 @@ 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)
@@ -81,13 +79,10 @@ class UserGroup(BaseTable):
acl_entries = relationship("ACE")
@event.listens_for(UserGroup.__table__, 'after_create')
@event.listens_for(UserGroup.__table__, "after_create")
def create_default_user_groups(target, connection, **kw):
default_groups = [
{"name": "Administrators", "is_builtin": True},
{"name": "Users", "is_builtin": True}
]
default_groups = [{"name": "Administrators", "is_builtin": True}, {"name": "Users", "is_builtin": True}]
stmt = target.insert().values(default_groups)
connection.execute(stmt)
+4 -21
View File
@@ -30,7 +30,6 @@ log = logging.getLogger(__name__)
class ApiKeysRepository(BaseRepository):
def __init__(self, db_session: AsyncSession) -> None:
super().__init__(db_session)
@@ -55,11 +54,7 @@ class ApiKeysRepository(BaseRepository):
return result.scalars().first()
async def get_api_keys_by_user(self, user_id: UUID) -> List[models.ApiKey]:
query = (
select(models.ApiKey)
.where(models.ApiKey.user_id == user_id)
.order_by(models.ApiKey.created_at.desc())
)
query = select(models.ApiKey).where(models.ApiKey.user_id == user_id).order_by(models.ApiKey.created_at.desc())
result = await self._db_session.execute(query)
return list(result.scalars().all())
@@ -69,31 +64,19 @@ class ApiKeysRepository(BaseRepository):
return result.scalars().first()
async def revoke_api_key(self, api_key_id: UUID) -> bool:
query = (
update(models.ApiKey)
.where(models.ApiKey.api_key_id == api_key_id)
.values(revoked=True)
)
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
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)
)
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
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())
)
query = update(models.ApiKey).where(models.ApiKey.api_key_id == api_key_id).values(last_used_at=func.now())
await self._db_session.execute(query)
await self._db_session.commit()
+1 -4
View File
@@ -27,7 +27,6 @@ from gns3server import schemas
class ComputesRepository(BaseRepository):
def __init__(self, db_session: AsyncSession) -> None:
super().__init__(db_session)
@@ -72,9 +71,7 @@ class ComputesRepository(BaseRepository):
if compute_update.password is not None:
update_values["password"] = compute_update.password.get_secret_value()
query = update(models.Compute).\
where(models.Compute.compute_id == compute_id).\
values(update_values)
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()
+19 -17
View File
@@ -31,7 +31,6 @@ log = logging.getLogger(__name__)
class ImagesRepository(BaseRepository):
def __init__(self, db_session: AsyncSession) -> None:
super().__init__(db_session)
@@ -43,8 +42,9 @@ class ImagesRepository(BaseRepository):
image_dir, image_name = os.path.split(image_path)
if image_dir:
query = select(models.Image).\
where(models.Image.filename == image_name, models.Image.path.endswith(image_path))
query = select(models.Image).where(
models.Image.filename == image_name, models.Image.path.endswith(image_path)
)
else:
query = select(models.Image).where(models.Image.filename == image_name)
result = await self._db_session.execute(query)
@@ -56,8 +56,9 @@ class ImagesRepository(BaseRepository):
"""
if image_dir:
query = select(models.Image).\
where(models.Image.checksum == checksum, models.Image.path.startswith(image_dir))
query = select(models.Image).where(
models.Image.checksum == checksum, models.Image.path.startswith(image_dir)
)
result = await self._db_session.execute(query)
return result.scalars().one_or_none()
else:
@@ -82,9 +83,7 @@ class ImagesRepository(BaseRepository):
Get all templates that an image belongs to.
"""
query = select(models.Template).\
join(models.Template.images).\
filter(models.Image.image_id == image_id)
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()
@@ -101,7 +100,7 @@ class ImagesRepository(BaseRepository):
image_size=image_size,
path=path,
checksum=checksum,
checksum_algorithm=checksum_algorithm
checksum_algorithm=checksum_algorithm,
)
self._db_session.add(db_image)
@@ -114,9 +113,11 @@ class ImagesRepository(BaseRepository):
Update an image.
"""
query = update(models.Image).\
where(models.Image.path == image_path).\
values(checksum=checksum, checksum_algorithm=checksum_algorithm)
query = (
update(models.Image)
.where(models.Image.path == image_path)
.values(checksum=checksum, checksum_algorithm=checksum_algorithm)
)
await self._db_session.execute(query)
await self._db_session.commit()
@@ -132,9 +133,11 @@ class ImagesRepository(BaseRepository):
image_dir, image_name = os.path.split(image_path)
if image_dir:
query = delete(models.Image).\
where(models.Image.filename == image_name, models.Image.path.endswith(image_path)).\
execution_options(synchronize_session=False)
query = (
delete(models.Image)
.where(models.Image.filename == image_name, models.Image.path.endswith(image_path))
.execution_options(synchronize_session=False)
)
else:
query = delete(models.Image).where(models.Image.filename == image_name)
result = await self._db_session.execute(query)
@@ -146,8 +149,7 @@ class ImagesRepository(BaseRepository):
Prune images not attached to any template.
"""
query = select(models.Image).\
filter(~models.Image.templates.any())
query = select(models.Image).filter(~models.Image.templates.any())
result = await self._db_session.execute(query)
images = result.scalars().all()
images_deleted = 0
+70 -122
View File
@@ -50,44 +50,36 @@ class LLMModelConfigsRepository(BaseRepository):
async def get_user_config(self, config_id: UUID) -> Optional[models.LLMModelConfig]:
"""Get a user's LLM model configuration by ID."""
query = select(models.LLMModelConfig).where(
and_(
models.LLMModelConfig.config_id == config_id,
models.LLMModelConfig.user_id.isnot(None)
)
and_(models.LLMModelConfig.config_id == config_id, models.LLMModelConfig.user_id.isnot(None))
)
result = await self._db_session.execute(query)
return result.scalars().first()
async def get_user_configs(self, user_id: UUID) -> List[models.LLMModelConfig]:
"""Get all LLM model configurations for a user."""
query = select(models.LLMModelConfig).where(
models.LLMModelConfig.user_id == user_id
).order_by(models.LLMModelConfig.created_at)
query = (
select(models.LLMModelConfig)
.where(models.LLMModelConfig.user_id == user_id)
.order_by(models.LLMModelConfig.created_at)
)
result = await self._db_session.execute(query)
return result.scalars().all()
async def get_user_default_config(self, user_id: UUID) -> Optional[models.LLMModelConfig]:
"""Get a user's default LLM model configuration."""
query = select(models.LLMModelConfig).where(
and_(
models.LLMModelConfig.user_id == user_id,
models.LLMModelConfig.is_default
)
and_(models.LLMModelConfig.user_id == user_id, models.LLMModelConfig.is_default)
)
result = await self._db_session.execute(query)
return result.scalars().first()
async def create_user_config(
self,
user_id: UUID,
name: str,
model_type: str,
config_data: Dict[str, Any],
is_default: bool = False
self, user_id: UUID, name: str, model_type: str, config_data: Dict[str, Any], is_default: bool = False
) -> models.LLMModelConfig:
"""Create a new LLM model configuration for a user."""
# Encrypt API key if present
from gns3server.utils.encryption import encrypt
config_to_store = config_data.copy()
if "api_key" in config_to_store and config_to_store["api_key"]:
try:
@@ -105,7 +97,7 @@ class LLMModelConfigsRepository(BaseRepository):
user_id=user_id,
is_default=is_default,
created_at=now,
updated_at=now
updated_at=now,
)
self._db_session.add(db_config)
await self._db_session.commit()
@@ -113,11 +105,7 @@ class LLMModelConfigsRepository(BaseRepository):
return db_config
async def update_user_config(
self,
config_id: UUID,
user_id: UUID,
updates: Dict[str, Any],
expected_version: Optional[int] = None
self, config_id: UUID, user_id: UUID, updates: Dict[str, Any], expected_version: Optional[int] = None
) -> Optional[models.LLMModelConfig]:
"""
Update a user's LLM model configuration.
@@ -130,10 +118,7 @@ class LLMModelConfigsRepository(BaseRepository):
:raises ValueError: If version mismatch (concurrent modification)
"""
query = select(models.LLMModelConfig).where(
and_(
models.LLMModelConfig.config_id == config_id,
models.LLMModelConfig.user_id == user_id
)
and_(models.LLMModelConfig.config_id == config_id, models.LLMModelConfig.user_id == user_id)
)
result = await self._db_session.execute(query)
db_config = result.scalars().first()
@@ -193,10 +178,7 @@ class LLMModelConfigsRepository(BaseRepository):
async def delete_user_config(self, config_id: UUID, user_id: UUID) -> bool:
"""Delete a user's LLM model configuration."""
query = delete(models.LLMModelConfig).where(
and_(
models.LLMModelConfig.config_id == config_id,
models.LLMModelConfig.user_id == user_id
)
and_(models.LLMModelConfig.config_id == config_id, models.LLMModelConfig.user_id == user_id)
)
result = await self._db_session.execute(query)
await self._db_session.commit()
@@ -208,22 +190,16 @@ class LLMModelConfigsRepository(BaseRepository):
# First, unset current default
await self._db_session.execute(
update(models.LLMModelConfig)
.where(
and_(
models.LLMModelConfig.user_id == user_id,
models.LLMModelConfig.is_default
)
)
.where(and_(models.LLMModelConfig.user_id == user_id, models.LLMModelConfig.is_default))
.values(is_default=False, updated_at=now)
)
# Set new default
query = update(models.LLMModelConfig).where(
and_(
models.LLMModelConfig.config_id == config_id,
models.LLMModelConfig.user_id == user_id
)
).values(is_default=True, updated_at=now)
query = (
update(models.LLMModelConfig)
.where(and_(models.LLMModelConfig.config_id == config_id, models.LLMModelConfig.user_id == user_id))
.values(is_default=True, updated_at=now)
)
result = await self._db_session.execute(query)
await self._db_session.commit()
return result.rowcount > 0
@@ -233,44 +209,36 @@ class LLMModelConfigsRepository(BaseRepository):
async def get_group_config(self, config_id: UUID) -> Optional[models.LLMModelConfig]:
"""Get a group's LLM model configuration by ID."""
query = select(models.LLMModelConfig).where(
and_(
models.LLMModelConfig.config_id == config_id,
models.LLMModelConfig.group_id.isnot(None)
)
and_(models.LLMModelConfig.config_id == config_id, models.LLMModelConfig.group_id.isnot(None))
)
result = await self._db_session.execute(query)
return result.scalars().first()
async def get_group_configs(self, group_id: UUID) -> List[models.LLMModelConfig]:
"""Get all LLM model configurations for a group."""
query = select(models.LLMModelConfig).where(
models.LLMModelConfig.group_id == group_id
).order_by(models.LLMModelConfig.created_at)
query = (
select(models.LLMModelConfig)
.where(models.LLMModelConfig.group_id == group_id)
.order_by(models.LLMModelConfig.created_at)
)
result = await self._db_session.execute(query)
return result.scalars().all()
async def get_group_default_config(self, group_id: UUID) -> Optional[models.LLMModelConfig]:
"""Get a group's default LLM model configuration."""
query = select(models.LLMModelConfig).where(
and_(
models.LLMModelConfig.group_id == group_id,
models.LLMModelConfig.is_default
)
and_(models.LLMModelConfig.group_id == group_id, models.LLMModelConfig.is_default)
)
result = await self._db_session.execute(query)
return result.scalars().first()
async def create_group_config(
self,
group_id: UUID,
name: str,
model_type: str,
config_data: Dict[str, Any],
is_default: bool = False
self, group_id: UUID, name: str, model_type: str, config_data: Dict[str, Any], is_default: bool = False
) -> models.LLMModelConfig:
"""Create a new LLM model configuration for a group."""
# Encrypt API key if present
from gns3server.utils.encryption import encrypt
config_to_store = config_data.copy()
if "api_key" in config_to_store and config_to_store["api_key"]:
try:
@@ -288,7 +256,7 @@ class LLMModelConfigsRepository(BaseRepository):
group_id=group_id,
is_default=is_default,
created_at=now,
updated_at=now
updated_at=now,
)
self._db_session.add(db_config)
await self._db_session.commit()
@@ -296,11 +264,7 @@ class LLMModelConfigsRepository(BaseRepository):
return db_config
async def update_group_config(
self,
config_id: UUID,
group_id: UUID,
updates: Dict[str, Any],
expected_version: Optional[int] = None
self, config_id: UUID, group_id: UUID, updates: Dict[str, Any], expected_version: Optional[int] = None
) -> Optional[models.LLMModelConfig]:
"""
Update a group's LLM model configuration.
@@ -313,10 +277,7 @@ class LLMModelConfigsRepository(BaseRepository):
:raises ValueError: If version mismatch (concurrent modification)
"""
query = select(models.LLMModelConfig).where(
and_(
models.LLMModelConfig.config_id == config_id,
models.LLMModelConfig.group_id == group_id
)
and_(models.LLMModelConfig.config_id == config_id, models.LLMModelConfig.group_id == group_id)
)
result = await self._db_session.execute(query)
db_config = result.scalars().first()
@@ -376,10 +337,7 @@ class LLMModelConfigsRepository(BaseRepository):
async def delete_group_config(self, config_id: UUID, group_id: UUID) -> bool:
"""Delete a group's LLM model configuration."""
query = delete(models.LLMModelConfig).where(
and_(
models.LLMModelConfig.config_id == config_id,
models.LLMModelConfig.group_id == group_id
)
and_(models.LLMModelConfig.config_id == config_id, models.LLMModelConfig.group_id == group_id)
)
result = await self._db_session.execute(query)
await self._db_session.commit()
@@ -391,22 +349,16 @@ class LLMModelConfigsRepository(BaseRepository):
# First, unset current default
await self._db_session.execute(
update(models.LLMModelConfig)
.where(
and_(
models.LLMModelConfig.group_id == group_id,
models.LLMModelConfig.is_default
)
)
.where(and_(models.LLMModelConfig.group_id == group_id, models.LLMModelConfig.is_default))
.values(is_default=False, updated_at=now)
)
# Set new default
query = update(models.LLMModelConfig).where(
and_(
models.LLMModelConfig.config_id == config_id,
models.LLMModelConfig.group_id == group_id
)
).values(is_default=True, updated_at=now)
query = (
update(models.LLMModelConfig)
.where(and_(models.LLMModelConfig.config_id == config_id, models.LLMModelConfig.group_id == group_id))
.values(is_default=True, updated_at=now)
)
result = await self._db_session.execute(query)
await self._db_session.commit()
return result.rowcount > 0
@@ -414,10 +366,7 @@ class LLMModelConfigsRepository(BaseRepository):
# Inheritance methods
async def get_user_effective_configs(
self,
user_id: UUID,
current_user_id: Optional[UUID] = None,
current_user_is_superadmin: bool = False
self, user_id: UUID, current_user_id: Optional[UUID] = None, current_user_is_superadmin: bool = False
) -> Dict[str, Any]:
"""
Get user's effective configurations (own + inherited from groups).
@@ -429,9 +378,7 @@ class LLMModelConfigsRepository(BaseRepository):
user_configs = await self.get_user_configs(user_id)
# Get user's groups
query = select(models.UserGroup).\
join(models.UserGroup.users).\
filter(models.User.user_id == user_id)
query = select(models.UserGroup).join(models.UserGroup.users).filter(models.User.user_id == user_id)
result = await self._db_session.execute(query)
user_groups = result.scalars().all()
@@ -453,28 +400,8 @@ class LLMModelConfigsRepository(BaseRepository):
# Always hide API key for security
config_dict = self._hide_api_key(config.config)
configs_with_source.append({
"config_id": config.config_id,
"name": config.name,
"model_type": config.model_type,
"config": config_dict,
"user_id": config.user_id,
"group_id": config.group_id,
"is_default": config.is_default,
"version": config.version,
"created_at": config.created_at,
"updated_at": config.updated_at,
"source": "user",
"group_name": None
})
# Add inherited group configs (always shown, regardless of user configs)
for group_id, configs in group_configs_map.items():
for config in configs:
# Always hide API key for security
config_dict = self._hide_api_key(config.config)
configs_with_source.append({
configs_with_source.append(
{
"config_id": config.config_id,
"name": config.name,
"model_type": config.model_type,
@@ -485,9 +412,33 @@ class LLMModelConfigsRepository(BaseRepository):
"version": config.version,
"created_at": config.created_at,
"updated_at": config.updated_at,
"source": "group",
"group_name": group_names_map[group_id]
})
"source": "user",
"group_name": None,
}
)
# Add inherited group configs (always shown, regardless of user configs)
for group_id, configs in group_configs_map.items():
for config in configs:
# Always hide API key for security
config_dict = self._hide_api_key(config.config)
configs_with_source.append(
{
"config_id": config.config_id,
"name": config.name,
"model_type": config.model_type,
"config": config_dict,
"user_id": config.user_id,
"group_id": config.group_id,
"is_default": config.is_default,
"version": config.version,
"created_at": config.created_at,
"updated_at": config.updated_at,
"source": "group",
"group_name": group_names_map[group_id],
}
)
# Select default_config with proper priority:
# 1. User's config marked with is_default: true
@@ -508,7 +459,4 @@ class LLMModelConfigsRepository(BaseRepository):
if default_config is None and configs_with_source:
default_config = configs_with_source[0]
return {
"configs": configs_with_source,
"default_config": default_config
}
return {"configs": configs_with_source, "default_config": default_config}
+29 -28
View File
@@ -32,7 +32,6 @@ log = logging.getLogger(__name__)
class ResourcePoolsRepository(BaseRepository):
def __init__(self, db_session: AsyncSession) -> None:
super().__init__(db_session)
@@ -61,9 +60,7 @@ class ResourcePoolsRepository(BaseRepository):
"""
db_resource = models.Resource(
resource_id=resource.resource_id,
resource_type=resource.resource_type,
name=resource.name
resource_id=resource.resource_id, resource_type=resource.resource_type, name=resource.name
)
self._db_session.add(db_resource)
await self._db_session.commit()
@@ -85,9 +82,11 @@ class ResourcePoolsRepository(BaseRepository):
Get all resource memberships in resource pools.
"""
query = select(models.ResourcePool).\
join(models.ResourcePool.resources).\
filter(models.Resource.resource_id == resource_id)
query = (
select(models.ResourcePool)
.join(models.ResourcePool.resources)
.filter(models.Resource.resource_id == resource_id)
)
result = await self._db_session.execute(query)
return result.scalars().all()
@@ -131,18 +130,18 @@ class ResourcePoolsRepository(BaseRepository):
return db_resource_pool
async def update_resource_pool(
self,
resource_pool_id: UUID,
resource_pool_update: schemas.ResourcePoolUpdate
self, resource_pool_id: UUID, resource_pool_update: schemas.ResourcePoolUpdate
) -> Optional[models.ResourcePool]:
"""
Update a resource pool.
"""
update_values = resource_pool_update.model_dump(exclude_unset=True)
query = update(models.ResourcePool).\
where(models.ResourcePool.resource_pool_id == resource_pool_id).\
values(update_values)
query = (
update(models.ResourcePool)
.where(models.ResourcePool.resource_pool_id == resource_pool_id)
.values(update_values)
)
await self._db_session.execute(query)
await self._db_session.commit()
@@ -170,17 +169,17 @@ class ResourcePoolsRepository(BaseRepository):
return result.rowcount > 0
async def add_resource_to_pool(
self,
resource_pool_id: UUID,
resource: models.Resource
self, resource_pool_id: UUID, resource: models.Resource
) -> Union[None, models.ResourcePool]:
"""
Add a resource to a resource pool.
"""
query = select(models.ResourcePool).\
options(selectinload(models.ResourcePool.resources)).\
where(models.ResourcePool.resource_pool_id == resource_pool_id)
query = (
select(models.ResourcePool)
.options(selectinload(models.ResourcePool.resources))
.where(models.ResourcePool.resource_pool_id == resource_pool_id)
)
result = await self._db_session.execute(query)
resource_pool_db = result.scalars().first()
if not resource_pool_db:
@@ -192,17 +191,17 @@ class ResourcePoolsRepository(BaseRepository):
return resource_pool_db
async def remove_resource_from_pool(
self,
resource_pool_id: UUID,
resource: models.Resource
self, resource_pool_id: UUID, resource: models.Resource
) -> Union[None, models.ResourcePool]:
"""
Remove a resource from a resource pool.
"""
query = select(models.ResourcePool).\
options(selectinload(models.ResourcePool.resources)).\
where(models.ResourcePool.resource_pool_id == resource_pool_id)
query = (
select(models.ResourcePool)
.options(selectinload(models.ResourcePool.resources))
.where(models.ResourcePool.resource_pool_id == resource_pool_id)
)
result = await self._db_session.execute(query)
resource_pool_db = result.scalars().first()
if not resource_pool_db:
@@ -219,9 +218,11 @@ class ResourcePoolsRepository(BaseRepository):
Get all resources from a resource pool.
"""
query = select(models.Resource).\
join(models.Resource.resource_pools).\
filter(models.ResourcePool.resource_pool_id == resource_pool_id)
query = (
select(models.Resource)
.join(models.Resource.resource_pools)
.filter(models.ResourcePool.resource_pool_id == resource_pool_id)
)
result = await self._db_session.execute(query)
return result.scalars().all()
+52 -83
View File
@@ -33,7 +33,6 @@ log = logging.getLogger(__name__)
class RbacRepository(BaseRepository):
def __init__(self, db_session: AsyncSession) -> None:
super().__init__(db_session)
@@ -43,9 +42,7 @@ class RbacRepository(BaseRepository):
Get a role by its ID.
"""
query = select(models.Role).\
options(selectinload(models.Role.privileges)).\
where(models.Role.role_id == role_id)
query = select(models.Role).options(selectinload(models.Role.privileges)).where(models.Role.role_id == role_id)
result = await self._db_session.execute(query)
return result.scalars().first()
@@ -54,9 +51,7 @@ class RbacRepository(BaseRepository):
Get a role by its name.
"""
query = select(models.Role).\
options(selectinload(models.Role.privileges)).\
where(models.Role.name == name)
query = select(models.Role).options(selectinload(models.Role.privileges)).where(models.Role.name == name)
result = await self._db_session.execute(query)
return result.scalars().first()
@@ -82,19 +77,13 @@ class RbacRepository(BaseRepository):
await self._db_session.commit()
return await self.get_role(db_role.role_id)
async def update_role(
self,
role_id: UUID,
role_update: schemas.RoleUpdate
) -> Optional[models.Role]:
async def update_role(self, role_id: UUID, role_update: schemas.RoleUpdate) -> Optional[models.Role]:
"""
Update a role.
"""
update_values = role_update.model_dump(exclude_unset=True)
query = update(models.Role).\
where(models.Role.role_id == role_id).\
values(update_values)
query = update(models.Role).where(models.Role.role_id == role_id).values(update_values)
await self._db_session.execute(query)
await self._db_session.commit()
@@ -113,18 +102,12 @@ class RbacRepository(BaseRepository):
await self._db_session.commit()
return result.rowcount > 0
async def add_privilege_to_role(
self,
role_id: UUID,
privilege: models.Privilege
) -> Union[None, models.Role]:
async def add_privilege_to_role(self, role_id: UUID, privilege: models.Privilege) -> Union[None, models.Role]:
"""
Add a privilege to a role.
"""
query = select(models.Role).\
options(selectinload(models.Role.privileges)).\
where(models.Role.role_id == role_id)
query = select(models.Role).options(selectinload(models.Role.privileges)).where(models.Role.role_id == role_id)
result = await self._db_session.execute(query)
role_db = result.scalars().first()
if not role_db:
@@ -142,18 +125,12 @@ class RbacRepository(BaseRepository):
await self._db_session.refresh(role_db)
return role_db
async def remove_privilege_from_role(
self,
role_id: UUID,
privilege: models.Privilege
) -> Union[None, models.Role]:
async def remove_privilege_from_role(self, role_id: UUID, privilege: models.Privilege) -> Union[None, models.Role]:
"""
Remove a privilege from a role.
"""
query = select(models.Role).\
options(selectinload(models.Role.privileges)).\
where(models.Role.role_id == role_id)
query = select(models.Role).options(selectinload(models.Role.privileges)).where(models.Role.role_id == role_id)
result = await self._db_session.execute(query)
role_db = result.scalars().first()
if not role_db:
@@ -169,9 +146,7 @@ class RbacRepository(BaseRepository):
Get all the role privileges.
"""
query = select(models.Privilege).\
join(models.Privilege.roles).\
filter(models.Role.role_id == role_id)
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()
@@ -237,15 +212,11 @@ class RbacRepository(BaseRepository):
This method includes related user, group, and role information.
"""
query = select(models.ACE).\
where(
(models.ACE.path == path) | (models.ACE.path.startswith(path + "/"))
).\
options(
selectinload(models.ACE.user),
selectinload(models.ACE.group),
selectinload(models.ACE.role)
)
query = (
select(models.ACE)
.where((models.ACE.path == path) | (models.ACE.path.startswith(path + "/")))
.options(selectinload(models.ACE.user), selectinload(models.ACE.group), selectinload(models.ACE.role))
)
result = await self._db_session.execute(query)
return result.scalars().all()
@@ -254,8 +225,7 @@ class RbacRepository(BaseRepository):
Check if an ACE exists.
"""
query = select(models.ACE).\
where(models.ACE.path == path)
query = select(models.ACE).where(models.ACE.path == path)
result = await self._db_session.execute(query)
return result.scalars().first() is not None
@@ -271,19 +241,13 @@ class RbacRepository(BaseRepository):
await self._db_session.refresh(db_ace)
return db_ace
async def update_ace(
self,
ace_id: UUID,
ace_update: schemas.ACEUpdate
) -> Optional[models.ACE]:
async def update_ace(self, ace_id: UUID, ace_update: schemas.ACEUpdate) -> Optional[models.ACE]:
"""
Update an ACE
"""
update_values = ace_update.model_dump(exclude_unset=True)
query = update(models.ACE).\
where(models.ACE.ace_id == ace_id).\
values(update_values)
query = update(models.ACE).where(models.ACE.ace_id == ace_id).values(update_values)
await self._db_session.execute(query)
await self._db_session.commit()
@@ -307,9 +271,7 @@ class RbacRepository(BaseRepository):
Delete all ACEs starting with path.
"""
query = delete(models.ACE).\
where(models.ACE.path.startswith(path)).\
execution_options(synchronize_session=False)
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")
@@ -344,9 +306,11 @@ class RbacRepository(BaseRepository):
for ace_path, ace_propagate, ace_allowed, ace_privilege in aces:
if ace_path.startswith("/pool"):
resource_pool_id = ace_path.split("/")[2]
query = select(models.Resource). \
join(models.Resource.resource_pools). \
filter(models.ResourcePool.resource_pool_id == resource_pool_id)
query = (
select(models.Resource)
.join(models.Resource.resource_pools)
.filter(models.ResourcePool.resource_pool_id == resource_pool_id)
)
result = await self._db_session.execute(query)
resources = result.scalars().all()
@@ -366,13 +330,15 @@ class RbacRepository(BaseRepository):
Retrieve all user ACEs matching the user_id and privilege name.
"""
query = select(models.ACE.path, models.ACE.propagate, models.ACE.allowed, models.Privilege.name).\
join(models.Privilege.roles).\
join(models.Role.acl_entries).\
join(models.ACE.user). \
filter(models.User.user_id == user_id).\
filter(models.Privilege.name == privilege_name).\
order_by(models.ACE.path.desc())
query = (
select(models.ACE.path, models.ACE.propagate, models.ACE.allowed, models.Privilege.name)
.join(models.Privilege.roles)
.join(models.Role.acl_entries)
.join(models.ACE.user)
.filter(models.User.user_id == user_id)
.filter(models.Privilege.name == privilege_name)
.order_by(models.ACE.path.desc())
)
result = await self._db_session.execute(query)
return result.all()
@@ -382,13 +348,15 @@ class RbacRepository(BaseRepository):
Retrieve all group ACEs matching the user_id and privilege name.
"""
query = select(models.ACE.path, models.ACE.propagate, models.ACE.allowed, models.Privilege.name). \
join(models.Privilege.roles). \
join(models.Role.acl_entries). \
join(models.ACE.group). \
join(models.UserGroup.users).\
filter(models.User.user_id == user_id). \
filter(models.Privilege.name == privilege_name)
query = (
select(models.ACE.path, models.ACE.propagate, models.ACE.allowed, models.Privilege.name)
.join(models.Privilege.roles)
.join(models.Role.acl_entries)
.join(models.ACE.group)
.join(models.UserGroup.users)
.filter(models.User.user_id == user_id)
.filter(models.Privilege.name == privilege_name)
)
result = await self._db_session.execute(query)
return result.all()
@@ -404,12 +372,7 @@ class RbacRepository(BaseRepository):
pool_resources.extend(await self._get_resources_in_pools(group_aces))
return list(set(pool_resources))
async def get_accessible_project_ids(
self,
user_id: UUID,
privilege_name: str,
all_project_ids: List[str]
):
async def get_accessible_project_ids(self, user_id: UUID, privilege_name: str, all_project_ids: List[str]):
"""
Batch check which projects a user can access via direct ACE or resource pools.
Performs 3 fixed DB queries regardless of project count.
@@ -448,8 +411,11 @@ class RbacRepository(BaseRepository):
user_denied.add(pid)
# --- User ACE: pool check ---
user_pool_ids = {ace_path.split("/")[2] for ace_path, _, ace_allowed, _ in user_aces
if ace_path.startswith("/pools/") and ace_allowed}
user_pool_ids = {
ace_path.split("/")[2]
for ace_path, _, ace_allowed, _ in user_aces
if ace_path.startswith("/pools/") and ace_allowed
}
pool_accessible_ids = set()
for pool_id, project_ids in pool_to_projects.items():
@@ -470,8 +436,11 @@ class RbacRepository(BaseRepository):
pass
# --- Group ACE: pool check ---
group_pool_ids = {ace_path.split("/")[2] for ace_path, _, ace_allowed, _ in group_aces
if ace_path.startswith("/pools/") and ace_allowed}
group_pool_ids = {
ace_path.split("/")[2]
for ace_path, _, ace_allowed, _ in group_aces
if ace_path.startswith("/pools/") and ace_allowed
}
for pool_id, project_ids in pool_to_projects.items():
if pool_id in group_pool_ids:
+34 -34
View File
@@ -47,7 +47,6 @@ TEMPLATE_TYPE_TO_MODEL = {
class TemplatesRepository(BaseRepository):
def __init__(self, db_session: AsyncSession) -> None:
super().__init__(db_session)
@@ -57,17 +56,21 @@ class TemplatesRepository(BaseRepository):
async def get_template(self, template_id: UUID) -> Union[None, models.Template]:
query = select(models.Template).\
options(selectinload(models.Template.images)).\
where(models.Template.template_id == template_id)
query = (
select(models.Template)
.options(selectinload(models.Template.images))
.where(models.Template.template_id == template_id)
)
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]:
query = select(models.Template).\
options(selectinload(models.Template.images)).\
where(models.Template.name == name, models.Template.version == version)
query = (
select(models.Template)
.options(selectinload(models.Template.images))
.where(models.Template.name == name, models.Template.version == version)
)
result = await self._db_session.execute(query)
return result.scalars().first()
@@ -76,9 +79,9 @@ class TemplatesRepository(BaseRepository):
Return the first template with this name, regardless of version.
"""
query = select(models.Template).\
options(selectinload(models.Template.images)).\
where(models.Template.name == name)
query = (
select(models.Template).options(selectinload(models.Template.images)).where(models.Template.name == name)
)
result = await self._db_session.execute(query)
return result.scalars().first()
@@ -115,9 +118,11 @@ class TemplatesRepository(BaseRepository):
async def duplicate_template(self, template_id: UUID) -> Optional[schemas.Template]:
query = select(models.Template).\
options(selectinload(models.Template.images)).\
where(models.Template.template_id == template_id)
query = (
select(models.Template)
.options(selectinload(models.Template.images))
.where(models.Template.template_id == template_id)
)
db_template = (await self._db_session.execute(query)).scalars().first()
if db_template:
# duplicate db object with new primary key (template_id)
@@ -136,8 +141,9 @@ class TemplatesRepository(BaseRepository):
image_dir, image_name = os.path.split(image_path)
if image_dir:
query = select(models.Image).\
where(models.Image.filename == image_name, models.Image.path.endswith(image_path))
query = select(models.Image).where(
models.Image.filename == image_name, models.Image.path.endswith(image_path)
)
else:
query = select(models.Image).where(models.Image.filename == image_name)
query = query.order_by(models.Image.image_id)
@@ -151,18 +157,16 @@ class TemplatesRepository(BaseRepository):
)
return images[0] if images else None
async def add_image_to_template(
self,
template_id: UUID,
image: models.Image
) -> Union[None, models.Template]:
async def add_image_to_template(self, template_id: UUID, image: models.Image) -> Union[None, models.Template]:
"""
Add an image to template.
"""
query = select(models.Template).\
options(selectinload(models.Template.images)).\
where(models.Template.template_id == template_id)
query = (
select(models.Template)
.options(selectinload(models.Template.images))
.where(models.Template.template_id == template_id)
)
result = await self._db_session.execute(query)
template_in_db = result.scalars().first()
if not template_in_db:
@@ -173,18 +177,16 @@ class TemplatesRepository(BaseRepository):
await self._db_session.refresh(template_in_db)
return template_in_db
async def remove_image_from_template(
self,
template_id: UUID,
image: models.Image
) -> Union[None, models.Template]:
async def remove_image_from_template(self, template_id: UUID, image: models.Image) -> Union[None, models.Template]:
"""
Remove an image from a template.
"""
query = select(models.Template).\
options(selectinload(models.Template.images)).\
where(models.Template.template_id == template_id)
query = (
select(models.Template)
.options(selectinload(models.Template.images))
.where(models.Template.template_id == template_id)
)
result = await self._db_session.execute(query)
template_in_db = result.scalars().first()
if not template_in_db:
@@ -201,8 +203,6 @@ class TemplatesRepository(BaseRepository):
Return all images attached to a template.
"""
query = select(models.Image).\
join(models.Image.templates).\
filter(models.Template.template_id == template_id)
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()
+26 -39
View File
@@ -33,7 +33,6 @@ log = logging.getLogger(__name__)
class UsersRepository(BaseRepository):
def __init__(self, db_session: AsyncSession) -> None:
super().__init__(db_session)
@@ -82,10 +81,7 @@ class UsersRepository(BaseRepository):
hashed_password = self._auth_service.hash_password(user.password.get_secret_value())
db_user = models.User(
username=user.username,
email=user.email,
full_name=user.full_name,
hashed_password=hashed_password
username=user.username, email=user.email, full_name=user.full_name, hashed_password=hashed_password
)
self._db_session.add(db_user)
await self._db_session.commit()
@@ -102,9 +98,7 @@ class UsersRepository(BaseRepository):
if password:
update_values["hashed_password"] = self._auth_service.hash_password(password=password.get_secret_value())
query = update(models.User).\
where(models.User.user_id == user_id).\
values(update_values)
query = update(models.User).where(models.User.user_id == user_id).values(update_values)
await self._db_session.execute(query)
await self._db_session.commit()
@@ -118,9 +112,11 @@ class UsersRepository(BaseRepository):
Increment token_version to invalidate all existing tokens for the user.
"""
query = update(models.User).\
where(models.User.user_id == user_id).\
values(token_version=models.User.token_version + 1)
query = (
update(models.User)
.where(models.User.user_id == user_id)
.values(token_version=models.User.token_version + 1)
)
await self._db_session.execute(query)
await self._db_session.commit()
@@ -147,8 +143,9 @@ class UsersRepository(BaseRepository):
# this is useful for manual password recovery like:
# sqlite3 gns3_controller.db "UPDATE users SET hashed_password = null WHERE username = 'admin';"
if user.hashed_password is None:
log.warning(f"User '{username}' has been authenticated without a password "
f"configured. Please set a new password.")
log.warning(
f"User '{username}' has been authenticated without a password configured. Please set a new password."
)
return user
if not self._auth_service.verify_password(password, user.hashed_password):
return None
@@ -168,9 +165,7 @@ class UsersRepository(BaseRepository):
Get all user memberships (user groups).
"""
query = select(models.UserGroup).\
join(models.UserGroup.users).\
filter(models.User.user_id == user_id)
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()
@@ -214,18 +209,14 @@ class UsersRepository(BaseRepository):
return db_user_group
async def update_user_group(
self,
user_group_id: UUID,
user_group_update: schemas.UserGroupUpdate
self, user_group_id: UUID, user_group_update: schemas.UserGroupUpdate
) -> Optional[models.UserGroup]:
"""
Update a user group.
"""
update_values = user_group_update.model_dump(exclude_unset=True)
query = update(models.UserGroup).\
where(models.UserGroup.user_group_id == user_group_id).\
values(update_values)
query = update(models.UserGroup).where(models.UserGroup.user_group_id == user_group_id).values(update_values)
await self._db_session.execute(query)
await self._db_session.commit()
@@ -244,18 +235,16 @@ class UsersRepository(BaseRepository):
await self._db_session.commit()
return result.rowcount > 0
async def add_member_to_user_group(
self,
user_group_id: UUID,
user: models.User
) -> Union[None, models.UserGroup]:
async def add_member_to_user_group(self, user_group_id: UUID, user: models.User) -> Union[None, models.UserGroup]:
"""
Add a member to a user group.
"""
query = select(models.UserGroup).\
options(selectinload(models.UserGroup.users)).\
where(models.UserGroup.user_group_id == user_group_id)
query = (
select(models.UserGroup)
.options(selectinload(models.UserGroup.users))
.where(models.UserGroup.user_group_id == user_group_id)
)
result = await self._db_session.execute(query)
user_group_db = result.scalars().first()
if not user_group_db:
@@ -267,17 +256,17 @@ class UsersRepository(BaseRepository):
return user_group_db
async def remove_member_from_user_group(
self,
user_group_id: UUID,
user: models.User
self, user_group_id: UUID, user: models.User
) -> Union[None, models.UserGroup]:
"""
Remove a member from a user group.
"""
query = select(models.UserGroup).\
options(selectinload(models.UserGroup.users)).\
where(models.UserGroup.user_group_id == user_group_id)
query = (
select(models.UserGroup)
.options(selectinload(models.UserGroup.users))
.where(models.UserGroup.user_group_id == user_group_id)
)
result = await self._db_session.execute(query)
user_group_db = result.scalars().first()
if not user_group_db:
@@ -293,9 +282,7 @@ class UsersRepository(BaseRepository):
Get all members from a user group.
"""
query = select(models.User).\
join(models.User.groups).\
filter(models.UserGroup.user_group_id == user_group_id)
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()
+23 -19
View File
@@ -79,7 +79,9 @@ async def connect_to_db(app: FastAPI) -> None:
db_path = os.path.join(Config.instance().config_dir, "gns3_controller.db")
db_url = os.environ.get("GNS3_DATABASE_URI", f"sqlite+aiosqlite:///{db_path}")
engine = create_async_engine(db_url, connect_args={"check_same_thread": False, "timeout": 20}, future=True, pool_size=512, max_overflow=1024)
engine = create_async_engine(
db_url, connect_args={"check_same_thread": False, "timeout": 20}, future=True, pool_size=512, max_overflow=1024
)
# Register PRAGMA on the sync engine to ensure it fires for async connections
@event.listens_for(engine.sync_engine, "connect")
@@ -91,19 +93,21 @@ async def connect_to_db(app: FastAPI) -> None:
# Verify WAL mode is active
async with engine.connect() as _verify_conn:
def _check_wal(conn):
cursor = conn.connection.cursor()
cursor.execute("PRAGMA journal_mode")
row = cursor.fetchone()
cursor.close()
return row[0] if row else "unknown"
wal_mode = await _verify_conn.run_sync(_check_wal)
log.info(f"SQLite journal mode: {wal_mode}")
if wal_mode and wal_mode.upper() != "WAL":
log.warning("WAL mode not active - concurrent writes may cause 'database is locked' errors")
alembic_cfg = config.Config()
alembic_cfg.set_main_option("script_location", "gns3server:db_migrations")
#alembic_cfg.set_main_option('sqlalchemy.url', db_url)
# alembic_cfg.set_main_option('sqlalchemy.url', db_url)
try:
async with engine.connect() as conn:
current_rev, head_rev = await conn.run_sync(check_revision, alembic_cfg)
@@ -116,27 +120,27 @@ async def connect_to_db(app: FastAPI) -> None:
inspector = sa.inspect(connection)
tables = inspector.get_table_names()
if 'users' not in tables:
return 'new' # Truly new database
if "users" not in tables:
return "new" # Truly new database
# Check for new feature columns that indicate this is already migrated
columns = [col['name'] for col in inspector.get_columns('users')]
if 'llm_model_configs' in tables:
columns = [col["name"] for col in inspector.get_columns("users")]
if "llm_model_configs" in tables:
# The llm_model_configs table already exists (created from code)
return 'new_with_llm_configs'
return "new_with_llm_configs"
else:
# Old database without llm_model_configs table, needs migration
return 'old_needs_migration'
return "old_needs_migration"
db_state = await conn.run_sync(check_db_state)
if db_state == 'new':
if db_state == "new":
# Truly new database: create all tables and stamp
await conn.run_sync(Base.metadata.create_all)
await conn.run_sync(run_stamp, alembic_cfg)
await conn.commit()
log.info("Created new database and stamped to head revision")
elif db_state == 'new_with_llm_configs':
elif db_state == "new_with_llm_configs":
# Database already has llm_model_configs table (from Base.metadata.create_all)
# Just stamp the version
await conn.run_sync(run_stamp, alembic_cfg)
@@ -216,6 +220,7 @@ async def update_disk_checksums(updated_disks: List[str]) -> None:
"""
from gns3server.api.server import app
async with AsyncSession(app.state._db_engine) as db_session:
images_repository = ImagesRepository(db_session)
for path in updated_disks:
@@ -226,6 +231,7 @@ async def update_disk_checksums(updated_disks: List[str]) -> None:
if image.checksum != checksum:
await images_repository.update_image(path, checksum, "md5")
class EventHandler(PatternMatchingEventHandler):
"""
Watchdog event handler.
@@ -237,7 +243,7 @@ class EventHandler(PatternMatchingEventHandler):
self._queue = queue
# ignore temporary files, md5sum files, hidden files and directories
super().__init__(ignore_patterns=["*.tmp", "*.md5sum", ".*"], ignore_directories = True, **kwargs)
super().__init__(ignore_patterns=["*.tmp", "*.md5sum", ".*"], ignore_directories=True, **kwargs)
def on_closed(self, event: FileSystemEvent) -> None:
# monitor for closed files (e.g. when a file has finished to be copied)
@@ -245,6 +251,7 @@ class EventHandler(PatternMatchingEventHandler):
return # ignore custom IOU libraries
self._loop.call_soon_threadsafe(self._queue.put_nowait, event)
class EventIterator(object):
"""
Watchdog Event iterator.
@@ -263,13 +270,11 @@ class EventIterator(object):
raise StopAsyncIteration
return item
async def monitor_images_on_filesystem(app: FastAPI):
def watchdog(
path: str,
queue: asyncio.Queue,
loop: asyncio.BaseEventLoop,
app: FastAPI, recursive: bool = False
path: str, queue: asyncio.Queue, loop: asyncio.BaseEventLoop, app: FastAPI, recursive: bool = False
) -> None:
"""
Thread to monitor a directory for new images.
@@ -294,7 +299,7 @@ async def monitor_images_on_filesystem(app: FastAPI):
loop = asyncio.get_event_loop()
server_config = Config.instance().settings.Server
image_dir = os.path.expanduser(server_config.images_path)
asyncio.get_event_loop().run_in_executor(None, watchdog,image_dir, queue, loop, app, True)
asyncio.get_event_loop().run_in_executor(None, watchdog, image_dir, queue, loop, app, True)
async for filesystem_event in EventIterator(queue):
# read the file system event from the queue
@@ -352,7 +357,7 @@ async def get_user_llm_config_full(user_id: str, app: FastAPI) -> Optional[dict]
result = await repo.get_user_effective_configs(
user_uuid,
current_user_id=user_uuid, # Viewing own config
current_user_is_superadmin=False
current_user_is_superadmin=False,
)
if not result or not result.get("default_config"):
@@ -393,7 +398,7 @@ async def get_user_llm_config_full(user_id: str, app: FastAPI) -> Optional[dict]
"group_name": default_config.get("group_name"),
"user_id": str(full_config.user_id) if full_config.user_id else None,
"group_id": str(full_config.group_id) if full_config.group_id else None,
**config_data # provider, api_key, model, temperature, etc.
**config_data, # provider, api_key, model, temperature, etc.
}
# Validate required fields
@@ -415,4 +420,3 @@ async def get_user_llm_config_full(user_id: str, app: FastAPI) -> Optional[dict]
except Exception as e:
log.error(f"Failed to retrieve LLM config for user {user_id}: {e}", exc_info=True)
return None