diff --git a/skyline_apiserver/api/v1/extension.py b/skyline_apiserver/api/v1/extension.py index 4c6ab97..a9f2f6f 100644 --- a/skyline_apiserver/api/v1/extension.py +++ b/skyline_apiserver/api/v1/extension.py @@ -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, diff --git a/skyline_apiserver/main.py b/skyline_apiserver/main.py index ece6a1e..d1947e5 100644 --- a/skyline_apiserver/main.py +++ b/skyline_apiserver/main.py @@ -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) diff --git a/skyline_apiserver/tests/unit/api/v1/test_extension.py b/skyline_apiserver/tests/unit/api/v1/test_extension.py index 220284e..8ef5da0 100644 --- a/skyline_apiserver/tests/unit/api/v1/test_extension.py +++ b/skyline_apiserver/tests/unit/api/v1/test_extension.py @@ -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" diff --git a/swagger.json b/swagger.json index 65c1f73..d6cfe32 100644 --- a/swagger.json +++ b/swagger.json @@ -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",