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." 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."), 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: ) -> schemas.ServersResponse:
all_projects = all_projects or False all_projects = all_projects or False
if all_projects: if all_projects:
@@ -158,6 +165,8 @@ def list_servers(
"all_tenants": all_projects, "all_tenants": all_projects,
"uuid": uuid, "uuid": uuid,
} }
if ip is not None:
search_opts["ip"] = ip
servers = nova.list_servers( servers = nova.list_servers(
profile=profile, profile=profile,
session=current_session, session=current_session,

View File

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

View File

@@ -245,3 +245,108 @@ class TestListVolumesReal:
assert hasattr(result, "count") assert hasattr(result, "count")
mock_cinder.list_volumes.assert_called_once() mock_cinder.list_volumes.assert_called_once()
mock_nova.list_servers.assert_called() 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." "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", "name": "X-Openstack-Request-Id",
"in": "header", "in": "header",