reset button interfaces
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, WebSocket, WebSocketDisconnect
|
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()
|
router = APIRouter()
|
||||||
logger = logging.getLogger("network_router")
|
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
|
ip: IPRoute | None = None
|
||||||
ndb: NDB | 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):
|
class BridgeMemberLinkStateInfo(BaseModel):
|
||||||
"""Current link-state snapshot for one bridge member."""
|
"""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")
|
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])
|
@router.get("/interfaces", response_model=List[InterfaceInfo])
|
||||||
def get_interfaces(ip: IPRoute = Depends(get_iproute)) -> List[InterfaceInfo]:
|
def get_interfaces(ip: IPRoute = Depends(get_iproute)) -> List[InterfaceInfo]:
|
||||||
"""List host interfaces with addresses and decoded flags."""
|
"""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)
|
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")
|
@router.post("/bridge/create")
|
||||||
def create_bridge(req: BridgeCreateRequest, ip: IPRoute = Depends(get_iproute)) -> dict:
|
def create_bridge(req: BridgeCreateRequest, ip: IPRoute = Depends(get_iproute)) -> dict:
|
||||||
"""Create a bridge and attach listed interfaces."""
|
"""Create a bridge and attach listed interfaces."""
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
BridgeInfo,
|
BridgeInfo,
|
||||||
BridgeRemoveRequest,
|
BridgeRemoveRequest,
|
||||||
FullState,
|
FullState,
|
||||||
|
InterfaceResetDefaultsRequest,
|
||||||
|
InterfaceResetDefaultsResponse,
|
||||||
InterfaceInfo,
|
InterfaceInfo,
|
||||||
RouteInfo,
|
RouteInfo,
|
||||||
} from '../types/network';
|
} from '../types/network';
|
||||||
@@ -82,6 +84,13 @@ export const fetchFullState = async (): Promise<FullState> => {
|
|||||||
return res.data;
|
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 => {
|
export const getNetworkStateWebSocketUrl = (): string => {
|
||||||
const url = new URL(BASE);
|
const url = new URL(BASE);
|
||||||
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
fetchFullState,
|
fetchFullState,
|
||||||
getNetworkStateWebSocketUrl,
|
getNetworkStateWebSocketUrl,
|
||||||
removeBridge,
|
removeBridge,
|
||||||
|
resetInterfaceDefaults,
|
||||||
} from '../api/apiClient';
|
} from '../api/apiClient';
|
||||||
import type {
|
import type {
|
||||||
EthernetProfile,
|
EthernetProfile,
|
||||||
@@ -34,6 +35,7 @@ import type {
|
|||||||
BridgeLinkStateWatcherStatus,
|
BridgeLinkStateWatcherStatus,
|
||||||
FullState,
|
FullState,
|
||||||
InterfaceInfo,
|
InterfaceInfo,
|
||||||
|
InterfaceResetDefaultsResponse,
|
||||||
} from '../types/network';
|
} from '../types/network';
|
||||||
|
|
||||||
const { Title, Paragraph } = Typography;
|
const { Title, Paragraph } = Typography;
|
||||||
@@ -47,6 +49,7 @@ export default function Network() {
|
|||||||
const [watcherStatuses, setWatcherStatuses] = useState<Record<string, BridgeLinkStateWatcherStatus>>({});
|
const [watcherStatuses, setWatcherStatuses] = useState<Record<string, BridgeLinkStateWatcherStatus>>({});
|
||||||
const [watcherBridgeName, setWatcherBridgeName] = useState<string>();
|
const [watcherBridgeName, setWatcherBridgeName] = useState<string>();
|
||||||
const [watcherBusyBridge, setWatcherBusyBridge] = useState<string>();
|
const [watcherBusyBridge, setWatcherBusyBridge] = useState<string>();
|
||||||
|
const [resetBusyInterface, setResetBusyInterface] = useState<string>();
|
||||||
|
|
||||||
const getNetworkPageState = (silent = false) => {
|
const getNetworkPageState = (silent = false) => {
|
||||||
fetchFullState()
|
fetchFullState()
|
||||||
@@ -227,8 +230,24 @@ export default function Network() {
|
|||||||
<span>—</span>
|
<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(
|
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 (
|
return (
|
||||||
<div style={{ padding: 16 }}>
|
<div style={{ padding: 16 }}>
|
||||||
<Row justify="space-between" align="middle" style={{ marginBottom: 12 }}>
|
<Row justify="space-between" align="middle" style={{ marginBottom: 12 }}>
|
||||||
|
|||||||
@@ -63,6 +63,21 @@ export interface BridgeLinkStateEnableRequest {
|
|||||||
recovery_holdoff_seconds: number;
|
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 {
|
export interface BridgeMemberLinkStateInfo {
|
||||||
ifname: string;
|
ifname: string;
|
||||||
admin_up?: boolean | null;
|
admin_up?: boolean | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user