1
0
mirror of https://github.com/GNS3/gns3-server synced 2024-12-27 01:08:33 +00:00

Merge remote-tracking branch 'origin/3.0' into gh-pages

This commit is contained in:
github-actions 2024-12-20 11:21:52 +00:00
commit 687ee321a2
30 changed files with 2248 additions and 156 deletions

View File

@ -1,5 +1,23 @@
# Change Log # Change Log
## 3.0.0 20/12/2024
* Bundle web-ui v3.0.0
* Use static favicon for API docs. Ref #3674
* Configure self-hosting JavaScript and CSS for docs
* Fix project auto open feature. Fixes #2455
* Store IOU licence in the secrets directory and disable the check by default
* Require "Project.Audit" permission to duplicate a project and check if "Project.Allocate" permission for the destination.
## 2.2.52 02/12/2024
* Bundle web-ui v2.2.52
* Sync appliances
* Remove restrictions based on file extension when listing images and fix ELF header checks
* Fix use project name instead of ID for fast duplication when running local server. Fixes #2446
* Overwrite user resources when the originals have changed.
* Relax setuptools requirement to allow for easier Debian packaging on Ubuntu Focal & Jammy
## 3.0.0rc2 20/11/2024 ## 3.0.0rc2 20/11/2024
* Bundle web-ui v3.0.0rc2 * Bundle web-ui v3.0.0rc2

View File

@ -426,26 +426,42 @@ async def import_project(
status_code=status.HTTP_201_CREATED, status_code=status.HTTP_201_CREATED,
response_model=schemas.Project, response_model=schemas.Project,
responses={**responses, 409: {"model": schemas.ErrorMessage, "description": "Could not duplicate project"}}, responses={**responses, 409: {"model": schemas.ErrorMessage, "description": "Could not duplicate project"}},
dependencies=[Depends(has_privilege("Project.Allocate"))] dependencies=[Depends(has_privilege("Project.Audit"))]
) )
async def duplicate_project( async def duplicate_project(
project_data: schemas.ProjectDuplicate, project_data: schemas.ProjectDuplicate,
project: Project = Depends(dep_project), project: Project = Depends(dep_project),
current_user: schemas.User = Depends(get_current_active_user),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)) pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository))
) -> schemas.Project: ) -> schemas.Project:
""" """
Duplicate a project. Duplicate a project.
Required privilege: Project.Allocate Required privilege: Project.Audit
""" """
pool_memberships = await pools_repo.get_resource_memberships(project.id)
# check if the project can be duplicated somewhere (either in a pool or in the root)
if not current_user.is_superadmin:
can_be_duplicated_somewhere = False
if pool_memberships:
for pool in pool_memberships:
if await rbac_repo.check_user_has_privilege(current_user.user_id, f"/pools/{pool.resource_pool_id}", "Project.Allocate"):
can_be_duplicated_somewhere = True
break
if not can_be_duplicated_somewhere and not await rbac_repo.check_user_has_privilege(current_user.user_id, "/projects", "Project.Allocate"):
log.warning(f"Project {project.name} cannot be duplicated anywhere")
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
reset_mac_addresses = project_data.reset_mac_addresses reset_mac_addresses = project_data.reset_mac_addresses
new_project = await project.duplicate( new_project = await project.duplicate(
name=project_data.name, reset_mac_addresses=reset_mac_addresses name=project_data.name, reset_mac_addresses=reset_mac_addresses
) )
# Add the new project in the same resource pools if the duplicated project is in any # Add the new project in the same resource pools if the duplicated project belongs to any
pool_memberships = await pools_repo.get_resource_memberships(project.id)
if pool_memberships: if pool_memberships:
resource_create = schemas.ResourceCreate(resource_id=new_project.id, resource_type="project", name=new_project.name) resource_create = schemas.ResourceCreate(resource_id=new_project.id, resource_type="project", name=new_project.name)
resource = await pools_repo.create_resource(resource_create) resource = await pools_repo.create_resource(resource_create)

View File

@ -27,20 +27,20 @@ router = APIRouter()
templates = Jinja2Templates(directory=os.path.join("gns3server", "templates")) templates = Jinja2Templates(directory=os.path.join("gns3server", "templates"))
@router.get("/") @router.get("/", include_in_schema=False)
async def root(): async def root():
return RedirectResponse("/static/web-ui/bundled", status_code=308) # permanent redirect return RedirectResponse("/static/web-ui/bundled", status_code=308) # permanent redirect
@router.get("/debug", response_class=HTMLResponse, deprecated=True) @router.get("/debug", response_class=HTMLResponse, deprecated=True, include_in_schema=False)
def debug(request: Request): def debug(request: Request):
kwargs = {"request": request, "gns3_version": __version__, "gns3_host": request.client.host} kwargs = {"request": request, "gns3_version": __version__, "gns3_host": request.client.host}
return templates.TemplateResponse("index.html", kwargs) return templates.TemplateResponse("index.html", kwargs)
@router.get("/static/web-ui/{file_path:path}", description="Web user interface") @router.get("/static/web-ui/{file_path:path}", description="Web user interface", include_in_schema=False)
async def web_ui(file_path: str): async def web_ui(file_path: str):
file_path = os.path.normpath(file_path).strip("/") file_path = os.path.normpath(file_path).strip("/")

View File

