reset button interfaces
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, WebSocket, WebSocketDisconnect
|
||||
@@ -16,6 +18,8 @@ from src.utilities.interface_bridge_helpers import get_bridge_ports_once, read_i
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger("network_router")
|
||||
_ETHTOOL_BIN = "/usr/sbin/ethtool" if os.path.exists("/usr/sbin/ethtool") else "ethtool"
|
||||
DEFAULT_INTERFACE_MTU = 1500
|
||||
|
||||
ip: IPRoute | None = None
|
||||
ndb: NDB | None = None
|
||||
@@ -106,6 +110,30 @@ class BridgeLinkStateEnableRequest(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class InterfaceResetDefaultsRequest(BaseModel):
|
||||
"""Payload for resetting one or more interfaces to baseline settings."""
|
||||
|
||||
interfaces: List[str] = Field(..., min_length=1, description="Interface names to reset.")
|
||||
|
||||
|
||||
class InterfaceResetDefaultsResult(BaseModel):
|
||||
"""Outcome for one interface reset attempt."""
|
||||
|
||||
interface: str = Field(..., description="Interface name.")
|
||||
mtu: Optional[int] = Field(None, description="Resulting MTU after reset.")
|
||||
ethernet_profile: Optional[Dict[str, Any]] = Field(
|
||||
None,
|
||||
description="Resulting speed/duplex/autoneg profile when available.",
|
||||
)
|
||||
message: str = Field(..., description="Human-readable reset result.")
|
||||
|
||||
|
||||
class InterfaceResetDefaultsResponse(BaseModel):
|
||||
"""Batch reset response for one or more interfaces."""
|
||||
|
||||
results: List[InterfaceResetDefaultsResult] = Field(default_factory=list)
|
||||
|
||||
|
||||
class BridgeMemberLinkStateInfo(BaseModel):
|
||||
"""Current link-state snapshot for one bridge member."""
|
||||
|
||||
@@ -282,6 +310,38 @@ def publish_network_state_update(reason: str) -> None:
|
||||
logger.exception("Failed to publish network state update")
|
||||
|
||||
|
||||
def _reset_interface_defaults(ifname: str, ip_route: IPRoute) -> InterfaceResetDefaultsResult:
|
||||
"""Reset one interface to a conservative baseline configuration."""
|
||||
idx = iface_index(ifname, ip_route)
|
||||
messages: list[str] = []
|
||||
|
||||
ip_route.link("set", index=idx, state="down")
|
||||
ip_route.link("set", index=idx, mtu=DEFAULT_INTERFACE_MTU)
|
||||
|
||||
try:
|
||||
subprocess.run(
|
||||
[_ETHTOOL_BIN, "-s", ifname, "autoneg", "on"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
messages.append("autoneg on")
|
||||
except (FileNotFoundError, subprocess.CalledProcessError) as exc:
|
||||
logger.debug("Failed to reset ethtool defaults on %s: %s", ifname, exc)
|
||||
messages.append("autoneg unchanged")
|
||||
|
||||
ip_route.link("set", index=idx, state="up")
|
||||
messages.append(f"mtu {DEFAULT_INTERFACE_MTU}")
|
||||
messages.append("admin up")
|
||||
|
||||
return InterfaceResetDefaultsResult(
|
||||
interface=ifname,
|
||||
mtu=DEFAULT_INTERFACE_MTU,
|
||||
ethernet_profile=read_interface_ethernet_profile(ifname),
|
||||
message=", ".join(messages),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/interfaces", response_model=List[InterfaceInfo])
|
||||
def get_interfaces(ip: IPRoute = Depends(get_iproute)) -> List[InterfaceInfo]:
|
||||
"""List host interfaces with addresses and decoded flags."""
|
||||
@@ -422,6 +482,18 @@ def full_state(ip: IPRoute = Depends(get_iproute)) -> FullStateResponse:
|
||||
return _build_full_state_response(ip)
|
||||
|
||||
|
||||
@router.post("/interfaces/reset-defaults", response_model=InterfaceResetDefaultsResponse)
|
||||
def reset_interfaces_to_defaults(
|
||||
req: InterfaceResetDefaultsRequest,
|
||||
ip: IPRoute = Depends(get_iproute),
|
||||
) -> InterfaceResetDefaultsResponse:
|
||||
"""Reset listed interfaces to baseline MTU/autoneg/up settings."""
|
||||
unique_ifaces = list(dict.fromkeys(req.interfaces))
|
||||
results = [_reset_interface_defaults(ifname, ip) for ifname in unique_ifaces]
|
||||
publish_network_state_update("interfaces_reset_defaults")
|
||||
return InterfaceResetDefaultsResponse(results=results)
|
||||
|
||||
|
||||
@router.post("/bridge/create")
|
||||
def create_bridge(req: BridgeCreateRequest, ip: IPRoute = Depends(get_iproute)) -> dict:
|
||||
"""Create a bridge and attach listed interfaces."""
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
BridgeInfo,
|
||||
BridgeRemoveRequest,
|
||||
FullState,
|
||||
InterfaceResetDefaultsRequest,
|
||||
InterfaceResetDefaultsResponse,
|
||||
InterfaceInfo,
|
||||
RouteInfo,
|
||||
} from '../types/network';
|
||||
@@ -82,6 +84,13 @@ export const fetchFullState = async (): Promise<FullState> => {
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const resetInterfaceDefaults = async (
|
||||
req: InterfaceResetDefaultsRequest,
|
||||
): Promise<InterfaceResetDefaultsResponse> => {
|
||||
const res = await api.post<InterfaceResetDefaultsResponse>('/network/interfaces/reset-defaults', req);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const getNetworkStateWebSocketUrl = (): string => {
|
||||
const url = new URL(BASE);
|
||||
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
fetchFullState,
|
||||
getNetworkStateWebSocketUrl,
|
||||
removeBridge,
|
||||
resetInterfaceDefaults,
|
||||
} from '../api/apiClient';
|
||||
import type {
|
||||
EthernetProfile,
|
||||
@@ -34,6 +35,7 @@ import type {
|
||||
BridgeLinkStateWatcherStatus,
|
||||
FullState,
|
||||
InterfaceInfo,
|
||||
InterfaceResetDefaultsResponse,
|
||||
} from '../types/network';
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
@@ -47,6 +49,7 @@ export default function Network() {
|
||||
const [watcherStatuses, setWatcherStatuses] = useState<Record<string, BridgeLinkStateWatcherStatus>>({});
|
||||
const [watcherBridgeName, setWatcherBridgeName] = useState<string>();
|
||||
const [watcherBusyBridge, setWatcherBusyBridge] = useState<string>();
|
||||
const [resetBusyInterface, setResetBusyInterface] = useState<string>();
|
||||
|
||||
const getNetworkPageState = (silent = false) => {
|
||||
fetchFullState()
|
||||
@@ -227,8 +230,24 @@ export default function Network() {
|
||||
<span>—</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Actions',
|
||||
key: 'actions',
|
||||
width: 160,
|
||||
render: (_: unknown, record: InterfaceInfo) => (
|
||||
<Popconfirm
|
||||
title={`Reset ${record.name} to defaults?`}
|
||||
description="Sets MTU to 1500, enables autoneg, and brings the interface up."
|
||||
onConfirm={() => handleResetInterface(record.name)}
|
||||
okText="Reset"
|
||||
cancelText="Cancel"
|
||||
>
|
||||
<Button loading={resetBusyInterface === record.name}>Reset defaults</Button>
|
||||
</Popconfirm>
|
||||
),
|
||||
},
|
||||
],
|
||||
[],
|
||||
[resetBusyInterface],
|
||||
);
|
||||
|
||||
const bridgeColumns: ColumnsType<BridgeInfo> = useMemo(
|
||||
@@ -392,6 +411,31 @@ export default function Network() {
|
||||
});
|
||||
}
|
||||
|
||||
function handleResetInterface(ifname: string) {
|
||||
setResetBusyInterface(ifname);
|
||||
resetInterfaceDefaults({ interfaces: [ifname] })
|
||||
.then((response: InterfaceResetDefaultsResponse) => {
|
||||
const result = response.results[0];
|
||||
notification.success({
|
||||
message: 'Success',
|
||||
description: result?.message
|
||||
? `${ifname}: ${result.message}`
|
||||
: `${ifname} reset to defaults`,
|
||||
});
|
||||
getNetworkPageState(true);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
notification.error({
|
||||
message: 'Error',
|
||||
description: (error as Error).message ?? `Failed to reset ${ifname}`,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setResetBusyInterface(undefined);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: 16 }}>
|
||||
<Row justify="space-between" align="middle" style={{ marginBottom: 12 }}>
|
||||
|
||||
@@ -63,6 +63,21 @@ export interface BridgeLinkStateEnableRequest {
|
||||
recovery_holdoff_seconds: number;
|
||||
}
|
||||
|
||||
export interface InterfaceResetDefaultsRequest {
|
||||
interfaces: string[];
|
||||
}
|
||||
|
||||
export interface InterfaceResetDefaultsResult {
|
||||
interface: string;
|
||||
mtu?: number | null;
|
||||
ethernet_profile?: EthernetProfile | null;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface InterfaceResetDefaultsResponse {
|
||||
results: InterfaceResetDefaultsResult[];
|
||||
}
|
||||
|
||||
export interface BridgeMemberLinkStateInfo {
|
||||
ifname: string;
|
||||
admin_up?: boolean | null;
|
||||
|
||||
Reference in New Issue
Block a user