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:
commit
687ee321a2
18
CHANGELOG
18
CHANGELOG
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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("/")
|
||||||
|
@ -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):
|
||||||
|
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -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": {
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
50
gns3server/appliances/innovaphone-app.gns3a
Normal file
50
gns3server/appliances/innovaphone-app.gns3a
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
78
gns3server/appliances/innovaphone-ipva.gns3a
Normal file
78
gns3server/appliances/innovaphone-ipva.gns3a
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
BIN
gns3server/static/favicon.ico
Normal file
BIN
gns3server/static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
1782
gns3server/static/redoc.standalone.js
Normal file
1782
gns3server/static/redoc.standalone.js
Normal file
File diff suppressed because one or more lines are too long
2
gns3server/static/swagger-ui-bundle.js
Normal file
2
gns3server/static/swagger-ui-bundle.js
Normal file
File diff suppressed because one or more lines are too long
3
gns3server/static/swagger-ui.css
Normal file
3
gns3server/static/swagger-ui.css
Normal file
File diff suppressed because one or more lines are too long
@ -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>
|
File diff suppressed because one or more lines are too long
@ -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:
|
||||||
|
@ -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:
|
||||||
|
@ -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:
|
||||||
|
@ -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:
|
||||||
|
@ -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"),
|
||||||
|
@ -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):
|
||||||
|
@ -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}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -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'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
Loading…
Reference in New Issue
Block a user