@ -19,15 +19,20 @@
FastAPI app FastAPI app
""" """
import time
from fastapi import FastAPI, Request, HTTPException, status from fastapi import FastAPI, Request, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError from fastapi.exceptions import RequestValidationError
from fastapi.staticfiles import StaticFiles
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from uvicorn.main import Server as UvicornServer from uvicorn.main import Server as UvicornServer
from fastapi.openapi.docs import (
get_redoc_html,
get_swagger_ui_html,
get_swagger_ui_oauth2_redirect_html,
)
from gns3server.controller.controller_error import ( from gns3server.controller.controller_error import (
ControllerError, ControllerError,
ControllerNotFoundError, ControllerNotFoundError,
@ -51,7 +56,11 @@ log = logging.getLogger(__name__)
def get_application() -> FastAPI: def get_application() -> FastAPI:
application = FastAPI( application = FastAPI(
title="GNS3 controller API", description="This page describes the public controller API for GNS3", version="v3" title="GNS3 controller API",
description="This page describes the public controller API for GNS3",
version="v3",
docs_url=None,
redoc_url=None
) )
application.add_middleware( application.add_middleware(
@ -66,6 +75,7 @@ def get_application() -> FastAPI:
application.add_event_handler("shutdown", tasks.create_shutdown_handler(application)) application.add_event_handler("shutdown", tasks.create_shutdown_handler(application))
application.include_router(index.router, tags=["Index"]) application.include_router(index.router, tags=["Index"])
application.include_router(controller.router, prefix="/v3") application.include_router(controller.router, prefix="/v3")
application.mount("/static", StaticFiles(packages=[('gns3server', 'static')]), name="static")
application.mount("/v3/compute", compute_api, name="compute") application.mount("/v3/compute", compute_api, name="compute")
return application return application
@ -85,6 +95,31 @@ def handle_exit(*args, **kwargs):
UvicornServer.handle_exit = handle_exit UvicornServer.handle_exit = handle_exit
# Configure self-hosting JavaScript and CSS for docs
@app.get("/docs", include_in_schema=False)
async def custom_swagger_ui_html():
return get_swagger_ui_html(
openapi_url=app.openapi_url,
title=app.title + " - Swagger UI",
oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url,
swagger_js_url="/static/swagger-ui-bundle.js",
swagger_css_url="/static/swagger-ui.css",
swagger_favicon_url="/static/favicon.ico"
)
@app.get(app.swagger_ui_oauth2_redirect_url, include_in_schema=False)
async def swagger_ui_redirect():
return get_swagger_ui_oauth2_redirect_html()
@app.get("/redoc", include_in_schema=False)
async def redoc_html():
return get_redoc_html(
openapi_url=app.openapi_url,
title=app.title + " - ReDoc",
redoc_js_url="/static/redoc.standalone.js",
redoc_favicon_url="/static/favicon.ico"
)
@app.exception_handler(ControllerError) @app.exception_handler(ControllerError)
async def controller_error_handler(request: Request, exc: ControllerError): async def controller_error_handler(request: Request, exc: ControllerError):

View File

@ -33,11 +33,23 @@
"md5sum": "cbbbea66a253f1dac0fcf81274dc778d", "md5sum": "cbbbea66a253f1dac0fcf81274dc778d",
"filesize": 87756936 "filesize": 87756936
}, },
{
"filename": "c7200-adventerprisek9-mz.152-4.M11.image",
"version": "152-4.M11",
"md5sum": "9a2005ad09ce1ec6fe7cf9af1e9b099e",
"filesize": 128487680
},
{ {
"filename": "c7200-adventerprisek9-mz.124-24.T5.image", "filename": "c7200-adventerprisek9-mz.124-24.T5.image",
"version": "124-24.T5", "version": "124-24.T5",
"md5sum": "6b89d0d804e1f2bb5b8bda66b5692047", "md5sum": "6b89d0d804e1f2bb5b8bda66b5692047",
"filesize": 102345240 "filesize": 102345240
},
{
"filename": "c7200-a3jk9s-mz.124-25g.image",
"version": "124-25G",
"md5sum": "9c7cc9b3f3b3571411a7f62faaa2c036",
"filesize": 71528984
} }
], ],
"versions": [ "versions": [
@ -55,12 +67,26 @@
"image": "c7200-advipservicesk9-mz.152-4.S5.image" "image": "c7200-advipservicesk9-mz.152-4.S5.image"
} }
}, },
{
"name": "152-4.M11",
"idlepc": "0x6062e5c0",
"images": {
"image": "c7200-adventerprisek9-mz.152-4.M11.image"
}
},
{ {
"name": "124-24.T5", "name": "124-24.T5",
"idlepc": "0x606df838", "idlepc": "0x606df838",
"images": { "images": {
"image": "c7200-adventerprisek9-mz.124-24.T5.image" "image": "c7200-adventerprisek9-mz.124-24.T5.image"
} }
},
{
"name": "124-25G",
"idlepc": "0x6066a998",
"images": {
"image": "c7200-a3jk9s-mz.124-25g.image"
}
} }
] ]
} }

View File

