Merge pull request #2635 from yueguobin/fix/iou-suspend-405

fix(iou): return 405 error for unsupported suspend operation
This commit is contained in:
Jeremy Grossmann 2026-03-15 22:47:42 +08:00 committed by GitHub
commit 632b3daab7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 204 additions and 67 deletions

View File

@ -20,7 +20,7 @@ API routes for ATM switch nodes.
import os
from fastapi import APIRouter, Depends, Body, Path, Response, status
from fastapi import APIRouter, Depends, Body, Path, status, HTTPException
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from uuid import UUID
@ -121,20 +121,24 @@ async def delete_atm_switch_node(node: ATMSwitch = Depends(dep_node)) -> None:
def start_atm_switch(node: ATMSwitch = Depends(dep_node)) -> None:
"""
Start an ATM switch node.
This endpoint results in no action since ATM switch nodes are always on.
"""
pass
raise HTTPException(
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
detail="Start is not supported for ATM switches"
)
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
def stop_atm_switch(node: ATMSwitch = Depends(dep_node)) -> None:
"""
Stop an ATM switch node.
This endpoint results in no action since ATM switch nodes are always on.
"""
pass
raise HTTPException(
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
detail="Stop is not supported for ATM switches"
)
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
@ -144,7 +148,10 @@ def suspend_atm_switch(node: ATMSwitch = Depends(dep_node)) -> None:
This endpoint results in no action since ATM switch nodes are always on.
"""
pass
raise HTTPException(
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
detail="Suspend is not supported for ATM switches"
)
@router.post(

View File

@ -20,7 +20,7 @@ API routes for cloud nodes.
import os
from fastapi import APIRouter, Depends, Path, Response, status
from fastapi import APIRouter, Depends, Path, status, HTTPException
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from typing import Union
@ -120,10 +120,12 @@ async def start_cloud(node: Cloud = Depends(dep_node)) -> None:
async def stop_cloud(node: Cloud = Depends(dep_node)) -> None:
"""
Stop a cloud node.
This endpoint results in no action since cloud nodes cannot be stopped.
"""
pass
raise HTTPException(
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
detail="Stop is not supported for cloud nodes"
)
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
@ -133,7 +135,10 @@ async def suspend_cloud(node: Cloud = Depends(dep_node)) -> None:
This endpoint results in no action since cloud nodes cannot be suspended.
"""
pass
raise HTTPException(
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
detail="Suspend is not supported for cloud nodes"
)
@router.post(

View File

@ -20,7 +20,7 @@ API routes for Ethernet hub nodes.
import os
from fastapi import APIRouter, Depends, Body, Path, Response, status
from fastapi import APIRouter, Depends, Body, Path, status, HTTPException
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from uuid import UUID
@ -123,27 +123,34 @@ def start_ethernet_hub(node: EthernetHub = Depends(dep_node)) -> None:
This endpoint results in no action since Ethernet hub nodes are always on.
"""
pass
raise HTTPException(
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
detail="Start is not supported for Ethernet hubs"
)
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
def stop_ethernet_hub(node: EthernetHub = Depends(dep_node)) -> None:
"""
Stop an Ethernet hub.
This endpoint results in no action since Ethernet hub nodes are always on.
"""
pass
raise HTTPException(
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
detail="Stop is not supported for Ethernet hubs"
)
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
def suspend_ethernet_hub(node: EthernetHub = Depends(dep_node)) -> None:
"""
Suspend an Ethernet hub.
This endpoint results in no action since Ethernet hub nodes are always on.
"""
pass
raise HTTPException(
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
detail="Suspend is not supported for Ethernet hubs"
)
@router.post(

View File

@ -20,7 +20,7 @@ API routes for Ethernet switch nodes.
import os
from fastapi import APIRouter, Depends, Body, Path, Response, status
from fastapi import APIRouter, Depends, Body, Path, status, HTTPException
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from uuid import UUID
@ -124,30 +124,36 @@ async def delete_ethernet_switch(node: EthernetSwitch = Depends(dep_node)) -> No
def start_ethernet_switch(node: EthernetSwitch = Depends(dep_node)) -> None:
"""
Start an Ethernet switch.
This endpoint results in no action since Ethernet switch nodes are always on.
"""
pass
raise HTTPException(
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
detail="Start is not supported for Ethernet switches"
)
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
def stop_ethernet_switch(node: EthernetSwitch = Depends(dep_node)) -> None:
"""
Stop an Ethernet switch.
This endpoint results in no action since Ethernet switch nodes are always on.
"""
pass
raise HTTPException(
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
detail="Stop is not supported for Ethernet switches"
)
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
def suspend_ethernet_switch(node: EthernetSwitch = Depends(dep_node)) -> None:
"""
Suspend an Ethernet switch.
This endpoint results in no action since Ethernet switch nodes are always on.
"""
pass
raise HTTPException(
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
detail="Suspend is not supported for Ethernet switches"
)
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
@ -157,7 +163,10 @@ def reload_ethernet_switch(node: EthernetSwitch = Depends(dep_node)) -> None:
This endpoint results in no action since Ethernet switch nodes are always on.
"""
pass
raise HTTPException(
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
detail="Reload is not supported for Ethernet switches"
)
@router.post(

View File

@ -20,7 +20,7 @@ API routes for Frame Relay switch nodes.
import os
from fastapi import APIRouter, Depends, Body, Path, Response, status
from fastapi import APIRouter, Depends, Body, Path, status, HTTPException
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from uuid import UUID
@ -124,30 +124,36 @@ async def delete_frame_relay_switch(node: FrameRelaySwitch = Depends(dep_node))
def start_frame_relay_switch(node: FrameRelaySwitch = Depends(dep_node)) -> None:
"""
Start a Frame Relay switch node.
This endpoint results in no action since Frame Relay switch nodes are always on.
"""
pass
raise HTTPException(
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
detail="Start is not supported for Frame Relay switches"
)
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
def stop_frame_relay_switch(node: FrameRelaySwitch = Depends(dep_node)) -> None:
"""
Stop a Frame Relay switch node.
This endpoint results in no action since Frame Relay switch nodes are always on.
"""
pass
raise HTTPException(
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
detail="Stop is not supported for Frame Relay switches"
)
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
def suspend_frame_relay_switch(node: FrameRelaySwitch = Depends(dep_node)) -> None:
"""
Suspend a Frame Relay switch node.
This endpoint results in no action since Frame Relay switch nodes are always on.
"""
pass
raise HTTPException(
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
detail="Suspend is not supported for Frame Relay switches"
)
@router.post(

View File

@ -20,7 +20,7 @@ API routes for IOU nodes.
import os
from fastapi import APIRouter, WebSocket, Depends, Body, status
from fastapi import APIRouter, WebSocket, Depends, Body, status, HTTPException
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from typing import Union
@ -186,17 +186,18 @@ async def stop_iou_node(node: IOUVM = Depends(dep_node)) -> None:
@router.post(
"/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
"/{node_id}/suspend",
dependencies=[Depends(compute_authentication)]
)
def suspend_iou_node(node: IOUVM = Depends(dep_node)) -> None:
"""
Suspend an IOU node.
Does nothing since IOU doesn't support being suspended.
"""
pass
raise HTTPException(
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
detail="Suspend is not supported for IOU nodes"
)
@router.post(

View File

@ -20,7 +20,7 @@ API routes for NAT nodes.
import os
from fastapi import APIRouter, Depends, Path, Response, status
from fastapi import APIRouter, Depends, Path, status, HTTPException
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from typing import Union
@ -115,20 +115,24 @@ async def start_nat_node(node: Nat = Depends(dep_node)) -> None:
async def stop_nat_node(node: Nat = Depends(dep_node)) -> None:
"""
Stop a NAT node.
This endpoint results in no action since cloud nodes cannot be stopped.
"""
pass
raise HTTPException(
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
detail="Stop is not supported for NAT nodes"
)
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
async def suspend_nat_node(node: Nat = Depends(dep_node)) -> None:
"""
Suspend a NAT node.
This endpoint results in no action since NAT nodes cannot be suspended.
"""
pass
raise HTTPException(
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
detail="Suspend is not supported for NAT nodes"
)
@router.post(

View File

@ -20,7 +20,7 @@ API routes for VPCS nodes.
import os
from fastapi import APIRouter, WebSocket, Depends, Body, Path, status
from fastapi import APIRouter, WebSocket, Depends, Body, Path, status, HTTPException
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from typing import Union
@ -174,10 +174,12 @@ async def stop_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
async def suspend_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
"""
Suspend a VPCS node.
Does nothing, suspend is not supported by VPCS.
"""
pass
raise HTTPException(
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
detail="Suspend is not supported for VPCS nodes"
)
@router.post(

View File

@ -22,7 +22,7 @@ import aiohttp
import asyncio
import ipaddress
from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect, Request, Response, status, Query
from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect, Request, Response, status, Query, HTTPException
from fastapi.encoders import jsonable_encoder
from fastapi.routing import APIRoute
from typing import List, Callable, Optional
@ -189,7 +189,11 @@ async def start_all_nodes(project: Project = Depends(dep_project)) -> None:
Required privilege: Node.PowerMgmt
"""
await project.start_all()
try:
await project.start_all()
except HTTPException as e:
if not e.status_code == status.HTTP_405_METHOD_NOT_ALLOWED:
raise
@router.post("/stop", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Node.PowerMgmt"))])
@ -200,7 +204,11 @@ async def stop_all_nodes(project: Project = Depends(dep_project)) -> None:
Required privilege: Node.PowerMgmt
"""
await project.stop_all()
try:
await project.stop_all()
except HTTPException as e:
if not e.status_code == status.HTTP_405_METHOD_NOT_ALLOWED:
raise
@router.post("/suspend", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Node.PowerMgmt"))])
@ -211,7 +219,11 @@ async def suspend_all_nodes(project: Project = Depends(dep_project)) -> None:
Required privilege: Node.PowerMgmt
"""
await project.suspend_all()
try:
await project.suspend_all()
except HTTPException as e:
if not e.status_code == status.HTTP_405_METHOD_NOT_ALLOWED:
raise
@router.post("/reload", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Node.PowerMgmt"))])
@ -222,8 +234,12 @@ async def reload_all_nodes(project: Project = Depends(dep_project)) -> None:
Required privilege: Node.PowerMgmt
"""
await project.stop_all()
await project.start_all()
try:
await project.stop_all()
await project.start_all()
except HTTPException as e:
if not e.status_code == status.HTTP_405_METHOD_NOT_ALLOWED:
raise
@router.get("/{node_id}", response_model=schemas.Node, dependencies=[Depends(has_privilege("Node.Audit"))])
@ -310,8 +326,11 @@ async def start_node(start_data: dict, node: Node = Depends(dep_node)) -> None:
Required privilege: Node.PowerMgmt
"""
await node.start(data=start_data)
try:
await node.start(data=start_data)
except HTTPException as e:
if not e.status_code == status.HTTP_405_METHOD_NOT_ALLOWED:
raise
@router.post(
"/{node_id}/stop",
@ -325,7 +344,11 @@ async def stop_node(node: Node = Depends(dep_node)) -> None:
Required privilege: Node.PowerMgmt
"""
await node.stop()
try:
await node.stop()
except HTTPException as e:
if not e.status_code == status.HTTP_405_METHOD_NOT_ALLOWED:
raise
@router.post(
@ -340,7 +363,11 @@ async def suspend_node(node: Node = Depends(dep_node)) -> None:
Required privilege: Node.PowerMgmt
"""
await node.suspend()
try:
await node.suspend()
except HTTPException as e:
if not e.status_code == status.HTTP_405_METHOD_NOT_ALLOWED:
raise
@router.post(
@ -355,8 +382,11 @@ async def reload_node(node: Node = Depends(dep_node)) -> None:
Required privilege: Node.PowerMgmt
"""
await node.reload()
try:
await node.reload()
except HTTPException as e:
if not e.status_code == status.HTTP_405_METHOD_NOT_ALLOWED:
raise
@router.post(
"/{node_id}/isolate",

View File

@ -163,10 +163,50 @@ class TestCloudNodesRoutes:
vm: dict
) -> None:
response = await compute_client.delete(app.url_path_for("compute:delete_cloud", project_id=vm["project_id"], node_id=vm["node_id"]))
response = await compute_client.delete(
app.url_path_for(
"compute:delete_cloud",
project_id=vm["project_id"],
node_id=vm["node_id"]
)
)
assert response.status_code == status.HTTP_204_NO_CONTENT
async def test_cloud_stop(
self, app: FastAPI,
compute_client: AsyncClient,
compute_project: Project,
vm: dict
) -> None:
response = await compute_client.post(
app.url_path_for(
"compute:stop_cloud",
project_id=vm["project_id"],
node_id=vm["node_id"]
)
)
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
async def test_cloud_suspend(
self, app: FastAPI,
compute_client: AsyncClient,
compute_project: Project,
vm: dict
) -> None:
response = await compute_client.post(
app.url_path_for(
"compute:suspend_cloud",
project_id=vm["project_id"],
node_id=vm["node_id"]
)
)
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
async def test_cloud_update(
self, app: FastAPI,
compute_client: AsyncClient,

View File

@ -326,9 +326,10 @@ class TestEthernetSwitchNodesRoutes:
app.url_path_for(
"compute:start_ethernet_switch",
project_id=ethernet_switch["project_id"],
node_id=ethernet_switch["node_id"])
node_id=ethernet_switch["node_id"]
)
)
assert response.status_code == status.HTTP_204_NO_CONTENT
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
async def test_ethernet_switch_stop(
@ -341,9 +342,10 @@ class TestEthernetSwitchNodesRoutes:
app.url_path_for(
"compute:stop_ethernet_switch",
project_id=ethernet_switch["project_id"],
node_id=ethernet_switch["node_id"])
node_id=ethernet_switch["node_id"]
)
)
assert response.status_code == status.HTTP_204_NO_CONTENT
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
async def test_ethernet_switch_suspend(
@ -356,9 +358,10 @@ class TestEthernetSwitchNodesRoutes:
app.url_path_for(
"compute:suspend_ethernet_switch",
project_id=ethernet_switch["project_id"],
node_id=ethernet_switch["node_id"])
node_id=ethernet_switch["node_id"]
)
)
assert response.status_code == status.HTTP_204_NO_CONTENT
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
async def test_ethernet_switch_reload(
@ -371,9 +374,10 @@ class TestEthernetSwitchNodesRoutes:
app.url_path_for(
"compute:reload_ethernet_switch",
project_id=ethernet_switch["project_id"],
node_id=ethernet_switch["node_id"])
node_id=ethernet_switch["node_id"]
)
)
assert response.status_code == status.HTTP_204_NO_CONTENT
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
async def test_ethernet_switch_create_udp(

View File

@ -246,6 +246,16 @@ class TestIOUNodesRoutes:
assert response.status_code == status.HTTP_204_NO_CONTENT
async def test_iou_suspend(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None:
response = await compute_client.delete(
app.url_path_for(
"compute:suspend_iou_node",
project_id=vm["project_id"],
node_id=vm["node_id"])
)
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
async def test_iou_update(
self, app: FastAPI,
compute_client: AsyncClient,

View File

@ -219,8 +219,20 @@ class TestVPCSNodesRoutes:
node_id=vm["node_id"]))
assert mock.called
assert response.status_code == status.HTTP_204_NO_CONTENT
async def test_vpcs_suspend(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None:
response = await compute_client.post(
app.url_path_for(
"compute:suspend_vpcs_node",
project_id=vm["project_id"],
node_id=vm["node_id"]
)
)
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
async def test_vpcs_duplicate(
self,
app: FastAPI,