mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 14:01:15 +02:00
Merge branch '3.1' into base-configs-3.0
This commit is contained in:
40 files changed
+3093
-204
No files matched your search
@@ -63,10 +63,10 @@ class TestLinkRoutes:
|
||||
node1, node2 = nodes
|
||||
|
||||
filters = {
|
||||
"latency": [10],
|
||||
"delay": [10, 0],
|
||||
"frequency_drop": [50]
|
||||
}
|
||||
|
||||
|
||||
with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock:
|
||||
response = await client.post(app.url_path_for("create_link", project_id=project.id), json={
|
||||
"nodes": [
|
||||
@@ -88,7 +88,7 @@ class TestLinkRoutes:
|
||||
],
|
||||
"filters": filters
|
||||
})
|
||||
|
||||
|
||||
assert mock.called
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.json()["link_id"] is not None
|
||||
@@ -250,10 +250,10 @@ class TestLinkRoutes:
|
||||
) -> None:
|
||||
|
||||
filters = {
|
||||
"latency": [10],
|
||||
"delay": [10, 0],
|
||||
"frequency_drop": [50]
|
||||
}
|
||||
|
||||
|
||||
node1, node2 = nodes
|
||||
with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock:
|
||||
response = await client.post(app.url_path_for("create_link", project_id=project.id), json={
|
||||
@@ -315,7 +315,7 @@ class TestLinkRoutes:
|
||||
) -> None:
|
||||
|
||||
filters = {
|
||||
"latency": [10],
|
||||
"delay": [10, 0],
|
||||
"frequency_drop": [50]
|
||||
}
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ class TestRolesPrivilegesRoutes:
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
privileges = await rbac_repo.get_role_privileges(role_in_db.role_id)
|
||||
assert len(privileges) == 25 # 24 default privileges + 1 custom privilege
|
||||
assert len(privileges) == 27 # 25 default privileges + 2 LLMConfig privileges
|
||||
|
||||
async def test_get_role_privileges(
|
||||
self,
|
||||
@@ -143,7 +143,7 @@ class TestRolesPrivilegesRoutes:
|
||||
role_id=role_in_db.role_id)
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()) == 25 # 24 default privileges + 1 custom privilege
|
||||
assert len(response.json()) == 27 # 25 default privileges + 2 LLMConfig privileges
|
||||
|
||||
async def test_remove_privilege_from_role(
|
||||
self,
|
||||
@@ -165,4 +165,4 @@ class TestRolesPrivilegesRoutes:
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
privileges = await rbac_repo.get_role_privileges(role_in_db.role_id)
|
||||
assert len(privileges) == 24 # 24 default privileges
|
||||
assert len(privileges) == 26 # 26 default privileges (24 + 2 LLMConfig)
|
||||
@@ -75,7 +75,7 @@ async def test_json():
|
||||
"scene_height": 1000,
|
||||
"zoom": 100,
|
||||
"show_grid": False,
|
||||
"show_interface_labels": False,
|
||||
"show_interface_labels": True,
|
||||
"show_layers": False,
|
||||
"snap_to_grid": False,
|
||||
"grid_size": 75,
|
||||
|
||||
@@ -21,6 +21,7 @@ import uuid
|
||||
|
||||
from fastapi import FastAPI, status
|
||||
from httpx import AsyncClient
|
||||
from httpx_ws.transport import ASGIWebSocketTransport
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from gns3server.controller import Controller
|
||||
@@ -30,6 +31,7 @@ from gns3server.db.repositories.pools import ResourcePoolsRepository
|
||||
from gns3server.schemas.controller.rbac import ACECreate
|
||||
from gns3server.schemas.controller.pools import ResourceCreate, ResourcePoolCreate
|
||||
from gns3server.db.models import User
|
||||
from gns3server.services import auth_service
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
@@ -168,22 +170,23 @@ class TestResourcePools:
|
||||
self,
|
||||
app: FastAPI,
|
||||
controller: Controller,
|
||||
authorized_client: AsyncClient,
|
||||
db_session: AsyncSession
|
||||
base_client: AsyncClient,
|
||||
db_session: AsyncSession,
|
||||
test_user: User
|
||||
) -> None:
|
||||
|
||||
# Clean up any existing ACEs from previous tests
|
||||
await RbacRepository(db_session).delete_all_ace_starting_with_path("/projects")
|
||||
await RbacRepository(db_session).delete_all_ace_starting_with_path("/pools")
|
||||
|
||||
uuid1 = str(uuid.uuid4())
|
||||
uuid2 = str(uuid.uuid4())
|
||||
uuid3 = str(uuid.uuid4())
|
||||
await controller.add_project(project_id=uuid1, name="Project1")
|
||||
await controller.add_project(project_id=uuid2, name="Project2")
|
||||
await controller.add_project(project_id=uuid3, name="Project3")
|
||||
|
||||
# user has no access to projects (no ACE on /projects or resource pools)
|
||||
response = await authorized_client.get(app.url_path_for("get_projects"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()) == 0
|
||||
await controller.add_project(project_id=uuid1, name="Project1", created_by=test_user.username)
|
||||
await controller.add_project(project_id=uuid2, name="Project2", created_by=test_user.username)
|
||||
await controller.add_project(project_id=uuid3, name="Project3", created_by=test_user.username)
|
||||
|
||||
# Create resource pool and add uuid2 to it
|
||||
pools_repo = ResourcePoolsRepository(db_session)
|
||||
new_resource_pool = ResourcePoolCreate(name="pool2")
|
||||
pool_in_db = await pools_repo.create_resource_pool(new_resource_pool)
|
||||
@@ -194,6 +197,8 @@ class TestResourcePools:
|
||||
|
||||
group_id = (await UsersRepository(db_session).get_user_group_by_name("Users")).user_group_id
|
||||
role_id = (await RbacRepository(db_session).get_role_by_name("User")).role_id
|
||||
|
||||
# Give user access to resource pool only
|
||||
ace = ACECreate(
|
||||
path=f"/pools/{pool_in_db.resource_pool_id}",
|
||||
ace_type="group",
|
||||
@@ -203,40 +208,47 @@ class TestResourcePools:
|
||||
)
|
||||
await RbacRepository(db_session).create_ace(ace)
|
||||
|
||||
response = await authorized_client.get(app.url_path_for("get_project", project_id=uuid2))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["name"] == "Project2"
|
||||
# Create a new client with test user authentication
|
||||
access_token = auth_service.create_access_token(test_user.username)
|
||||
async with AsyncClient(
|
||||
base_url="http://test-api",
|
||||
headers={"Content-Type": "application/json", "Authorization": f"Bearer {access_token}"},
|
||||
transport=ASGIWebSocketTransport(app=app)
|
||||
) as user_client:
|
||||
# user should see only uuid2 (from resource pool)
|
||||
response = await user_client.get(app.url_path_for("get_projects"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
projects = response.json()
|
||||
assert len(projects) == 1
|
||||
assert projects[0]["project_id"] == uuid2
|
||||
|
||||
# user should only see one project because it is in the resource pool he has access to
|
||||
response = await authorized_client.get(app.url_path_for("get_projects"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
projects = response.json()
|
||||
assert len(projects) == 1
|
||||
assert projects[0]["project_id"] == uuid2
|
||||
response = await user_client.get(app.url_path_for("get_project", project_id=uuid2))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["name"] == "Project2"
|
||||
|
||||
ace = ACECreate(
|
||||
path=f"/projects",
|
||||
ace_type="group",
|
||||
propagate=True,
|
||||
group_id=str(group_id),
|
||||
role_id=str(role_id)
|
||||
)
|
||||
await RbacRepository(db_session).create_ace(ace)
|
||||
# Now give user access to /projects (in addition to resource pool)
|
||||
ace = ACECreate(
|
||||
path="/projects",
|
||||
ace_type="group",
|
||||
propagate=True,
|
||||
group_id=str(group_id),
|
||||
role_id=str(role_id)
|
||||
)
|
||||
await RbacRepository(db_session).create_ace(ace)
|
||||
|
||||
# now user should see all projects because he has access to /projects and the resource pool
|
||||
response = await authorized_client.get(app.url_path_for("get_projects"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
projects = response.json()
|
||||
assert len(projects) == 3
|
||||
# user should see all 3 projects: 3 from /projects ACE (uuid2 also in pool but deduplicated)
|
||||
response = await user_client.get(app.url_path_for("get_projects"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
projects = response.json()
|
||||
assert len(projects) == 3
|
||||
|
||||
await RbacRepository(db_session).delete_all_ace_starting_with_path(f"/pools/{pool_in_db.resource_pool_id}")
|
||||
response = await authorized_client.get(app.url_path_for("get_project", project_id=uuid2))
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
# Remove resource pool ACE
|
||||
await RbacRepository(db_session).delete_all_ace_starting_with_path(f"/pools/{pool_in_db.resource_pool_id}")
|
||||
|
||||
# now user should only see the projects that are not in a resource pool
|
||||
response = await authorized_client.get(app.url_path_for("get_projects"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()) == 2
|
||||
# user should still see all 3 projects via /projects ACE (created_by check)
|
||||
response = await user_client.get(app.url_path_for("get_projects"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()) == 3
|
||||
|
||||
|
||||
# class TestProjectsWithRbac:
|
||||
|
||||
@@ -45,7 +45,7 @@ async def test_project_to_topology_empty(tmpdir):
|
||||
"revision": GNS3_FILE_FORMAT_REVISION,
|
||||
"zoom": 100,
|
||||
"show_grid": False,
|
||||
"show_interface_labels": False,
|
||||
"show_interface_labels": True,
|
||||
"show_layers": False,
|
||||
"snap_to_grid": False,
|
||||
"grid_size": 75,
|
||||
|
||||
@@ -46,7 +46,7 @@ async def test_create(project):
|
||||
|
||||
link = UDPLink(project)
|
||||
await link.add_node(node1, 0, 4)
|
||||
await link.update_filters({"latency": [10]})
|
||||
await link.update_filters({"delay": [10, 0]})
|
||||
|
||||
async def compute1_callback(path, data={}, **kwargs):
|
||||
"""
|
||||
@@ -77,7 +77,7 @@ async def test_create(project):
|
||||
"rhost": "192.168.1.2",
|
||||
"rport": 2048,
|
||||
"type": "nio_udp",
|
||||
"filters": {"latency": [10]},
|
||||
"filters": {"delay": [10, 0]},
|
||||
"suspend": False,
|
||||
}, timeout=120)
|
||||
|
||||
@@ -313,7 +313,7 @@ async def test_update(project):
|
||||
|
||||
link = UDPLink(project)
|
||||
await link.add_node(node1, 0, 4)
|
||||
await link.update_filters({"latency": [10]})
|
||||
await link.update_filters({"delay": [10, 0]})
|
||||
|
||||
async def compute1_callback(path, data={}, **kwargs):
|
||||
"""
|
||||
@@ -345,7 +345,7 @@ async def test_update(project):
|
||||
"rport": 2048,
|
||||
"type": "nio_udp",
|
||||
"suspend": False,
|
||||
"filters": {"latency": [10]}
|
||||
"filters": {"delay": [10, 0]}
|
||||
}, timeout=120)
|
||||
|
||||
compute2.post.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/3/ports/1/nio".format(project.id, node2.id), data={
|
||||
@@ -358,7 +358,7 @@ async def test_update(project):
|
||||
}, timeout=120)
|
||||
|
||||
assert link.created
|
||||
await link.update_filters({"drop": [5], "bpf": ["icmp[icmptype] == 8"]})
|
||||
await link.update_filters({"frequency_drop": [5], "bpf": ["icmp[icmptype] == 8"]})
|
||||
compute1.put.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/0/ports/4/nio".format(project.id, node1.id), data={
|
||||
"lport": 1024,
|
||||
"rhost": "192.168.1.2",
|
||||
@@ -366,7 +366,7 @@ async def test_update(project):
|
||||
"type": "nio_udp",
|
||||
"suspend": False,
|
||||
"filters": {
|
||||
"drop": [5],
|
||||
"frequency_drop": [5],
|
||||
"bpf": ["icmp[icmptype] == 8"]
|
||||
}
|
||||
}, timeout=120)
|
||||
@@ -392,7 +392,7 @@ async def test_update_suspend(project):
|
||||
|
||||
link = UDPLink(project)
|
||||
await link.add_node(node1, 0, 4)
|
||||
await link.update_filters({"latency": [10]})
|
||||
await link.update_filters({"frequency_drop": [-1]})
|
||||
await link.update_suspend(True)
|
||||
|
||||
async def compute1_callback(path, data={}, **kwargs):
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Benchmark test for GET /projects performance on a running GNS3 server
|
||||
|
||||
This test connects to an already running GNS3 server and measures
|
||||
the response time of the GET /projects endpoint.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
import argparse
|
||||
import sys
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
|
||||
|
||||
async def create_test_projects(base_url, headers, count, prefix="perf_test_project"):
|
||||
"""Create specified number of test projects"""
|
||||
print(f"Creating {count} test projects...")
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
batch_size = 50
|
||||
|
||||
for batch_start in range(0, count, batch_size):
|
||||
batch_end = min(batch_start + batch_size, count)
|
||||
batch_tasks = []
|
||||
|
||||
for i in range(batch_start, batch_end):
|
||||
name = f"{prefix}_{i}_{uuid.uuid4().hex[:8]}"
|
||||
task = client.post(
|
||||
f"{base_url}/projects",
|
||||
headers=headers,
|
||||
json={"name": name}
|
||||
)
|
||||
batch_tasks.append(task)
|
||||
|
||||
results = await asyncio.gather(*batch_tasks, return_exceptions=True)
|
||||
|
||||
for i, result in enumerate(results, batch_start):
|
||||
if hasattr(result, 'status_code'):
|
||||
if result.status_code == 201:
|
||||
print(f" Created project {i+1}/{count}")
|
||||
else:
|
||||
print(f" Failed to create project {i+1}: {result.status_code}")
|
||||
else:
|
||||
print(f" Error creating project {i+1}: {result}")
|
||||
|
||||
|
||||
async def cleanup_test_projects(base_url, headers, prefix="perf_test_project"):
|
||||
"""Clean up test projects"""
|
||||
print("Cleaning up test projects...")
|
||||
cleanup_start = time.time()
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||
response = await client.get(f"{base_url}/projects", headers=headers)
|
||||
if response.status_code == 200:
|
||||
projects = response.json()
|
||||
|
||||
test_projects = [p for p in projects if p.get('name', '').startswith(prefix)]
|
||||
print(f" Found {len(test_projects)} test projects to delete")
|
||||
|
||||
batch_size = 50
|
||||
total_deleted = 0
|
||||
for batch_start in range(0, len(test_projects), batch_size):
|
||||
batch_end = min(batch_start + batch_size, len(test_projects))
|
||||
batch_tasks = []
|
||||
|
||||
for project in test_projects[batch_start:batch_end]:
|
||||
task = client.delete(
|
||||
f"{base_url}/projects/{project['project_id']}",
|
||||
headers=headers
|
||||
)
|
||||
batch_tasks.append(task)
|
||||
|
||||
results = await asyncio.gather(*batch_tasks, return_exceptions=True)
|
||||
|
||||
for i, result in enumerate(results, batch_start):
|
||||
project = test_projects[i]
|
||||
if hasattr(result, 'status_code') and result.status_code == 204:
|
||||
total_deleted += 1
|
||||
|
||||
elapsed = time.time() - cleanup_start
|
||||
print(f" Deleted {total_deleted} projects in {elapsed:.1f}s ({total_deleted/elapsed:.0f} projects/sec)")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error during cleanup: {type(e).__name__}: {e}")
|
||||
|
||||
|
||||
async def benchmark_get_projects(base_url, headers, project_count, iterations=10, prefix="perf_test_project"):
|
||||
"""Benchmark GET /projects endpoint"""
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"GET /projects Performance Benchmark")
|
||||
print(f"{'='*60}")
|
||||
print(f"Server: {base_url}")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
if project_count > 0:
|
||||
await create_test_projects(base_url, headers, project_count, prefix)
|
||||
|
||||
# Warm-up run
|
||||
print("Performing warm-up run...")
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=120.0) as client:
|
||||
response = await client.get(f"{base_url}/projects", headers=headers)
|
||||
print(f" Response status: {response.status_code}")
|
||||
if response.status_code != 200:
|
||||
print(f"Error during warm-up: HTTP {response.status_code}")
|
||||
print(f"Response text: {response.text[:200]}")
|
||||
return
|
||||
actual_count = len(response.json())
|
||||
print(f" Warm-up successful: {actual_count} projects")
|
||||
except Exception as e:
|
||||
print(f"Error during warm-up: {type(e).__name__}: {e}")
|
||||
return
|
||||
|
||||
# Benchmark runs
|
||||
response_times = []
|
||||
project_counts = []
|
||||
|
||||
print(f"\nRunning {iterations} benchmark iterations...")
|
||||
async with httpx.AsyncClient(timeout=120.0) as client:
|
||||
for i in range(iterations):
|
||||
start_time = time.time()
|
||||
try:
|
||||
response = await client.get(f"{base_url}/projects", headers=headers)
|
||||
end_time = time.time()
|
||||
|
||||
if response.status_code == 200:
|
||||
elapsed_ms = (end_time - start_time) * 1000
|
||||
projects = response.json()
|
||||
response_times.append(elapsed_ms)
|
||||
project_counts.append(len(projects))
|
||||
print(f" Iteration {i+1}: {elapsed_ms:.2f}ms ({len(projects)} projects)")
|
||||
else:
|
||||
print(f" Iteration {i+1}: ERROR {response.status_code}")
|
||||
except Exception as e:
|
||||
print(f" Iteration {i+1}: Exception - {type(e).__name__}: {str(e)}")
|
||||
|
||||
# Cleanup test projects
|
||||
if project_count > 0:
|
||||
await cleanup_test_projects(base_url, headers, prefix)
|
||||
|
||||
# Calculate statistics
|
||||
if response_times:
|
||||
avg_time = sum(response_times) / len(response_times)
|
||||
min_time = min(response_times)
|
||||
max_time = max(response_times)
|
||||
median_time = sorted(response_times)[len(response_times) // 2]
|
||||
avg_project_count = sum(project_counts) / len(project_counts) if project_counts else 0
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
if project_count > 0:
|
||||
print(f"Results for {project_count} projects (created specifically):")
|
||||
else:
|
||||
print(f"Results for existing projects (~{avg_project_count:.0f} per request):")
|
||||
print(f"{'='*60}")
|
||||
print(f"Average: {avg_time:.2f}ms")
|
||||
print(f"Median: {median_time:.2f}ms")
|
||||
print(f"Min: {min_time:.2f}ms")
|
||||
print(f"Max: {max_time:.2f}ms")
|
||||
print(f"{'='*60}")
|
||||
|
||||
# Performance assessment
|
||||
if avg_time < 100:
|
||||
status = "EXCELLENT"
|
||||
elif avg_time < 500:
|
||||
status = "ACCEPTABLE"
|
||||
elif avg_time < 2000:
|
||||
status = "SLOW"
|
||||
else:
|
||||
status = "CRITICAL"
|
||||
|
||||
print(f"Status: {status}")
|
||||
|
||||
# Throughput: projects returned per second
|
||||
if project_counts and sum(response_times) > 0:
|
||||
total_projects = sum(project_counts)
|
||||
total_seconds = sum(response_times) / 1000
|
||||
projects_per_second = total_projects / total_seconds
|
||||
print(f"Throughput: {projects_per_second:.1f} projects/sec")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
return {
|
||||
'project_count': project_count,
|
||||
'avg_time_ms': avg_time,
|
||||
'median_time_ms': median_time,
|
||||
'min_time_ms': min_time,
|
||||
'max_time_ms': max_time,
|
||||
'projects_per_second': projects_per_second if project_counts and sum(response_times) > 0 else 0
|
||||
}
|
||||
|
||||
|
||||
async def main():
|
||||
parser = argparse.ArgumentParser(description='Benchmark GET /projects performance on running server')
|
||||
parser.add_argument('--host', type=str, default='127.0.0.1', help='GNS3 server host')
|
||||
parser.add_argument('--port', type=int, default=3080, help='GNS3 server port')
|
||||
parser.add_argument('--username', type=str, required=True, help='Username for authentication')
|
||||
parser.add_argument('--password', type=str, required=True, help='Password for authentication')
|
||||
parser.add_argument('--project-counts', type=int, nargs='+', default=[10, 50, 100],
|
||||
help='Numbers of projects to test (default: 10 50 100)')
|
||||
parser.add_argument('--iterations', type=int, default=10,
|
||||
help='Number of iterations per test (default: 10)')
|
||||
parser.add_argument('--full-scale', action='store_true',
|
||||
help='Run full scale test: 10, 50, 100, 200, 500 projects')
|
||||
parser.add_argument('--no-cleanup', action='store_true',
|
||||
help='Do not create/cleanup test projects (use existing projects)')
|
||||
parser.add_argument('--prefix', type=str, default='perf_test_project',
|
||||
help='Project name prefix for test/cleanup (default: perf_test_project)')
|
||||
parser.add_argument('--cleanup-only', action='store_true',
|
||||
help='Only clean up test projects, do not run benchmark')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
base_url = f"http://{args.host}:{args.port}/v3"
|
||||
|
||||
# Login to get JWT token
|
||||
print(f"Connecting to {base_url}")
|
||||
print(f"Authenticating as {args.username}...")
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
login_response = await client.post(
|
||||
f"{base_url}/access/users/login",
|
||||
data={"username": args.username, "password": args.password}
|
||||
)
|
||||
|
||||
if login_response.status_code != 200:
|
||||
print(f"Login failed: {login_response.status_code}")
|
||||
print(f"Response: {login_response.text}")
|
||||
sys.exit(1)
|
||||
|
||||
token_data = login_response.json()
|
||||
access_token = token_data.get("access_token")
|
||||
headers = {"Authorization": f"Bearer {access_token}"}
|
||||
print("Authentication successful")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error connecting to server: {e}")
|
||||
print("Make sure GNS3 server is running and accessible")
|
||||
sys.exit(1)
|
||||
|
||||
# Determine test scope
|
||||
if args.full_scale:
|
||||
project_counts = [10, 50, 100, 200, 500]
|
||||
else:
|
||||
project_counts = args.project_counts
|
||||
|
||||
# If cleanup-only flag, just clean up and exit
|
||||
if args.cleanup_only:
|
||||
await cleanup_test_projects(base_url, headers, args.prefix)
|
||||
return
|
||||
|
||||
# If no-cleanup flag, test with existing projects as-is
|
||||
if args.no_cleanup:
|
||||
project_counts = [0]
|
||||
args.iterations = 20
|
||||
|
||||
results = []
|
||||
|
||||
for count in project_counts:
|
||||
try:
|
||||
result = await benchmark_get_projects(base_url, headers, count, args.iterations, args.prefix)
|
||||
if result:
|
||||
results.append(result)
|
||||
except Exception as e:
|
||||
print(f"Error testing {count} projects: {e}")
|
||||
|
||||
# Summary
|
||||
if len(results) > 1:
|
||||
print(f"\n{'='*60}")
|
||||
print("Summary - Performance Scaling")
|
||||
print(f"{'='*60}")
|
||||
print(f"{'Projects':<12} {'Avg (ms)':<12} {'Status':<12}")
|
||||
print(f"{'-'*40}")
|
||||
for r in results:
|
||||
avg = r['avg_time_ms']
|
||||
if avg < 100:
|
||||
status = "EXCELLENT"
|
||||
elif avg < 500:
|
||||
status = "ACCEPTABLE"
|
||||
elif avg < 2000:
|
||||
status = "SLOW"
|
||||
else:
|
||||
status = "CRITICAL"
|
||||
print(f"{r['project_count']:<12} {avg:<12.2f} {status:<12}")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,265 @@
|
||||
"""
|
||||
Unit tests for packet filter validation.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from gns3server.utils.packet_filter_validation import (
|
||||
validate_filter_parameters,
|
||||
validate_all_filters,
|
||||
filter_inactive_filters,
|
||||
FilterValidationError
|
||||
)
|
||||
|
||||
|
||||
class TestPacketFilterValidation:
|
||||
"""Test packet filter parameter validation."""
|
||||
|
||||
def test_frequency_drop_valid(self):
|
||||
"""Test valid frequency drop parameters."""
|
||||
# Valid range: -1 to 32767
|
||||
validate_filter_parameters("frequency_drop", [-1])
|
||||
validate_filter_parameters("frequency_drop", [1])
|
||||
validate_filter_parameters("frequency_drop", [100])
|
||||
validate_filter_parameters("frequency_drop", [32767])
|
||||
|
||||
def test_frequency_drop_invalid(self):
|
||||
"""Test invalid frequency drop parameters."""
|
||||
# Too low
|
||||
with pytest.raises(FilterValidationError, match="between -1 and 32767"):
|
||||
validate_filter_parameters("frequency_drop", [-2])
|
||||
|
||||
# Too high
|
||||
with pytest.raises(FilterValidationError, match="between -1 and 32767"):
|
||||
validate_filter_parameters("frequency_drop", [32768])
|
||||
|
||||
# Wrong type
|
||||
with pytest.raises(FilterValidationError, match="must be an integer"):
|
||||
validate_filter_parameters("frequency_drop", ["invalid"])
|
||||
|
||||
def test_packet_loss_valid(self):
|
||||
"""Test valid packet loss parameters."""
|
||||
# Valid range: 0-100%
|
||||
validate_filter_parameters("packet_loss", [0])
|
||||
validate_filter_parameters("packet_loss", [50])
|
||||
validate_filter_parameters("packet_loss", [100])
|
||||
|
||||
def test_packet_loss_invalid(self):
|
||||
"""Test invalid packet loss parameters."""
|
||||
# Negative
|
||||
with pytest.raises(FilterValidationError, match="between 0 and 100"):
|
||||
validate_filter_parameters("packet_loss", [-1])
|
||||
|
||||
# Over 100%
|
||||
with pytest.raises(FilterValidationError, match="between 0 and 100"):
|
||||
validate_filter_parameters("packet_loss", [101])
|
||||
|
||||
def test_delay_valid(self):
|
||||
"""Test valid delay parameters."""
|
||||
# Valid range: 1-32767ms latency, 0-32767ms jitter
|
||||
validate_filter_parameters("delay", [1, 0])
|
||||
validate_filter_parameters("delay", [100, 50])
|
||||
validate_filter_parameters("delay", [32767, 32767])
|
||||
|
||||
def test_delay_invalid(self):
|
||||
"""Test invalid delay parameters."""
|
||||
# Zero latency (ubridge rejects latency <= 0)
|
||||
with pytest.raises(FilterValidationError, match="between 1 and 32767"):
|
||||
validate_filter_parameters("delay", [0, 0])
|
||||
|
||||
# Negative latency
|
||||
with pytest.raises(FilterValidationError, match="between 1 and 32767"):
|
||||
validate_filter_parameters("delay", [-1, 0])
|
||||
|
||||
# Over max
|
||||
with pytest.raises(FilterValidationError, match="between 1 and 32767"):
|
||||
validate_filter_parameters("delay", [32768, 0])
|
||||
|
||||
# Negative jitter
|
||||
with pytest.raises(FilterValidationError, match="between 0 and 32767"):
|
||||
validate_filter_parameters("delay", [100, -1])
|
||||
|
||||
def test_corrupt_valid(self):
|
||||
"""Test valid corrupt parameters."""
|
||||
# Valid range: 0-100%
|
||||
validate_filter_parameters("corrupt", [0])
|
||||
validate_filter_parameters("corrupt", [50])
|
||||
validate_filter_parameters("corrupt", [100])
|
||||
|
||||
def test_corrupt_invalid(self):
|
||||
"""Test invalid corrupt parameters."""
|
||||
# Over 100%
|
||||
with pytest.raises(FilterValidationError, match="between 0 and 100"):
|
||||
validate_filter_parameters("corrupt", [101])
|
||||
|
||||
def test_bpf_valid(self):
|
||||
"""Test valid BPF parameters."""
|
||||
validate_filter_parameters("bpf", ["tcp port 80"])
|
||||
validate_filter_parameters("bpf", ["tcp and not port 22"])
|
||||
validate_filter_parameters("bpf", [""]) # Empty is valid
|
||||
validate_filter_parameters("bpf", ["host 192.168.1.1 and port 443"])
|
||||
|
||||
def test_bpf_multi_line_valid(self):
|
||||
"""Test valid multi-line BPF expressions."""
|
||||
validate_filter_parameters("bpf", ["tcp port 80\nnot arp"])
|
||||
validate_filter_parameters("bpf", ["tcp and not port 22\nhost 192.168.1.1\nicmp"])
|
||||
|
||||
def test_bpf_multi_line_invalid(self):
|
||||
"""Test multi-line BPF with invalid line."""
|
||||
with pytest.raises(FilterValidationError) as excinfo:
|
||||
validate_filter_parameters("bpf", ["tcp port 80\ninvalid!!!"])
|
||||
err = str(excinfo.value).lower()
|
||||
assert "syntax error" in err
|
||||
|
||||
def test_bpf_invalid(self):
|
||||
"""Test invalid BPF parameters."""
|
||||
# Wrong type
|
||||
with pytest.raises(FilterValidationError, match="must be a string"):
|
||||
validate_filter_parameters("bpf", [123])
|
||||
|
||||
# Invalid BPF syntax
|
||||
with pytest.raises(FilterValidationError) as excinfo:
|
||||
validate_filter_parameters("bpf", ["tcp port"]) # Missing port number
|
||||
assert "syntax error" in str(excinfo.value).lower()
|
||||
|
||||
def test_parameter_count_mismatch(self):
|
||||
"""Test wrong number of parameters."""
|
||||
# frequency_drop expects 1 parameter
|
||||
with pytest.raises(FilterValidationError, match="expects 1 parameter"):
|
||||
validate_filter_parameters("frequency_drop", [])
|
||||
|
||||
with pytest.raises(FilterValidationError, match="expects 1 parameter"):
|
||||
validate_filter_parameters("frequency_drop", [1, 2])
|
||||
|
||||
# delay expects 2 parameters
|
||||
with pytest.raises(FilterValidationError, match="expects 2 parameter"):
|
||||
validate_filter_parameters("delay", [100])
|
||||
|
||||
def test_string_to_int_conversion(self):
|
||||
"""Test string to integer conversion."""
|
||||
# Should work with string numbers
|
||||
validate_filter_parameters("frequency_drop", ["10"])
|
||||
validate_filter_parameters("packet_loss", ["50"])
|
||||
validate_filter_parameters("delay", ["100", "50"])
|
||||
|
||||
def test_validate_all_filters(self):
|
||||
"""Test validating multiple filters at once."""
|
||||
filters = {
|
||||
"frequency_drop": [10],
|
||||
"delay": [100, 50]
|
||||
}
|
||||
validate_all_filters(filters) # Should not raise
|
||||
|
||||
def test_validate_all_filters_with_invalid(self):
|
||||
"""Test validate_all_filters with invalid filter."""
|
||||
filters = {
|
||||
"frequency_drop": [10],
|
||||
"packet_loss": [150] # Invalid: over 100%
|
||||
}
|
||||
with pytest.raises(FilterValidationError):
|
||||
validate_all_filters(filters)
|
||||
|
||||
def test_unknown_filter_type(self):
|
||||
"""Test unknown filter type."""
|
||||
with pytest.raises(FilterValidationError, match="Unknown filter type"):
|
||||
validate_filter_parameters("unknown_filter", [1])
|
||||
|
||||
|
||||
class TestFilterInactiveFilters:
|
||||
"""Test filter_inactive_filters function for smart filter filtering logic."""
|
||||
|
||||
def test_filter_inactive_delay_disabled(self):
|
||||
"""Test delay [0, 0] is filtered out (user wants to disable delay)."""
|
||||
filters = {"delay": [0, 0]}
|
||||
result = filter_inactive_filters(filters)
|
||||
assert result == {} # Should be filtered out
|
||||
|
||||
def test_filter_inactive_delay_invalid_config(self):
|
||||
"""Test delay [0, 100] is kept for validation (invalid config)."""
|
||||
filters = {"delay": [0, 100]}
|
||||
result = filter_inactive_filters(filters)
|
||||
assert result == {"delay": [0, 100]} # Should be kept for validation error
|
||||
|
||||
def test_filter_inactive_delay_normal_config(self):
|
||||
"""Test delay [100, 20] is kept (normal configuration)."""
|
||||
filters = {"delay": [100, 20]}
|
||||
result = filter_inactive_filters(filters)
|
||||
assert result == {"delay": [100, 20]} # Should be kept
|
||||
|
||||
def test_filter_inactive_delay_zero_jitter(self):
|
||||
"""Test delay [100, 0] is kept (normal config with zero jitter)."""
|
||||
filters = {"delay": [100, 0]}
|
||||
result = filter_inactive_filters(filters)
|
||||
assert result == {"delay": [100, 0]} # Should be kept
|
||||
|
||||
def test_filter_inactive_packet_loss_zero(self):
|
||||
"""Test packet_loss [0] is filtered out (disabled)."""
|
||||
filters = {"packet_loss": [0]}
|
||||
result = filter_inactive_filters(filters)
|
||||
assert result == {} # Should be filtered out
|
||||
|
||||
def test_filter_inactive_packet_loss_active(self):
|
||||
"""Test packet_loss [5] is kept (active)."""
|
||||
filters = {"packet_loss": [5]}
|
||||
result = filter_inactive_filters(filters)
|
||||
assert result == {"packet_loss": [5]} # Should be kept
|
||||
|
||||
def test_filter_inactive_corrupt_zero(self):
|
||||
"""Test corrupt [0] is filtered out (disabled)."""
|
||||
filters = {"corrupt": [0]}
|
||||
result = filter_inactive_filters(filters)
|
||||
assert result == {} # Should be filtered out
|
||||
|
||||
def test_filter_inactive_corrupt_active(self):
|
||||
"""Test corrupt [2] is kept (active)."""
|
||||
filters = {"corrupt": [2]}
|
||||
result = filter_inactive_filters(filters)
|
||||
assert result == {"corrupt": [2]} # Should be kept
|
||||
|
||||
def test_filter_inactive_frequency_drop_zero(self):
|
||||
"""Test frequency_drop [0] is filtered out (disabled)."""
|
||||
filters = {"frequency_drop": [0]}
|
||||
result = filter_inactive_filters(filters)
|
||||
assert result == {} # Should be filtered out
|
||||
|
||||
def test_filter_inactive_frequency_drop_active(self):
|
||||
"""Test frequency_drop [10] is kept (active)."""
|
||||
filters = {"frequency_drop": [10]}
|
||||
result = filter_inactive_filters(filters)
|
||||
assert result == {"frequency_drop": [10]} # Should be kept
|
||||
|
||||
def test_filter_inactive_bpf_empty(self):
|
||||
"""Test BPF empty string is filtered out."""
|
||||
filters = {"bpf": [""]}
|
||||
result = filter_inactive_filters(filters)
|
||||
assert result == {} # Should be filtered out
|
||||
|
||||
def test_filter_inactive_bpf_active(self):
|
||||
"""Test BPF with expression is kept."""
|
||||
filters = {"bpf": ["tcp port 80"]}
|
||||
result = filter_inactive_filters(filters)
|
||||
assert result == {"bpf": ["tcp port 80"]} # Should be kept
|
||||
|
||||
def test_filter_inactive_multiple_filters_mixed(self):
|
||||
"""Test multiple filters with mixed active/inactive states."""
|
||||
filters = {
|
||||
"delay": [0, 0], # Disabled: [0, 0]
|
||||
"packet_loss": [0], # Disabled: 0%
|
||||
"corrupt": [2], # Active: 2%
|
||||
"frequency_drop": [10] # Active: every 10th packet
|
||||
}
|
||||
result = filter_inactive_filters(filters)
|
||||
assert result == {
|
||||
"corrupt": [2],
|
||||
"frequency_drop": [10]
|
||||
}
|
||||
|
||||
def test_filter_inactive_empty_filters(self):
|
||||
"""Test empty filters dictionary."""
|
||||
filters = {}
|
||||
result = filter_inactive_filters(filters)
|
||||
assert result == {}
|
||||
|
||||
def test_filter_inactive_none_filters(self):
|
||||
"""Test None filters."""
|
||||
result = filter_inactive_filters(None)
|
||||
assert result == {}
|
||||
Reference in new issue
Block a user