feature: Add fixed ip filter in list_servers

Change-Id: I846f17ab2c27ffecc2c262eca07546eb0504b2c7
Signed-off-by: Wu Wenxiang <wu.wenxiang@99cloud.net>
This commit is contained in:
Wu Wenxiang
2025-08-09 11:35:03 +08:00
parent de037340e8
commit 9d7d38a20a
4 changed files with 144 additions and 9 deletions

View File

@@ -115,6 +115,13 @@ def list_servers(
None, description="Filter the list of servers by the given flavor ID."
),
uuid: str = Query(None, description="Filter the list of servers by the given server UUID."),
ip: Optional[str] = Query(
None,
description=(
"Filter the list of servers by the given IP address (only fixed, not floating). "
"Also passed to Nova API if supported."
),
),
) -> schemas.ServersResponse:
all_projects = all_projects or False
if all_projects:
@@ -158,6 +165,8 @@ def list_servers(
"all_tenants": all_projects,
"uuid": uuid,
}
if ip is not None:
search_opts["ip"] = ip
servers = nova.list_servers(
profile=profile,
session=current_session,

View File

@@ -20,7 +20,8 @@ from pathlib import Path
from typing import AsyncGenerator
import jose
from fastapi import FastAPI, Request, Response, status
from fastapi import FastAPI, Request, status
from fastapi.responses import JSONResponse
from starlette.middleware.cors import CORSMiddleware
from skyline_apiserver.api.v1 import api_router
@@ -93,8 +94,9 @@ async def validate_token(request: Request, call_next):
# Get token from cookie
token = request.cookies.get(CONF.default.session_name)
if not token:
return Response(
content="Unauthorized: Token not found", status_code=status.HTTP_401_UNAUTHORIZED
return JSONResponse(
content={"message": "Unauthorized: Token not found"},
status_code=status.HTTP_401_UNAUTHORIZED,
)
try:
@@ -105,8 +107,8 @@ async def validate_token(request: Request, call_next):
parsed_token = parse_access_token(token)
is_revoked = db_api.check_token(parsed_token.uuid)
if is_revoked:
return Response(
content="Unauthorized: Token revoked",
return JSONResponse(
content={"message": "Unauthorized: Token revoked"},
status_code=status.HTTP_401_UNAUTHORIZED,
)
@@ -136,13 +138,14 @@ async def validate_token(request: Request, call_next):
request.state.new_exp = str(profile.exp)
except jose.exceptions.ExpiredSignatureError as e:
return Response(
content=f"Unauthorized: Token expired - {str(e)}",
return JSONResponse(
content={"message": f"Unauthorized: Token expired - {str(e)}"},
status_code=status.HTTP_401_UNAUTHORIZED,
)
except Exception as e:
return Response(
content=f"Unauthorized: {str(e)}", status_code=status.HTTP_401_UNAUTHORIZED
return JSONResponse(
content={"message": f"Unauthorized: {str(e)}"},
status_code=status.HTTP_401_UNAUTHORIZED,
)
response = await call_next(request)

View File

@@ -245,3 +245,108 @@ class TestListVolumesReal:
assert hasattr(result, "count")
mock_cinder.list_volumes.assert_called_once()
mock_nova.list_servers.assert_called()
class TestListServersReal:
"""Real test cases for list_servers function"""
@pytest.fixture
def mock_profile(self):
profile = Mock()
profile.project.id = "test-project-id"
profile.project.name = "test-project"
profile.region = "test-region"
return profile
@pytest.fixture
def mock_server_data(self):
return {
"id": "server-1",
"name": "vm-1",
"image": None,
"volumes_attached": [],
"project_id": "test-project-id",
}
@patch("skyline_apiserver.api.v1.extension.nova")
@patch("skyline_apiserver.api.v1.extension.glance")
@patch("skyline_apiserver.api.v1.extension.cinder")
@patch("skyline_apiserver.api.v1.extension.keystone")
@patch("skyline_apiserver.api.v1.extension.generate_session")
@patch("skyline_apiserver.api.v1.extension.get_system_session")
@patch("skyline_apiserver.api.v1.extension.OSServer")
@patch("skyline_apiserver.api.v1.extension.Server")
@patch("skyline_apiserver.api.v1.extension.schemas")
def test_list_servers_search_opts(
self,
mock_schemas,
mock_server_wrapper,
mock_osserver_wrapper,
mock_get_system_session,
mock_generate_session,
mock_keystone,
mock_cinder,
mock_glance,
mock_nova,
mock_profile,
mock_server_data,
):
# Setup sessions
mock_system_session = Mock()
mock_current_session = Mock()
mock_get_system_session.return_value = mock_system_session
mock_generate_session.return_value = mock_current_session
# Mock nova.list_servers
server_obj = Mock()
mock_nova.list_servers.return_value = [server_obj]
# Mock wrappers
server_wrapper_obj = Mock()
server_wrapper_obj.to_dict.return_value = mock_server_data
mock_server_wrapper.return_value = server_wrapper_obj
osserver_wrapper_obj = Mock()
osserver_wrapper_obj.to_dict.return_value = mock_server_data
mock_osserver_wrapper.return_value = osserver_wrapper_obj
# Mock schemas.ServersResponse
response_obj = Mock()
response_obj.servers = [mock_server_data]
mock_schemas.ServersResponse.return_value = response_obj
# Import target after patches are ready
from skyline_apiserver.api.v1.extension import list_servers
# Call function with a set of filters to verify passthrough
result = list_servers(
profile=mock_profile,
x_openstack_request_id="req-1",
all_projects=False,
limit=None,
marker=None,
sort_dirs=None,
sort_keys=[],
project_id="should-be-ignored",
project_name=None,
name="vm-1",
status=None,
host="compute-1",
flavor_id="flavor-1",
uuid="uuid-1",
ip="10.0.0.5",
)
assert result is not None
mock_nova.list_servers.assert_called_once()
call_args = mock_nova.list_servers.call_args
assert call_args[1]["session"] == mock_current_session
search_opts = call_args[1]["search_opts"]
# project_id is ignored when all_projects is False
assert search_opts["project_id"] is None
assert search_opts["all_tenants"] is False
assert search_opts["name"] == "vm-1"
assert search_opts["host"] == "compute-1"
assert search_opts["flavor"] == "flavor-1"
assert search_opts["uuid"] == "uuid-1"
assert search_opts["ip"] == "10.0.0.5"

View File

@@ -522,6 +522,24 @@
},
"description": "Filter the list of servers by the given server UUID."
},
{
"name": "ip",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Filter the list of servers by the given IP address (only fixed, not floating). Also passed to Nova API if supported.",
"title": "Ip"
},
"description": "Filter the list of servers by the given IP address (only fixed, not floating). Also passed to Nova API if supported."
},
{
"name": "X-Openstack-Request-Id",
"in": "header",