@ -12,7 +12,7 @@
"status": "stable", "status": "stable",
"maintainer": "GNS3 Team", "maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net", "maintainer_email": "developers@gns3.net",
"usage": "There is no default password and enable password. A default configuration is present. ASAv goes through a double-boot before becoming active. This is normal and expected.", "usage": "There is no default password and enable password. A default configuration is present. ASAv goes through a double-boot before becoming active. This is normal and expected. Switch to the Telnet console type after the first boot.",
"symbol": ":/symbols/asa.svg", "symbol": ":/symbols/asa.svg",
"first_port_name": "Management0/0", "first_port_name": "Management0/0",
"port_name_format": "Gi0/{0}", "port_name_format": "Gi0/{0}",
@ -26,6 +26,13 @@
"kvm": "require" "kvm": "require"
}, },
"images": [ "images": [
{
"filename": "asav9-22-1-1.qcow2",
"version": "9.22.1.1 CML",
"md5sum": "250a924cdc2370208eaac9d1dc8dc9e3",
"filesize": 379518976,
"download_url": "https://learningnetworkstore.cisco.com/cisco-modeling-labs-personal/cisco-modeling-labs-personal/CML-PERSONAL.html"
},
{ {
"filename": "asav9-18-2.qcow2", "filename": "asav9-18-2.qcow2",
"version": "9.18.2 CML", "version": "9.18.2 CML",
@ -126,6 +133,12 @@
} }
], ],
"versions": [ "versions": [
{
"name": "9.22.1.1 CML",
"images": {
"hda_disk_image": "asav9-22-1-1.qcow2"
}
},
{ {
"name": "9.18.2 CML", "name": "9.18.2 CML",
"images": { "images": {

View File

@ -13,13 +13,19 @@
"iou": { "iou": {
"ethernet_adapters": 4, "ethernet_adapters": 4,
"serial_adapters": 0, "serial_adapters": 0,
"nvram": 128, "nvram": 512,
"ram": 256, "ram": 512,
"startup_config": "iou_l2_base_startup-config.txt" "startup_config": "iou_l2_base_startup-config.txt"
}, },
"images": [ "images": [
{ {
"filename": "x86_64_crb_linux_l2-adventerprisek9-ms.bin", "filename": "x86_64_crb_linux_l2-adventerprisek9-ms.iol",
"version": "17.15.1",
"md5sum": "6c587cdfd5056078e70b3f6c26800d66",
"filesize": 243251976
},
{
"filename": "x86_64_crb_linux_l2-adventerprisek9-ms",
"version": "17.12.1", "version": "17.12.1",
"md5sum": "2b5055e4cef8fd257416d74a94adb626", "md5sum": "2b5055e4cef8fd257416d74a94adb626",
"filesize": 240355720 "filesize": 240355720
@ -44,10 +50,16 @@
} }
], ],
"versions": [ "versions": [
{
"name": "17.15.1",
"images": {
"image": "x86_64_crb_linux_l2-adventerprisek9-ms.iol"
}
},
{ {
"name": "17.12.1", "name": "17.12.1",
"images": { "images": {
"image": "x86_64_crb_linux_l2-adventerprisek9-ms.bin" "image": "x86_64_crb_linux_l2-adventerprisek9-ms"
} }
}, },
{ {

View File

@ -13,13 +13,19 @@
"iou": { "iou": {
"ethernet_adapters": 2, "ethernet_adapters": 2,
"serial_adapters": 2, "serial_adapters": 2,
"nvram": 128, "nvram": 512,
"ram": 256, "ram": 512,
"startup_config": "iou_l3_base_startup-config.txt" "startup_config": "iou_l3_base_startup-config.txt"
}, },
"images": [ "images": [
{ {
"filename": "x86_64_crb_linux-adventerprisek9-ms.bin", "filename": "x86_64_crb_linux-adventerprisek9-ms.iol",
"version": "17.15.1",
"md5sum": "5d584f6cfbeaadc87d55f613da1049ed",
"filesize": 292001512
},
{
"filename": "x86_64_crb_linux-adventerprisek9-ms",
"version": "17.12.1", "version": "17.12.1",
"md5sum": "4a2fce8de21d1831fbceffd155e41ae7", "md5sum": "4a2fce8de21d1831fbceffd155e41ae7",
"filesize": 288947184 "filesize": 288947184
@ -44,10 +50,16 @@
} }
], ],
"versions": [ "versions": [
{
"name": "17.15.1",
"images": {
"image": "x86_64_crb_linux-adventerprisek9-ms.iol"
}
},
{ {
"name": "17.12.1", "name": "17.12.1",
"images": { "images": {
"image": "x86_64_crb_linux-adventerprisek9-ms.bin" "image": "x86_64_crb_linux-adventerprisek9-ms"
} }
}, },
{ {

View File

@ -0,0 +1,50 @@
{
"appliance_id": "92dbd0e9-144e-4c59-a4a8-97b6a1661818",
"name": "Innovaphone App-Platform",
"category": "guest",
"description": "In addition to telephony, apps for Video Telephony, Chat, Conferencing, Application Sharing and many other functions have become indispensable UCC tools in the area of business communication. Based on the myApps platform and its various components, innovaphone provides a collaborative work and communication platform for enhanced corporate communications \u2013 regardless of the location and the device being used. The innovaphone platform myApps consists of many independent components that work well individually, yet unfold their remarkable performance when combined.",
"vendor_name": "Innovaphone",
"vendor_url": "https://www.innovaphone.com",
"vendor_logo_url": "https://www.innovaphone.com/content/downloads/innovaphone-myapps-logo-short-without-background-screen.png",
"documentation_url": "https://wiki.innovaphone.com/index.php?title=Reference14r2:Concept_App_Platform",
"product_name": "App-Platform",
"product_url": "https://www.innovaphone.com/en/products/myapps/myapps-platform.html",
"registry_version": 4,
"status": "experimental",
"availability": "free-to-try",
"maintainer": "Thomas Marchsteiner",
"maintainer_email": "thomas.marchsteiner@acp.at",
"usage": "Default users console:root/iplinux , ssh:admin/ipapps , Webinterface:pwd \nAfter first boot wait for automatic reboot.\nA static ip can be set via the setip utility. \nLoading another keymap can be done via the loadkeys command. \nThe app-platform-disk1.vmdk file is contained within an ova file. \nIt can be extraced with the tar utility, 7Zip or any other tool which can handle tar files.",
"symbol": "innovaphone-ap-icon.jpg",
"first_port_name": "eth0",
"qemu": {
"adapter_type": "vmxnet3",
"adapters": 1,
"ram": 512,
"cpus": 1,
"hda_disk_interface": "scsi",
"arch": "x86_64",
"console_type": "vnc",
"boot_priority": "d",
"kvm": "allow",
"on_close": "power_off",
"process_priority": "normal"
},
"images": [
{
"filename": "app-platform-disk1_120010.vmdk",
"version": "12.0010",
"md5sum": "d5a5a77f682c2c988b0810935d79a787",
"filesize": 129474560,
"download_url": "https://store.innovaphone.com/"
}
],
"versions": [
{
"images": {
"hda_disk_image": "app-platform-disk1_120010.vmdk"
},
"name": "12.0010"
}
]
}

View File

@ -0,0 +1,78 @@
{
"appliance_id": "ddf8f7a4-60c0-4c9d-849c-ffc3c9d1d082",
"name": "Innovaphone IPVA",
"category": "guest",
"description": "The innovaphone PBX is a professional IP telephone system. The IPVA is a software-only solution. It appears as and performs as an innovaphone 'hard-box' excluding DSP-, ISDN-/AB-resources.",
"vendor_name": "Innovaphone",
"vendor_url": "https://www.innovaphone.com/",
"vendor_logo_url": "https://www.innovaphone.com/content/downloads/innovaphone-myapps-logo-short-without-background-screen.png",
"documentation_url": "https://wiki.innovaphone.com/index.php?title=Reference15r1:Concept_Innovaphone_Virtual_Appliance_(IPVA)",
"product_name": "IPVA",
"product_url": "https://www.innovaphone.com/en/products/innovaphone-pbx.html",
"registry_version": 4,
"status": "experimental",
"availability": "free-to-try",
"maintainer": "Thomas Marchsteiner",
"maintainer_email": "thomas.marchsteiner@acp.at",
"usage": "Default user admin/ipva \nDefault network configuration: DHCP client on eth0 with fallback to static address 192.168.0.1/24 after timeout. Static address 192.168.1.1/24 on eth1\n The ova in the zip file contains the disk images to run this appliance. Disableing the dhcp client and setting a static IP is possible with the following commands:\n> config change IP0 ETH0 /addr 192.168.0.1 /mask 255.255.255.0 \n> config change DHCP0 /mode off \n> config write \n> config activate \n> reset",
"symbol": "innovaphone-pbx-green.png",
"first_port_name": "eth0",
"port_name_format": "eth{port1}",
"qemu": {
"adapter_type": "vmxnet3",
"adapters": 2,
"ram": 256,
"cpus": 1,
"hda_disk_interface": "ide",
"hdb_disk_interface": "ide",
"hdc_disk_interface": "ide",
"hdd_disk_interface": "ide",
"arch": "x86_64",
"console_type": "vnc",
"boot_priority": "d",
"kvm": "allow",
"on_close": "power_off",
"process_priority": "normal"
},
"images": [
{
"filename": "ipva-qemu-disk1-14r2.vmdk",
"version": "14r2",
"md5sum": "aaa1c3885eee30ca6ffa3827619e8643",
"filesize": 6269952,
"download_url": "https://store.innovaphone.com/"
},
{
"filename": "ipva-qemu-disk2-14r2.vmdk",
"version": "14r2",
"md5sum": "008a8fc6b0b1e5f11a3e7fd6f22ba349",
"filesize": 72192,
"download_url": "https://store.innovaphone.com/"
},
{
"filename": "ipva-qemu-disk3-14r2.vmdk",
"version": "14r2",
"md5sum": "20516731c480e2112b3fb4a4d7f514f2",
"filesize": 68096,
"download_url": "https://store.innovaphone.com/"
},
{
"filename": "ipva-qemu-disk4-14r2.vmdk",
"version": "14r2",
"md5sum": "15d7d79ef8c28bd29b2eceac8405f964",
"filesize": 68096,
"download_url": "https://store.innovaphone.com/"
}
],
"versions": [
{
"images": {
"hda_disk_image": "ipva-qemu-disk1-14r2.vmdk",
"hdb_disk_image": "ipva-qemu-disk2-14r2.vmdk",
"hdc_disk_image": "ipva-qemu-disk3-14r2.vmdk",
"hdd_disk_image": "ipva-qemu-disk4-14r2.vmdk"
},
"name": "14r2"
}
]
}

View File

@ -436,14 +436,16 @@ class IOUVM(BaseNode):
) )
) )
async def _check_iou_licence(self): def _is_iou_license_check_enabled(self):
""" """
Checks for a valid IOU key in the iourc file (paranoid mode). Returns if IOU license check is enabled.
:return: boolean
""" """
# license check is sent by the controller # license check is sent by the controller
if self.license_check is False: if self.license_check is False:
return return False
try: try:
# we allow license check to be disabled server wide # we allow license check to be disabled server wide
@ -453,7 +455,14 @@ class IOUVM(BaseNode):
if server_wide_license_check is False: if server_wide_license_check is False:
log.warning("License check is explicitly disabled on this server") log.warning("License check is explicitly disabled on this server")
return return False
return True
async def _check_iou_license(self):
"""
Checks for a valid IOU key in the iourc file (paranoid mode).
"""
config = configparser.ConfigParser() config = configparser.ConfigParser()
try: try:
@ -559,15 +568,16 @@ class IOUVM(BaseNode):
except OSError as e: except OSError as e:
raise IOUError(f"Could not rename nvram files: {e}") raise IOUError(f"Could not rename nvram files: {e}")
iourc_path = None
if self._is_iou_license_check_enabled():
iourc_path = self.iourc_path iourc_path = self.iourc_path
if not iourc_path: if not iourc_path:
raise IOUError("Could not find an iourc file (IOU license), please configure an IOU license") raise IOUError("Could not find an iourc file (IOU license), please configure an IOU license")
if not os.path.isfile(iourc_path): if not os.path.isfile(iourc_path):
raise IOUError(f"The iourc path '{iourc_path}' is not a regular file") raise IOUError(f"The iourc path '{iourc_path}' is not a regular file")
await self._check_iou_license()
await self._check_iou_licence()
await self._start_ubridge() await self._start_ubridge()
self._create_netmap_config() self._create_netmap_config()
if self.use_default_iou_values: if self.use_default_iou_values:
# make sure we have the default nvram amount to correctly push the configs # make sure we have the default nvram amount to correctly push the configs
@ -579,7 +589,7 @@ class IOUVM(BaseNode):
self._nvram_watcher = FileWatcher(self._nvram_file(), self._nvram_changed, delay=2) self._nvram_watcher = FileWatcher(self._nvram_file(), self._nvram_changed, delay=2)
# created a environment variable pointing to the iourc file. # created an environment variable pointing to the iourc file.
env = os.environ.copy() env = os.environ.copy()
if "IOURC" not in os.environ and iourc_path: if "IOURC" not in os.environ and iourc_path:
env["IOURC"] = iourc_path env["IOURC"] = iourc_path

View File

@ -64,7 +64,7 @@ class Controller:
self.gns3vm = GNS3VM(self) self.gns3vm = GNS3VM(self)
self.symbols = Symbols() self.symbols = Symbols()
self._appliance_manager = ApplianceManager() self._appliance_manager = ApplianceManager()
self._iou_license_settings = {"iourc_content": "", "license_check": True} self._iou_license_settings = {"iourc_content": "", "license_check": False}
self._vars_loaded = False self._vars_loaded = False
self._vars_file = Config.instance().controller_vars self._vars_file = Config.instance().controller_vars
log.info(f'Loading controller vars file "{self._vars_file}"') log.info(f'Loading controller vars file "{self._vars_file}"')
@ -134,7 +134,9 @@ class Controller:
log.warning(str(e)) log.warning(str(e))
await self.load_projects() await self.load_projects()
await self._project_auto_open()
# start to auto open projects (if configured) 5 seconds after the controller has started
asyncio.get_event_loop().call_later(5, asyncio.create_task, self._project_auto_open())
def _create_ssl_context(self, server_config): def _create_ssl_context(self, server_config):
@ -208,19 +210,15 @@ class Controller:
if self._vars_loaded: if self._vars_loaded:
controller_vars = { controller_vars = {
"appliances_etag": self._appliance_manager.appliances_etag, "appliances_etag": self._appliance_manager.appliances_etag,
"iou_license_check": self._iou_license_settings["license_check"],
"version": __version__ "version": __version__
} }
if self._iou_license_settings["iourc_content"]: if self._iou_license_settings["iourc_content"]:
iou_config = Config.instance().settings.IOU
server_config = Config.instance().settings.Server server_config = Config.instance().settings.Server
if iou_config.iourc_path:
iourc_path = iou_config.iourc_path
else:
os.makedirs(server_config.secrets_dir, exist_ok=True) os.makedirs(server_config.secrets_dir, exist_ok=True)
iourc_path = os.path.join(server_config.secrets_dir, "gns3_iourc_license") iourc_path = os.path.join(server_config.secrets_dir, "iou_license")
try: try:
with open(iourc_path, "w+") as f: with open(iourc_path, "w+") as f:
@ -251,15 +249,11 @@ class Controller:
return [] return []
# load the IOU license settings # load the IOU license settings
iou_config = Config.instance().settings.IOU
server_config = Config.instance().settings.Server server_config = Config.instance().settings.Server
if iou_config.iourc_path:
iourc_path = iou_config.iourc_path
else:
if not server_config.secrets_dir: if not server_config.secrets_dir:
server_config.secrets_dir = os.path.dirname(Config.instance().server_config) server_config.secrets_dir = os.path.dirname(Config.instance().server_config)
iourc_path = os.path.join(server_config.secrets_dir, "gns3_iourc_license") iourc_path = os.path.join(server_config.secrets_dir, "iou_license")
if os.path.exists(iourc_path): if os.path.exists(iourc_path):
try: try:
@ -268,7 +262,10 @@ class Controller:
log.info(f"iourc file '{iourc_path}' loaded") log.info(f"iourc file '{iourc_path}' loaded")
except OSError as e: except OSError as e:
log.error(f"Cannot read IOU license file '{iourc_path}': {e}") log.error(f"Cannot read IOU license file '{iourc_path}': {e}")
self._iou_license_settings["license_check"] = iou_config.license_check
# IOU license check is disabled by default
self._iou_license_settings["license_check"] = controller_vars.get("iou_license_check", False)
log.info("IOU license check is {} on the controller".format("enabled" if self._iou_license_settings["license_check"] else "disabled"))
# install the built-in appliances if needed # install the built-in appliances if needed
if Config.instance().settings.Server.install_builtin_appliances: if Config.instance().settings.Server.install_builtin_appliances:
@ -600,9 +597,12 @@ class Controller:
Auto open the project with auto open enable Auto open the project with auto open enable
""" """
try:
for project in self._projects.values(): for project in self._projects.values():
if project.auto_open: if project.auto_open:
await project.open() await project.open()
except ControllerError as e:
log.error(f"Could not auto open projects: {e}")
def get_free_project_name(self, base_name): def get_free_project_name(self, base_name):
""" """

View File

@ -572,15 +572,11 @@ class Node:
Start a node Start a node
""" """
try: try:
# For IOU we need to send the licence everytime # For IOU: we need to send the licence everytime we start a node
if self.node_type == "iou": if self.node_type == "iou":
license_check = self._project.controller.iou_license.get("license_check", True) license_check = self._project.controller.iou_license.get("license_check", True)
iourc_content = self._project.controller.iou_license.get("iourc_content", None) iourc_content = self._project.controller.iou_license.get("iourc_content", None)
# if license_check and not iourc_content: await self.post("/start", timeout=240, data={"license_check": license_check, "iourc_content": iourc_content})
# raise aiohttp.web.HTTPConflict(text="IOU licence is not configured")
await self.post(
"/start", timeout=240, data={"license_check": license_check, "iourc_content": iourc_content}
)
else: else:
await self.post("/start", data=data, timeout=240) await self.post("/start", data=data, timeout=240)
except asyncio.TimeoutError: except asyncio.TimeoutError:

View File

@ -595,7 +595,7 @@ class Project:
if node_type == "iou": if node_type == "iou":
async with self._iou_id_lock: async with self._iou_id_lock:
# wait for a IOU node to be completely created before adding a new one # wait for an IOU node to be completely created before adding a new one
# this is important otherwise we allocate the same application ID (used # this is important otherwise we allocate the same application ID (used
# to generate MAC addresses) when creating multiple IOU node at the same time # to generate MAC addresses) when creating multiple IOU node at the same time
if "properties" in kwargs.keys(): if "properties" in kwargs.keys():
@ -1339,7 +1339,6 @@ class Project:
Copy the project files directly rather than in an import-export fashion. Copy the project files directly rather than in an import-export fashion.
:param name: Name of the new project. A new one will be generated in case of conflicts :param name: Name of the new project. A new one will be generated in case of conflicts
:param location: Parent directory of the new project
:param reset_mac_addresses: Reset MAC addresses for the new project :param reset_mac_addresses: Reset MAC addresses for the new project
""" """

View File

@ -58,7 +58,7 @@ class CrashReport:
Report crash to a third party service Report crash to a third party service
""" """
DSN = "https://29d15f2b7fde7fbd860843b7ee24dc7f@o19455.ingest.us.sentry.io/38482" DSN = "https://8374a6208714ff37e18725c21a04b8d1@o19455.ingest.us.sentry.io/38482"
_instance = None _instance = None
def __init__(self): def __init__(self):

View File

@ -80,7 +80,7 @@ class ResourcePoolsRepository(BaseRepository):
await self._db_session.commit() await self._db_session.commit()
return result.rowcount > 0 return result.rowcount > 0
async def get_resource_memberships(self, resource_id: UUID) -> List[models.UserGroup]: async def get_resource_memberships(self, resource_id: UUID) -> List[models.ResourcePool]:
""" """
Get all resource memberships in resource pools. Get all resource memberships in resource pools.
""" """

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -46,6 +46,6 @@
gtag('config', 'G-0BT7QQV1W1'); gtag('config', 'G-0BT7QQV1W1');
</script> </script>
<script src="runtime.24fa95b7061d7056.js" type="module"></script><script src="polyfills.319c79dd175e50d0.js" type="module"></script><script src="main.ed82697b58d803e7.js" type="module"></script> <script src="runtime.24fa95b7061d7056.js" type="module"></script><script src="polyfills.319c79dd175e50d0.js" type="module"></script><script src="main.f802edd2b8c6db1d.js" type="module"></script>
</body></html> </body></html>

View File

@ -62,14 +62,12 @@ async def list_images(image_type):
directory = os.path.normpath(directory) directory = os.path.normpath(directory)
for root, _, filenames in _os_walk(directory, recurse=recurse): for root, _, filenames in _os_walk(directory, recurse=recurse):
for filename in filenames: for filename in filenames:
if filename not in files: if filename in files:
log.debug("File {} has already been found, skipping...".format(filename))
continue
if filename.endswith(".md5sum") or filename.startswith("."): if filename.endswith(".md5sum") or filename.startswith("."):
continue continue
elif (
((filename.endswith(".image") or filename.endswith(".bin")) and image_type == "dynamips")
or ((filename.endswith(".bin") or filename.startswith("i86bi")) and image_type == "iou")
or (not filename.endswith(".bin") and not filename.endswith(".image") and image_type == "qemu")
):
files.add(filename) files.add(filename)
# It the image is located in the standard directory the path is relative # It the image is located in the standard directory the path is relative
@ -78,14 +76,26 @@ async def list_images(image_type):
else: else:
path = os.path.relpath(os.path.join(root, filename), default_directory) path = os.path.relpath(os.path.join(root, filename), default_directory)
filesize = os.stat(os.path.join(root, filename)).st_size
if filesize < 7:
log.debug(f"File {filename} is too small to be an image, skipping...")
continue
try: try:
if image_type in ["dynamips", "iou"]:
with open(os.path.join(root, filename), "rb") as f: with open(os.path.join(root, filename), "rb") as f:
# read the first 7 bytes of the file. # read the first 7 bytes of the file.
elf_header_start = f.read(7) elf_header_start = f.read(7)
# valid IOU or IOS images must start with the ELF magic number, be 32-bit or 64-bit, if image_type == "dynamips" and elf_header_start != b'\x7fELF\x01\x02\x01':
# little endian and have an ELF version of 1 # IOS images must start with the ELF magic number, be 32-bit, big endian and have an ELF version of 1
if elf_header_start != b'\x7fELF\x02\x01\x01' and elf_header_start != b'\x7fELF\x01\x01\x01': log.warning(f"IOS image {filename} does not start with a valid ELF magic number, skipping...")
continue
elif image_type == "iou" and elf_header_start != b'\x7fELF\x02\x01\x01' and elf_header_start != b'\x7fELF\x01\x01\x01':
# IOU images must start with the ELF magic number, be 32-bit or 64-bit, little endian and have an ELF version of 1
log.warning(f"IOU image {filename} does not start with a valid ELF magic number, skipping...")
continue
elif image_type == "qemu" and elf_header_start[:4] == b'\x7fELF':
# QEMU images should not start with an ELF magic number
log.warning(f"QEMU image {filename} starts with an ELF magic number, skipping...")
continue continue
images.append( images.append(
@ -93,7 +103,7 @@ async def list_images(image_type):
"filename": filename, "filename": filename,
"path": force_unix_path(path), "path": force_unix_path(path),
"md5sum": await wait_run_in_executor(md5sum, os.path.join(root, filename)), "md5sum": await wait_run_in_executor(md5sum, os.path.join(root, filename)),
"filesize": os.stat(os.path.join(root, filename)).st_size, "filesize": filesize,
} }
) )
except OSError as e: except OSError as e:

View File

@ -22,8 +22,8 @@
# or negative for a release candidate or beta (after the base version # or negative for a release candidate or beta (after the base version
# number has been incremented) # number has been incremented)
__version__ = "3.0.0rc2" __version__ = "3.0.0"
__version_info__ = (3, 0, 0, -99) __version_info__ = (3, 0, 0, 0)
if "dev" in __version__: if "dev" in __version__:
try: try:

View File

@ -146,7 +146,7 @@ def fake_image(tmpdir) -> str:
path = str(tmpdir / "7200.bin") path = str(tmpdir / "7200.bin")
with open(path, "wb+") as f: with open(path, "wb+") as f:
f.write(b'\x7fELF\x01\x01\x01') f.write(b'\x7fELF\x01\x02\x01')
os.chmod(path, stat.S_IREAD) os.chmod(path, stat.S_IREAD)
return path return path
@ -170,7 +170,7 @@ async def test_images(app: FastAPI, compute_client: AsyncClient, tmpdir, fake_im
assert response.json() == [{"filename": "7200.bin", assert response.json() == [{"filename": "7200.bin",
"path": "7200.bin", "path": "7200.bin",
"filesize": 7, "filesize": 7,
"md5sum": "e573e8f5c93c6c00783f20c7a170aa6c"}] "md5sum": "b0d5aa897d937aced5a6b1046e8f7e2e"}]
async def test_upload_image(app: FastAPI, compute_client: AsyncClient, images_dir: str) -> None: async def test_upload_image(app: FastAPI, compute_client: AsyncClient, images_dir: str) -> None:

View File

@ -48,7 +48,7 @@ def fake_qemu_vm(images_dir) -> str:
img_dir = os.path.join(images_dir, "QEMU") img_dir = os.path.join(images_dir, "QEMU")
bin_path = os.path.join(img_dir, "linux载.img") bin_path = os.path.join(img_dir, "linux载.img")
with open(bin_path, "w+") as f: with open(bin_path, "w+") as f:
f.write("1") f.write("1234567")
os.chmod(bin_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) os.chmod(bin_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
return bin_path return bin_path
@ -127,7 +127,7 @@ async def test_qemu_create_with_params(app: FastAPI,
assert response.json()["project_id"] == compute_project.id assert response.json()["project_id"] == compute_project.id
assert response.json()["ram"] == 1024 assert response.json()["ram"] == 1024
assert response.json()["hda_disk_image"] == "linux载.img" assert response.json()["hda_disk_image"] == "linux载.img"
assert response.json()["hda_disk_image_md5sum"] == "c4ca4238a0b923820dcc509a6f75849b" assert response.json()["hda_disk_image_md5sum"] == "fcea920f7412b5da7be0cf42b8c93759"
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -387,7 +387,8 @@ async def test_images(app: FastAPI, compute_client: AsyncClient, fake_qemu_vm) -
response = await compute_client.get(app.url_path_for("compute:get_qemu_images")) response = await compute_client.get(app.url_path_for("compute:get_qemu_images"))
assert response.status_code == status.HTTP_200_OK assert response.status_code == status.HTTP_200_OK
assert {"filename": "linux载.img", "path": "linux载.img", "md5sum": "c4ca4238a0b923820dcc509a6f75849b", "filesize": 1} in response.json() print(response.json())
assert {"filename": "linux载.img", "path": "linux载.img", "md5sum": "fcea920f7412b5da7be0cf42b8c93759", "filesize": 7} in response.json()
async def test_upload_image(app: FastAPI, compute_client: AsyncClient, tmpdir: str) -> None: async def test_upload_image(app: FastAPI, compute_client: AsyncClient, tmpdir: str) -> None:

View File

@ -31,6 +31,9 @@ ALLOWED_CONTROLLER_ENDPOINTS = [
("/", "GET"), ("/", "GET"),
("/debug", "GET"), ("/debug", "GET"),
("/static/web-ui/{file_path:path}", "GET"), ("/static/web-ui/{file_path:path}", "GET"),
("/docs", "GET"),
("/docs/oauth2-redirect", "GET"),
("/redoc", "GET"),
("/v3/version", "GET"), ("/v3/version", "GET"),
("/v3/version", "POST"), ("/v3/version", "POST"),
("/v3/access/users/login", "POST"), ("/v3/access/users/login", "POST"),

View File

@ -92,7 +92,7 @@ async def test_start(vm):
mock_process = MagicMock() mock_process = MagicMock()
vm._check_requirements = AsyncioMagicMock(return_value=True) vm._check_requirements = AsyncioMagicMock(return_value=True)
vm._check_iou_licence = AsyncioMagicMock(return_value=True) vm._check_iou_license = AsyncioMagicMock(return_value=True)
vm._start_ubridge = AsyncioMagicMock(return_value=True) vm._start_ubridge = AsyncioMagicMock(return_value=True)
vm._ubridge_send = AsyncioMagicMock() vm._ubridge_send = AsyncioMagicMock()
@ -104,7 +104,7 @@ async def test_start(vm):
assert vm.command_line == ' '.join(mock_exec.call_args[0]) assert vm.command_line == ' '.join(mock_exec.call_args[0])
assert vm._check_requirements.called assert vm._check_requirements.called
assert vm._check_iou_licence.called assert vm._check_iou_license.called
assert vm._start_ubridge.called assert vm._start_ubridge.called
vm._ubridge_send.assert_any_call("iol_bridge delete IOL-BRIDGE-513") vm._ubridge_send.assert_any_call("iol_bridge delete IOL-BRIDGE-513")
vm._ubridge_send.assert_any_call("iol_bridge create IOL-BRIDGE-513 513") vm._ubridge_send.assert_any_call("iol_bridge create IOL-BRIDGE-513 513")
@ -120,7 +120,8 @@ async def test_start_with_iourc(vm, tmpdir, config):
mock_process = MagicMock() mock_process = MagicMock()
vm._check_requirements = AsyncioMagicMock(return_value=True) vm._check_requirements = AsyncioMagicMock(return_value=True)
vm._check_iou_licence = AsyncioMagicMock(return_value=True) vm._is_iou_license_check_enabled = AsyncioMagicMock(return_value=True)
vm._check_iou_license = AsyncioMagicMock(return_value=True)
vm._start_ioucon = AsyncioMagicMock(return_value=True) vm._start_ioucon = AsyncioMagicMock(return_value=True)
vm._start_ubridge = AsyncioMagicMock(return_value=True) vm._start_ubridge = AsyncioMagicMock(return_value=True)
vm._ubridge_send = AsyncioMagicMock() vm._ubridge_send = AsyncioMagicMock()
@ -157,7 +158,7 @@ async def test_stop(vm):
process = MagicMock() process = MagicMock()
vm._check_requirements = AsyncioMagicMock(return_value=True) vm._check_requirements = AsyncioMagicMock(return_value=True)
vm._check_iou_licence = AsyncioMagicMock(return_value=True) vm._check_iou_license = AsyncioMagicMock(return_value=True)
vm._start_ioucon = AsyncioMagicMock(return_value=True) vm._start_ioucon = AsyncioMagicMock(return_value=True)
vm._start_ubridge = AsyncioMagicMock(return_value=True) vm._start_ubridge = AsyncioMagicMock(return_value=True)
vm._ubridge_send = AsyncioMagicMock() vm._ubridge_send = AsyncioMagicMock()
@ -183,7 +184,7 @@ async def test_reload(vm, fake_iou_bin):
process = MagicMock() process = MagicMock()
vm._check_requirements = AsyncioMagicMock(return_value=True) vm._check_requirements = AsyncioMagicMock(return_value=True)
vm._check_iou_licence = AsyncioMagicMock(return_value=True) vm._check_iou_license = AsyncioMagicMock(return_value=True)
vm._start_ioucon = AsyncioMagicMock(return_value=True) vm._start_ioucon = AsyncioMagicMock(return_value=True)
vm._start_ubridge = AsyncioMagicMock(return_value=True) vm._start_ubridge = AsyncioMagicMock(return_value=True)
vm._ubridge_send = AsyncioMagicMock() vm._ubridge_send = AsyncioMagicMock()
@ -383,42 +384,42 @@ def test_get_legacy_vm_workdir():
async def test_invalid_iou_file(vm, iourc_file): async def test_invalid_iou_file(vm, iourc_file):
hostname = socket.gethostname() hostname = socket.gethostname()
await vm._check_iou_licence() await vm._check_iou_license()
# Missing ; # Missing ;
with pytest.raises(IOUError): with pytest.raises(IOUError):
with open(iourc_file, "w+") as f: with open(iourc_file, "w+") as f:
f.write("[license]\n{} = aaaaaaaaaaaaaaaa".format(hostname)) f.write("[license]\n{} = aaaaaaaaaaaaaaaa".format(hostname))
await vm._check_iou_licence() await vm._check_iou_license()
# Key too short # Key too short
with pytest.raises(IOUError): with pytest.raises(IOUError):
with open(iourc_file, "w+") as f: with open(iourc_file, "w+") as f:
f.write("[license]\n{} = aaaaaaaaaaaaaa;".format(hostname)) f.write("[license]\n{} = aaaaaaaaaaaaaa;".format(hostname))
await vm._check_iou_licence() await vm._check_iou_license()
# Invalid hostname # Invalid hostname
with pytest.raises(IOUError): with pytest.raises(IOUError):
with open(iourc_file, "w+") as f: with open(iourc_file, "w+") as f:
f.write("[license]\nbla = aaaaaaaaaaaaaa;") f.write("[license]\nbla = aaaaaaaaaaaaaa;")
await vm._check_iou_licence() await vm._check_iou_license()
# Missing licence section # Missing licence section
with pytest.raises(IOUError): with pytest.raises(IOUError):
with open(iourc_file, "w+") as f: with open(iourc_file, "w+") as f:
f.write("[licensetest]\n{} = aaaaaaaaaaaaaaaa;") f.write("[licensetest]\n{} = aaaaaaaaaaaaaaaa;")
await vm._check_iou_licence() await vm._check_iou_license()
# Broken config file # Broken config file
with pytest.raises(IOUError): with pytest.raises(IOUError):
with open(iourc_file, "w+") as f: with open(iourc_file, "w+") as f:
f.write("[") f.write("[")
await vm._check_iou_licence() await vm._check_iou_license()
# Missing file # Missing file
with pytest.raises(IOUError): with pytest.raises(IOUError):
os.remove(iourc_file) os.remove(iourc_file)
await vm._check_iou_licence() await vm._check_iou_license()
def test_iourc_content(vm): def test_iourc_content(vm):

View File

@ -217,12 +217,12 @@ async def test_list_images(qemu, tmpdir):
os.makedirs(tmp_images_dir, exist_ok=True) os.makedirs(tmp_images_dir, exist_ok=True)
for image in fake_images: for image in fake_images:
with open(os.path.join(tmp_images_dir, image), "w+") as f: with open(os.path.join(tmp_images_dir, image), "w+") as f:
f.write("1") f.write("1234567")
with patch("gns3server.utils.images.default_images_directory", return_value=str(tmp_images_dir)): with patch("gns3server.utils.images.default_images_directory", return_value=str(tmp_images_dir)):
assert sorted(await qemu.list_images(), key=lambda k: k['filename']) == [ assert sorted(await qemu.list_images(), key=lambda k: k['filename']) == [
{"filename": "a.qcow2", "path": "a.qcow2", "md5sum": "c4ca4238a0b923820dcc509a6f75849b", "filesize": 1}, {"filename": "a.qcow2", "path": "a.qcow2", "md5sum": "fcea920f7412b5da7be0cf42b8c93759", "filesize": 7},
{"filename": "b.qcow2", "path": "b.qcow2", "md5sum": "c4ca4238a0b923820dcc509a6f75849b", "filesize": 1} {"filename": "b.qcow2", "path": "b.qcow2", "md5sum": "fcea920f7412b5da7be0cf42b8c93759", "filesize": 7}
] ]
@ -234,19 +234,19 @@ async def test_list_images_recursives(qemu, tmpdir):
fake_images = ["a.qcow2", "b.qcow2", ".blu.qcow2", "a.qcow2.md5sum"] fake_images = ["a.qcow2", "b.qcow2", ".blu.qcow2", "a.qcow2.md5sum"]
for image in fake_images: for image in fake_images:
with open(os.path.join(tmp_images_dir, image), "w+") as f: with open(os.path.join(tmp_images_dir, image), "w+") as f:
f.write("1") f.write("1234567")
os.makedirs(os.path.join(tmp_images_dir, "c")) os.makedirs(os.path.join(tmp_images_dir, "c"))
fake_images = ["c.qcow2", "c.qcow2.md5sum"] fake_images = ["c.qcow2", "c.qcow2.md5sum"]
for image in fake_images: for image in fake_images:
with open(os.path.join(tmp_images_dir, "c", image), "w+") as f: with open(os.path.join(tmp_images_dir, "c", image), "w+") as f:
f.write("1") f.write("1234567")
with patch("gns3server.utils.images.default_images_directory", return_value=str(tmp_images_dir)): with patch("gns3server.utils.images.default_images_directory", return_value=str(tmp_images_dir)):
assert sorted(await qemu.list_images(), key=lambda k: k['filename']) == [ assert sorted(await qemu.list_images(), key=lambda k: k['filename']) == [
{"filename": "a.qcow2", "path": "a.qcow2", "md5sum": "c4ca4238a0b923820dcc509a6f75849b", "filesize": 1}, {"filename": "a.qcow2", "path": "a.qcow2", "md5sum": "fcea920f7412b5da7be0cf42b8c93759", "filesize": 7},
{"filename": "b.qcow2", "path": "b.qcow2", "md5sum": "c4ca4238a0b923820dcc509a6f75849b", "filesize": 1}, {"filename": "b.qcow2", "path": "b.qcow2", "md5sum": "fcea920f7412b5da7be0cf42b8c93759", "filesize": 7},
{"filename": "c.qcow2", "path": force_unix_path(os.path.sep.join(["c", "c.qcow2"])), "md5sum": "c4ca4238a0b923820dcc509a6f75849b", "filesize": 1} {"filename": "c.qcow2", "path": force_unix_path(os.path.sep.join(["c", "c.qcow2"])), "md5sum": "fcea920f7412b5da7be0cf42b8c93759", "filesize": 7}
] ]

View File

@ -114,64 +114,89 @@ def test_remove_checksum(tmpdir):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_list_images(tmpdir, config): async def test_list_images(tmpdir, config):
path1 = tmpdir / "images1" / "IOS" / "test1.image" # IOS image in the images directory
path1.write(b'\x7fELF\x01\x01\x01', ensure=True) ios_image_1 = tmpdir / "images1" / "IOS" / "ios_image_1.image"
path1 = force_unix_path(str(path1)) ios_image_1.write(b'\x7fELF\x01\x02\x01', ensure=True)
ios_image_1 = force_unix_path(str(ios_image_1))
path2 = tmpdir / "images2" / "test2.image" # IOS image in an additional images path
path2.write(b'\x7fELF\x01\x01\x01', ensure=True) ios_image_2 = tmpdir / "images2" / "ios_image_2.image"
path2 = force_unix_path(str(path2)) ios_image_2.write(b'\x7fELF\x01\x02\x01', ensure=True)
ios_image_2 = force_unix_path(str(ios_image_2))
# Invalid image because not a valid elf file # Not a valid elf file
path = tmpdir / "images2" / "test_invalid.image" not_elf_file = tmpdir / "images1" / "IOS" / "not_elf.image"
path.write(b'NOTANELF', ensure=True) not_elf_file.write(b'NOTANELF', ensure=True)
not_elf_file = force_unix_path(str(not_elf_file))
# Invalid image because it is very small
small_file = tmpdir / "images1" / "too_small.image"
small_file.write(b'1', ensure=True)
if sys.platform.startswith("linux"): if sys.platform.startswith("linux"):
path3 = tmpdir / "images1" / "IOU" / "test3.bin" # 64-bit IOU image
path3.write(b'\x7fELF\x02\x01\x01', ensure=True) iou_image_1 = tmpdir / "images1" / "IOU" / "iou64.bin"
path3 = force_unix_path(str(path3)) iou_image_1.write(b'\x7fELF\x02\x01\x01', ensure=True)
iou_image_1 = force_unix_path(str(iou_image_1))
# 32-bit IOU image
iou_image_2 = tmpdir / "images1" / "IOU" / "iou32.bin"
iou_image_2.write(b'\x7fELF\x01\x01\x01', ensure=True) # 32-bit IOU image
iou_image_2 = force_unix_path(str(iou_image_2))
path4 = tmpdir / "images1" / "QEMU" / "test4.qcow2"
path4.write("1", ensure=True)
path4 = force_unix_path(str(path4))
path5 = tmpdir / "images1" / "QEMU" / "test4.qcow2.md5sum" # Qemu image
path5.write("1", ensure=True) qemu_image_1 = tmpdir / "images1" / "QEMU" / "qemu_image.qcow2"
path5 = force_unix_path(str(path5)) qemu_image_1.write("1234567", ensure=True)
qemu_image_1 = force_unix_path(str(qemu_image_1))
# ELF file inside the Qemu
elf_file = tmpdir / "images1" / "QEMU" / "elf_file.bin"
elf_file.write(b'\x7fELF\x02\x01\x01', ensure=True) # ELF file
elf_file = force_unix_path(str(elf_file))
md5sum_file = tmpdir / "images1" / "QEMU" / "image.qcow2.md5sum"
md5sum_file.write("1", ensure=True)
md5sum_file = force_unix_path(str(md5sum_file))
config.settings.Server.images_path = str(tmpdir / "images1") config.settings.Server.images_path = str(tmpdir / "images1")
config.settings.Server.additional_images_paths = "/tmp/null24564;" + str(tmpdir / "images2") config.settings.Server.additional_images_paths = "/tmp/null24564;" + str(tmpdir / "images2")
assert await list_images("dynamips") == [ assert sorted(await list_images("dynamips"), key=lambda k: k['filename']) == [
{ {
'filename': 'test1.image', 'filename': 'ios_image_1.image',
'filesize': 7, 'filesize': 7,
'md5sum': 'e573e8f5c93c6c00783f20c7a170aa6c', 'md5sum': 'b0d5aa897d937aced5a6b1046e8f7e2e',
'path': 'test1.image' 'path': 'ios_image_1.image'
}, },
{ {
'filename': 'test2.image', 'filename': 'ios_image_2.image',
'filesize': 7, 'filesize': 7,
'md5sum': 'e573e8f5c93c6c00783f20c7a170aa6c', 'md5sum': 'b0d5aa897d937aced5a6b1046e8f7e2e',
'path': str(path2) 'path': str(ios_image_2)
} }
] ]
if sys.platform.startswith("linux"): if sys.platform.startswith("linux"):
assert await list_images("iou") == [ assert sorted(await list_images("iou"), key=lambda k: k['filename']) == [
{ {
'filename': 'test3.bin', 'filename': 'iou32.bin',
'filesize': 7,
'md5sum': 'e573e8f5c93c6c00783f20c7a170aa6c',
'path': 'iou32.bin'
},
{
'filename': 'iou64.bin',
'filesize': 7, 'filesize': 7,
'md5sum': 'c73626d23469519894d58bc98bee9655', 'md5sum': 'c73626d23469519894d58bc98bee9655',
'path': 'test3.bin' 'path': 'iou64.bin'
} }
] ]
assert await list_images("qemu") == [ assert await list_images("qemu") == [
{ {
'filename': 'test4.qcow2', 'filename': 'qemu_image.qcow2',
'filesize': 1, 'filesize': 7,
'md5sum': 'c4ca4238a0b923820dcc509a6f75849b', 'md5sum': 'fcea920f7412b5da7be0cf42b8c93759',
'path': 'test4.qcow2' 'path': 'qemu_image.qcow2'
} }
] ]