1
0
mirror of https://github.com/GNS3/gns3-server synced 2024-12-25 16:28:11 +00:00

Add prune images endpoint.

Use many-to-many relationship between images and templates.
This commit is contained in:
grossmj 2021-08-20 15:58:41 +09:30
parent 078c42f185
commit 4d9e4e1059
5 changed files with 90 additions and 13 deletions

View File

@ -22,7 +22,7 @@ import os
import logging
import urllib.parse
from fastapi import APIRouter, Request, Depends, status
from fastapi import APIRouter, Request, Response, Depends, status
from typing import List
from gns3server import schemas
@ -125,3 +125,15 @@ async def delete_image(
success = await images_repo.delete_image(image_name)
if not success:
raise ControllerError(f"Image '{image_name}' could not be deleted")
@router.post("/prune", status_code=status.HTTP_204_NO_CONTENT)
async def prune_images(
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
) -> Response:
"""
Prune images not attached to any template.
"""
await images_repo.prune_images()
return Response(status_code=status.HTTP_204_NO_CONTENT)

View File

@ -15,10 +15,18 @@
# 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 sqlalchemy import Column, String, Integer, BigInteger
from sqlalchemy import Table, Column, String, Integer, ForeignKey, BigInteger
from sqlalchemy.orm import relationship
from .base import BaseTable
from .base import Base, BaseTable, GUID
image_template_link = Table(
"images_templates_link",
Base.metadata,
Column("image_id", Integer, ForeignKey("images.id", ondelete="CASCADE")),
Column("template_id", GUID, ForeignKey("templates.template_id", ondelete="CASCADE"))
)
class Image(BaseTable):
@ -32,4 +40,4 @@ class Image(BaseTable):
path = Column(String)
checksum = Column(String)
checksum_algorithm = Column(String)
templates = relationship("Template")
templates = relationship("Template", secondary=image_template_link, back_populates="images")

View File

@ -17,8 +17,10 @@
from sqlalchemy import Boolean, Column, String, Integer, ForeignKey, PickleType
from sqlalchemy.orm import relationship
from .base import BaseTable, generate_uuid, GUID
from .images import image_template_link
class Template(BaseTable):
@ -34,8 +36,7 @@ class Template(BaseTable):
compute_id = Column(String)
usage = Column(String)
template_type = Column(String)
image_id = Column(Integer, ForeignKey('images.id', ondelete="CASCADE"))
images = relationship("Image", secondary=image_template_link, back_populates="templates")
__mapper_args__ = {
"polymorphic_identity": "templates",

View File

@ -15,6 +15,8 @@
# 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 os
from typing import Optional, List
from sqlalchemy import select, delete
from sqlalchemy.ext.asyncio import AsyncSession
@ -23,6 +25,10 @@ from .base import BaseRepository
import gns3server.db.models as models
import logging
log = logging.getLogger(__name__)
class ImagesRepository(BaseRepository):
@ -31,33 +37,48 @@ class ImagesRepository(BaseRepository):
super().__init__(db_session)
async def get_image(self, image_name: str) -> Optional[models.Image]:
"""
Get an image by its name (filename).
"""
query = select(models.Image).where(models.Image.filename == image_name)
result = await self._db_session.execute(query)
return result.scalars().first()
async def get_image_by_checksum(self, checksum: str) -> Optional[models.Image]:
"""
Get an image by its checksum.
"""
query = select(models.Image).where(models.Image.checksum == checksum)
result = await self._db_session.execute(query)
return result.scalars().first()
async def get_images(self) -> List[models.Image]:
"""
Get all images.
"""
query = select(models.Image)
result = await self._db_session.execute(query)
return result.scalars().all()
async def get_image_templates(self, image_id: int) -> Optional[List[models.Template]]:
"""
Get all templates that an image belongs to.
"""
query = select(models.Template).\
join(models.Image.templates). \
join(models.Template.images).\
filter(models.Image.id == image_id)
result = await self._db_session.execute(query)
return result.scalars().all()
async def get_image_by_checksum(self, checksum: str) -> Optional[models.Image]:
query = select(models.Image).where(models.Image.checksum == checksum)
result = await self._db_session.execute(query)
return result.scalars().first()
async def add_image(self, image_name, image_type, image_size, path, checksum, checksum_algorithm) -> models.Image:
"""
Create a new image.
"""
db_image = models.Image(
id=None,
@ -75,8 +96,31 @@ class ImagesRepository(BaseRepository):
return db_image
async def delete_image(self, image_name: str) -> bool:
"""
Delete an image.
"""
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
async def prune_images(self) -> int:
"""
Prune images not attached to any template.
"""
query = select(models.Image).\
filter(~models.Image.templates.any())
result = await self._db_session.execute(query)
images = result.scalars().all()
images_deleted = 0
for image in images:
try:
os.remove(image.path)
except OSError:
log.warning(f"Could not delete image file {image.path}")
if await self.delete_image(image.filename):
images_deleted += 1
log.info(f"{images_deleted} image have been deleted")
return images_deleted

View File

@ -19,9 +19,12 @@ import os
import pytest
import hashlib
from sqlalchemy.ext.asyncio import AsyncSession
from fastapi import FastAPI, status
from httpx import AsyncClient
from gns3server.db.repositories.images import ImagesRepository
pytestmark = pytest.mark.asyncio
@ -193,3 +196,12 @@ class TestImageRoutes:
response = await client.delete(app.url_path_for("delete_image", image_name=image_name))
assert response.status_code == status.HTTP_204_NO_CONTENT
assert not os.path.exists(os.path.join(images_dir, "QEMU", image_name))
async def test_prune_images(self, app: FastAPI, client: AsyncClient, db_session: AsyncSession) -> None:
response = await client.post(app.url_path_for("prune_images"))
assert response.status_code == status.HTTP_204_NO_CONTENT
images_repo = ImagesRepository(db_session)
images_in_db = await images_repo.get_images()
assert len(images_in_db) == 0