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:
@@ -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,
|
||||||
|
@@ -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)
|
||||||
|
@@ -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"
|
||||||
|
18
swagger.json
18
swagger.json
@@ -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",
|
||||||
|
Reference in New Issue
Block a user