mirror of
https://github.com/GNS3/gns3-server.git
synced 2025-01-08 14:02:58 +00:00
Merge pull request #2471 from GNS3/watchdog-migration
Watchdog migration
This commit is contained in:
commit
b472f6dbf8
@ -16,13 +16,11 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import signal
|
import time
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
from watchfiles import awatch, Change
|
|
||||||
|
|
||||||
from typing import List
|
from typing import List
|
||||||
from sqlalchemy import event
|
from sqlalchemy import event
|
||||||
from sqlalchemy.engine import Engine
|
from sqlalchemy.engine import Engine
|
||||||
@ -32,10 +30,12 @@ from alembic import command, config
|
|||||||
from alembic.script import ScriptDirectory
|
from alembic.script import ScriptDirectory
|
||||||
from alembic.runtime.migration import MigrationContext
|
from alembic.runtime.migration import MigrationContext
|
||||||
from alembic.util.exc import CommandError
|
from alembic.util.exc import CommandError
|
||||||
|
from watchdog.observers import Observer
|
||||||
|
from watchdog.events import FileSystemEvent, PatternMatchingEventHandler
|
||||||
|
|
||||||
from gns3server.db.repositories.computes import ComputesRepository
|
from gns3server.db.repositories.computes import ComputesRepository
|
||||||
from gns3server.db.repositories.images import ImagesRepository
|
from gns3server.db.repositories.images import ImagesRepository
|
||||||
from gns3server.utils.images import discover_images, check_valid_image_header, read_image_info, default_images_directory, InvalidImageError
|
from gns3server.utils.images import discover_images, read_image_info, default_images_directory, InvalidImageError
|
||||||
from gns3server import schemas
|
from gns3server import schemas
|
||||||
|
|
||||||
from .models import Base
|
from .models import Base
|
||||||
@ -130,80 +130,6 @@ async def get_computes(app: FastAPI) -> List[dict]:
|
|||||||
return computes
|
return computes
|
||||||
|
|
||||||
|
|
||||||
def image_filter(change: Change, path: str) -> bool:
|
|
||||||
|
|
||||||
if change == Change.added and os.path.isfile(path):
|
|
||||||
if path.endswith(".tmp") or path.endswith(".md5sum") or path.startswith("."):
|
|
||||||
return False
|
|
||||||
if "/lib/" in path or "/lib64/" in path:
|
|
||||||
# ignore custom IOU libraries
|
|
||||||
return False
|
|
||||||
header_magic_len = 7
|
|
||||||
with open(path, "rb") as f:
|
|
||||||
image_header = f.read(header_magic_len) # read the first 7 bytes of the file
|
|
||||||
if len(image_header) >= header_magic_len:
|
|
||||||
try:
|
|
||||||
check_valid_image_header(image_header)
|
|
||||||
except InvalidImageError as e:
|
|
||||||
log.debug(f"New image '{path}': {e}")
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
log.debug(f"New image '{path}': size is too small to be valid")
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
# FIXME: should we support image deletion?
|
|
||||||
# elif change == Change.deleted:
|
|
||||||
# return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
async def monitor_images_on_filesystem(app: FastAPI):
|
|
||||||
|
|
||||||
directories_to_monitor = []
|
|
||||||
for image_type in ("qemu", "ios", "iou"):
|
|
||||||
image_dir = default_images_directory(image_type)
|
|
||||||
if os.path.isdir(image_dir):
|
|
||||||
log.debug(f"Monitoring for new images in '{image_dir}'")
|
|
||||||
directories_to_monitor.append(image_dir)
|
|
||||||
|
|
||||||
try:
|
|
||||||
async for changes in awatch(
|
|
||||||
*directories_to_monitor,
|
|
||||||
watch_filter=image_filter,
|
|
||||||
raise_interrupt=True
|
|
||||||
):
|
|
||||||
async with AsyncSession(app.state._db_engine) as db_session:
|
|
||||||
images_repository = ImagesRepository(db_session)
|
|
||||||
for change in changes:
|
|
||||||
change_type, image_path = change
|
|
||||||
if change_type == Change.added:
|
|
||||||
try:
|
|
||||||
image = await read_image_info(image_path)
|
|
||||||
except InvalidImageError as e:
|
|
||||||
log.warning(str(e))
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
if await images_repository.get_image(image_path):
|
|
||||||
continue
|
|
||||||
await images_repository.add_image(**image)
|
|
||||||
log.info(f"Discovered image '{image_path}' has been added to the database")
|
|
||||||
except SQLAlchemyError as e:
|
|
||||||
log.warning(f"Error while adding image '{image_path}' to the database: {e}")
|
|
||||||
# if change_type == Change.deleted:
|
|
||||||
# try:
|
|
||||||
# if await images_repository.get_image(image_path):
|
|
||||||
# success = await images_repository.delete_image(image_path)
|
|
||||||
# if not success:
|
|
||||||
# log.warning(f"Could not delete image '{image_path}' from the database")
|
|
||||||
# else:
|
|
||||||
# log.info(f"Image '{image_path}' has been deleted from the database")
|
|
||||||
# except SQLAlchemyError as e:
|
|
||||||
# log.warning(f"Error while deleting image '{image_path}' from the database: {e}")
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
# send SIGTERM to the server PID so uvicorn can shutdown the process
|
|
||||||
os.kill(os.getpid(), signal.SIGTERM)
|
|
||||||
|
|
||||||
|
|
||||||
async def discover_images_on_filesystem(app: FastAPI):
|
async def discover_images_on_filesystem(app: FastAPI):
|
||||||
|
|
||||||
async with AsyncSession(app.state._db_engine) as db_session:
|
async with AsyncSession(app.state._db_engine) as db_session:
|
||||||
@ -228,3 +154,99 @@ async def discover_images_on_filesystem(app: FastAPI):
|
|||||||
|
|
||||||
# monitor if images have been manually added
|
# monitor if images have been manually added
|
||||||
asyncio.create_task(monitor_images_on_filesystem(app))
|
asyncio.create_task(monitor_images_on_filesystem(app))
|
||||||
|
|
||||||
|
class EventHandler(PatternMatchingEventHandler):
|
||||||
|
"""
|
||||||
|
Watchdog event handler.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, queue: asyncio.Queue, loop: asyncio.BaseEventLoop, **kwargs):
|
||||||
|
|
||||||
|
self._loop = loop
|
||||||
|
self._queue = queue
|
||||||
|
|
||||||
|
# ignore temporary files, md5sum files, hidden files and directories
|
||||||
|
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)
|
||||||
|
if "/lib/" in event.src_path or "/lib64/" in event.src_path:
|
||||||
|
return # ignore custom IOU libraries
|
||||||
|
self._loop.call_soon_threadsafe(self._queue.put_nowait, event)
|
||||||
|
|
||||||
|
class EventIterator(object):
|
||||||
|
"""
|
||||||
|
Watchdog Event iterator.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, queue: asyncio.Queue):
|
||||||
|
self.queue = queue
|
||||||
|
|
||||||
|
def __aiter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __anext__(self):
|
||||||
|
|
||||||
|
item = await self.queue.get()
|
||||||
|
if item is None:
|
||||||
|
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
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Thread to monitor a directory for new images.
|
||||||
|
"""
|
||||||
|
|
||||||
|
handler = EventHandler(queue, loop)
|
||||||
|
observer = Observer()
|
||||||
|
observer.schedule(handler, str(path), recursive=recursive)
|
||||||
|
observer.start()
|
||||||
|
log.info(f"Monitoring for new images in '{path}'")
|
||||||
|
while True:
|
||||||
|
time.sleep(1)
|
||||||
|
# stop when the app is exiting
|
||||||
|
if app.state.exiting:
|
||||||
|
observer.stop()
|
||||||
|
observer.join(10)
|
||||||
|
log.info(f"Stopping monitoring for new images in '{path}'")
|
||||||
|
loop.call_soon_threadsafe(queue.put_nowait, None)
|
||||||
|
break
|
||||||
|
|
||||||
|
queue = asyncio.Queue()
|
||||||
|
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)
|
||||||
|
|
||||||
|
async for filesystem_event in EventIterator(queue):
|
||||||
|
# read the file system event from the queue
|
||||||
|
print(filesystem_event)
|
||||||
|
image_path = filesystem_event.src_path
|
||||||
|
expected_image_type = None
|
||||||
|
if "IOU" in image_path:
|
||||||
|
expected_image_type = "iou"
|
||||||
|
elif "QEMU" in image_path:
|
||||||
|
expected_image_type = "qemu"
|
||||||
|
elif "IOS" in image_path:
|
||||||
|
expected_image_type = "ios"
|
||||||
|
async with AsyncSession(app.state._db_engine) as db_session:
|
||||||
|
images_repository = ImagesRepository(db_session)
|
||||||
|
try:
|
||||||
|
image = await read_image_info(image_path, expected_image_type)
|
||||||
|
except InvalidImageError as e:
|
||||||
|
log.warning(str(e))
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
if await images_repository.get_image(image_path):
|
||||||
|
continue
|
||||||
|
await images_repository.add_image(**image)
|
||||||
|
log.info(f"Discovered image '{image_path}' has been added to the database")
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
log.warning(f"Error while adding image '{image_path}' to the database: {e}")
|
||||||
|
@ -18,7 +18,7 @@ alembic==1.14.0
|
|||||||
bcrypt==4.2.1
|
bcrypt==4.2.1
|
||||||
joserfc==1.0.1
|
joserfc==1.0.1
|
||||||
email-validator==2.2.0
|
email-validator==2.2.0
|
||||||
watchfiles==1.0.3
|
watchdog==6.0.0
|
||||||
zstandard==0.23.0
|
zstandard==0.23.0
|
||||||
platformdirs>=2.4.0,<3 # platformdirs >=3 conflicts when building Debian packages
|
platformdirs>=2.4.0,<3 # platformdirs >=3 conflicts when building Debian packages
|
||||||
importlib-resources>=1.3; python_version <= '3.9'
|
importlib-resources>=1.3; python_version <= '3.9'
|
||||||
|
@ -400,10 +400,12 @@ def run_around_tests(monkeypatch, config, port_manager):
|
|||||||
config.settings.VMware.vmrun_path = tmppath
|
config.settings.VMware.vmrun_path = tmppath
|
||||||
config.settings.Dynamips.dynamips_path = tmppath
|
config.settings.Dynamips.dynamips_path = tmppath
|
||||||
|
|
||||||
|
|
||||||
# Force turn off KVM because it's not available on CI
|
# Force turn off KVM because it's not available on CI
|
||||||
config.settings.Qemu.enable_hardware_acceleration = False
|
config.settings.Qemu.enable_hardware_acceleration = False
|
||||||
|
|
||||||
|
# avoid monitoring for new images while testing
|
||||||
|
config.settings.Server.auto_discover_images = False
|
||||||
|
|
||||||
monkeypatch.setattr("gns3server.utils.path.get_default_project_directory", lambda *args: os.path.join(tmppath, 'projects'))
|
monkeypatch.setattr("gns3server.utils.path.get_default_project_directory", lambda *args: os.path.join(tmppath, 'projects'))
|
||||||
|
|
||||||
# Force sys.platform to the original value. Because it seems not be restored correctly after each test
|
# Force sys.platform to the original value. Because it seems not be restored correctly after each test
|
||||||
|
Loading…
Reference in New Issue
Block a user