diff --git a/pom.xml b/pom.xml index d891a32..2127f8c 100644 --- a/pom.xml +++ b/pom.xml @@ -52,6 +52,12 @@ runtime + + com.baomidou + mybatis-plus-jsqlparser + ${mybatis-plus.version} + + org.springdoc springdoc-openapi-starter-webmvc-ui diff --git a/src/main/java/com/cisd/tms/common/config/MybatisPlusConfig.java b/src/main/java/com/cisd/tms/common/config/MybatisPlusConfig.java index 96d9499..54aaf7c 100644 --- a/src/main/java/com/cisd/tms/common/config/MybatisPlusConfig.java +++ b/src/main/java/com/cisd/tms/common/config/MybatisPlusConfig.java @@ -1,6 +1,8 @@ package com.cisd.tms.common.config; +import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -9,6 +11,8 @@ public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { - return new MybatisPlusInterceptor(); + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + return interceptor; } } diff --git a/src/main/java/com/cisd/tms/common/config/WebMvcConfig.java b/src/main/java/com/cisd/tms/common/config/WebMvcConfig.java index 05d5f36..d3d4917 100644 --- a/src/main/java/com/cisd/tms/common/config/WebMvcConfig.java +++ b/src/main/java/com/cisd/tms/common/config/WebMvcConfig.java @@ -1,24 +1,26 @@ package com.cisd.tms.common.config; +import com.cisd.tms.common.util.TraceIdUtil; import com.cisd.tms.modules.auth.security.InternalAuthorizationInterceptor; +import com.cisd.tms.security.internal.CachedBodyHttpServletRequest; import com.cisd.tms.security.internal.InternalApiAuthInterceptor; import com.cisd.tms.security.internal.InternalApiReplayInterceptor; -import com.cisd.tms.security.internal.CachedBodyHttpServletRequest; import com.cisd.tms.security.openapi.OpenApiSignAuthInterceptor; -import com.cisd.tms.common.util.TraceIdUtil; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; import org.slf4j.MDC; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.io.IOException; + @Configuration public class WebMvcConfig implements WebMvcConfigurer { @@ -117,7 +119,7 @@ public class WebMvcConfig implements WebMvcConfigurer { .allowedOriginPatterns("*") .allowedMethods("*") .allowedHeaders("*") - .exposedHeaders("X-Trace-Id") + .exposedHeaders("X-Trace-Id", HttpHeaders.CONTENT_DISPOSITION) .allowCredentials(true) .maxAge(3600); } diff --git a/src/main/java/com/cisd/tms/modules/device/controller/NetworkConfigController.java b/src/main/java/com/cisd/tms/modules/device/controller/NetworkConfigController.java index 2610cce..ecd0d4b 100644 --- a/src/main/java/com/cisd/tms/modules/device/controller/NetworkConfigController.java +++ b/src/main/java/com/cisd/tms/modules/device/controller/NetworkConfigController.java @@ -2,23 +2,21 @@ package com.cisd.tms.modules.device.controller; import com.cisd.tms.common.api.ApiResponse; -import com.cisd.tms.common.enums.ErrorCode; -import com.cisd.tms.common.exception.BizException; import com.cisd.tms.modules.device.dto.network.*; +import com.cisd.tms.modules.device.entity.PageResult; import com.cisd.tms.modules.device.service.NetworkConfigService; import com.cisd.tms.modules.log.annotation.AuditedOperation; import com.cisd.tms.modules.log.enums.ActionType; import com.cisd.tms.modules.log.enums.ModuleCode; -import com.cisd.tms.security.internal.ReplayProtected; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import java.util.List; - @Tag(name = "网络配置管理", description = "提供设备网络信息查询、IPv4/IPv6配置、Bond配置及路由管理等接口") @RestController -@RequestMapping("/api/v1/device/network-config") +@RequestMapping("/api/v1/device/network") public class NetworkConfigController { private final NetworkConfigService networkConfigService; @@ -27,158 +25,88 @@ public class NetworkConfigController { this.networkConfigService = networkConfigService; } - - @Operation(summary = "获取网络信息", description = "获取当前设备的所有网络连接信息") - @GetMapping("/network-info") - public ApiResponse> getNetworkInfo(){ - return ApiResponse.success(networkConfigService.getNetworkInfo()); + @Operation(summary = "分页查询网卡配置", description = "分页查询设备网卡配置列表,支持按网卡名称、状态等条件筛选") + @PostMapping("/card/Page") + public ApiResponse> getNetworkTablePage(@RequestBody NetworkTableQueryRequest request) { + PageResult pageData = networkConfigService.getNetworkTablePage(request); + return ApiResponse.success(pageData); } - - @Operation(summary = "获取IPv4配置信息", description = "根据设备名称获取指定网络接口的IPv4配置详情") - @GetMapping("/ipv4-info/{deviceName}") - public ApiResponse getIpv4Info(@PathVariable String deviceName){ - return ApiResponse.success(networkConfigService.getIpv4Info(deviceName)); - } - - - @Operation(summary = "设置IPv4配置", description = "为指定网络接口配置IPv4地址、掩码、网关等信息") - @PostMapping("/ipv4-config/set") - @ReplayProtected - @AuditedOperation(module = ModuleCode.NETWORK, action = ActionType.CREATE, summary = "设置IPv4配置") - public ApiResponse setIpv4Config(@RequestBody Ipv4ConfigRequest req) { - networkConfigService.setIpv4Config(req); + @Operation(summary = "配置网卡", description = "配置指定网卡的 IPv4 或 IPv6 地址、前缀长度、网关和状态") + @PostMapping("/card/configure") + public ApiResponse configureNetworkCard(@RequestBody @Validated NetworkConfigureRequest request) { + networkConfigService.configureNetworkCard(request); return ApiResponse.success(); } - @Operation(summary = "获取IPv6配置信息", description = "根据设备名称获取指定网络接口的IPv6配置详情") - @GetMapping("/ipv6-info/{deviceName}") - public ApiResponse getIpv6Info(@PathVariable String deviceName){ - return ApiResponse.success(networkConfigService.getIpv6Info(deviceName)); - } - - - @Operation(summary = "设置IPv6配置", description = "为指定网络接口配置IPv6地址、前缀长度、网关等信息") - @PostMapping("/ipv6-config/set") - @ReplayProtected - @AuditedOperation(module = ModuleCode.NETWORK, action = ActionType.CREATE, summary = "设置IPv6配置") - public ApiResponse setIpv6Config(@RequestBody Ipv6ConfigRequest req) { - networkConfigService.setIpv6Config(req); + @Operation(summary = "新增网卡配置", description = "新增或配置指定网卡的网络地址信息") + @PostMapping("/card/add") + public ApiResponse addNetworkCard(@RequestBody @Validated NetworkConfigureRequest req) { + networkConfigService.configureNetworkCard(req); return ApiResponse.success(); } - @Operation(summary = "获取所有Bond名称", description = "返回当前系统中所有已创建的Bond接口名称") - @GetMapping("/all-bond-name") - public ApiResponse> getAllBondName(){ - return ApiResponse.success(networkConfigService.getAllBondName()); - } - - @Operation(summary = "创建Bond", description = "创建一个新的Bond聚合接口,需指定名称、模式和从属接口") - @PostMapping("/bond/create") - @ReplayProtected - @AuditedOperation(module = ModuleCode.NETWORK, action = ActionType.CREATE, summary = "创建Bond") - public ApiResponse createBond(@RequestBody BondCreateRequest req) { - networkConfigService.createBond(req); + @Operation(summary = "删除网卡 IP 配置", description = "删除指定网卡下的某条 IPv4 或 IPv6 地址配置") + @PostMapping("/card/delete") + public ApiResponse deleteNetworkCardRecord(@RequestBody NetworkDeleteRequest req) { + networkConfigService.deleteNetworkCardRecord(req); return ApiResponse.success(); } + @Operation(summary = "分页查询 Bond 配置", description = "分页查询 Bond 聚合接口配置列表,包含 Bond 名称、模式、IP、网关和状态") + @PostMapping("/bond/page") + public ApiResponse> getBondPage(@RequestBody BondTableRequest req) { + return ApiResponse.success(networkConfigService.getBondPage(req)); + } - @Operation(summary = "删除Bond", description = "根据Bond名称删除指定的聚合接口") + + @Operation(summary = "删除 Bond", description = "删除指定 Bond 聚合接口及其从属网卡连接配置") @PostMapping("/bond/delete/{bondName}") - @ReplayProtected - @AuditedOperation(module = ModuleCode.NETWORK, action = ActionType.DELETE, summary = "删除Bond") - public ApiResponse deleteBond(@PathVariable("bondName") String bondName){ + public ApiResponse deleteBond(@PathVariable String bondName) { networkConfigService.deleteBond(bondName); return ApiResponse.success(); } - @Operation(summary = "添加从属接口到Bond", description = "向指定Bond中添加一个或多个从属网络接口") - @PostMapping("/bond-slave/add") - @ReplayProtected - @AuditedOperation(module = ModuleCode.NETWORK, action = ActionType.UPDATE, summary = "添加从属接口到Bond") - public ApiResponse addSlavesTOBond(@RequestBody BondAddSlavesRequest req) { - networkConfigService.addSlavesTOBond(req); + + @Operation(summary = "创建Bond", description = "创建一个新的Bond聚合接口,需指定名称、模式和从属接口") + @PostMapping("/bond/create") + @AuditedOperation(module = ModuleCode.NETWORK, action = ActionType.CREATE, summary = "创建Bond") + public ApiResponse createBond(@Valid @RequestBody BondCreateRequest req) { + networkConfigService.createBond(req); return ApiResponse.success(); } - @Operation(summary = "从Bond中移除从属接口", description = "从指定Bond中移除一个或多个从属网络接口") - @PostMapping("/bond-slave/remove") - @ReplayProtected - @AuditedOperation(module = ModuleCode.NETWORK, action = ActionType.DELETE, summary = "从Bond中移除从属接口") - public ApiResponse removeSlaveFromBond(@RequestBody BondRemoveSlaveRequest req) { - String resultMessage = networkConfigService.removeSlaveFromBond(req); - return ApiResponse.success(resultMessage); - } - - @Operation(summary = "获取Bond的从属接口列表", description = "根据Bond名称返回其所有从属接口名称") - @GetMapping("/bond-slave/{bondName}") - public ApiResponse> getBondSlaves(@PathVariable String bondName){ - return ApiResponse.success(networkConfigService.getBondSlaves(bondName)); - } - - - @Operation(summary = "获取Bond模式", description = "根据Bond名称查询当前的绑定模式") - @GetMapping("/bond/mode/{bondName}") - public ApiResponse getBondMode(@PathVariable("bondName") String bondName) { - - String mode = networkConfigService.getBondMode(bondName); - - if (mode == null) { - throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "未找到该连接的 Bond 模式"); - } - - return ApiResponse.success(mode); - } - - @Operation(summary = "设置Bond模式", description = "修改指定Bond的绑定模式") - @PostMapping("/bond/mode/set") - @ReplayProtected - @AuditedOperation(module = ModuleCode.NETWORK, action = ActionType.UPDATE, summary = "设置Bond模式") - public ApiResponse setBondMode(@RequestBody BondModifyModeRequest req) { - networkConfigService.setBondMode(req); + @Operation(summary = "配置 Bond", description = "修改指定 Bond 的模式、IP 地址、网关、从属物理网卡和激活状态") + @PostMapping("/bond/config") + public ApiResponse configBond(@Valid @RequestBody BondConfigRequest req) { + networkConfigService.configBond(req); return ApiResponse.success(); } - @Operation(summary = "获取路由表", description = "返回当前系统的IPv4/IPv6路由表信息") - @GetMapping("/routes/routingTable") - public ApiResponse> getRoutingTable() { - List routes = networkConfigService.getRoutingTable(); - return ApiResponse.success(routes); + @Operation(summary = "分页查询路由配置", description = "分页查询静态路由列表,支持 IPv4/IPv6、目标类型和状态筛选") + @PostMapping("/route/page") + public ApiResponse> routePage(@RequestBody RouteTableRequest req) { + return ApiResponse.success(networkConfigService.getRoutePage(req)); } - @Operation(summary = "设置默认路由", description = "配置或修改系统的默认网关路由") - @PostMapping("/routes/default/set") - @ReplayProtected - @AuditedOperation(module = ModuleCode.NETWORK, action = ActionType.UPDATE, summary = "设置默认路由") - public ApiResponse setDefaultRoute(@RequestBody SetDefaultRouteRequest req) { - networkConfigService.setDefaultRoute(req); + @Operation(summary = "新增路由", description = "新增一条 IPv4 或 IPv6 静态路由配置,可指定目标地址、前缀长度、网口和下一跳") + @PostMapping("/route/add") + public ApiResponse routeAdd(@Valid @RequestBody RouteCreateRequest req) { + networkConfigService.addRoute(req); return ApiResponse.success(); } - @Operation(summary = "添加静态路由", description = "新增一条静态路由规则") - @PostMapping("/routes/static/add") - @ReplayProtected - @AuditedOperation(module = ModuleCode.NETWORK, action = ActionType.UPDATE, summary = "添加静态路由") - public ApiResponse addStaticRoute(@RequestBody AddStaticRouteRequest req) { - networkConfigService.addStaticRoute(req); + @Operation(summary = "修改路由", description = "修改一条已有静态路由,通过删除旧路由并新增新路由实现") + @PostMapping("/route/update") + public ApiResponse routeUpdate(@Valid @RequestBody RouteUpdateRequest req) { + networkConfigService.updateRoute(req); return ApiResponse.success(); } - @Operation(summary = "删除默认路由", description = "删除指定的默认路由") - @PostMapping("/routes/default/delete") - @ReplayProtected - @AuditedOperation(module = ModuleCode.NETWORK, action = ActionType.DELETE, summary = "删除默认路由") - public ApiResponse deleteDefaultRoute(@RequestBody DeleteDefaultRouteRequest req) { - networkConfigService.deleteDefaultRoute(req); - return ApiResponse.success(); - } - - @Operation(summary = "删除静态路由", description = "删除指定的静态路由") - @PostMapping("/routes/static/delete") - @ReplayProtected - @AuditedOperation(module = ModuleCode.NETWORK, action = ActionType.DELETE, summary = "删除静态路由") - public ApiResponse deleteStaticRoute(@RequestBody DeleteStaticRouteRequest req) { - networkConfigService.deleteStaticRoute(req); + @Operation(summary = "删除路由", description = "删除指定网口下的一条 IPv4 或 IPv6 静态路由配置") + @PostMapping("/route/delete") + public ApiResponse routeDelete(@Valid @RequestBody RouteDeleteRequest req) { + networkConfigService.deleteRoute(req); return ApiResponse.success(); } } diff --git a/src/main/java/com/cisd/tms/modules/device/dto/network/AddStaticRouteRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/network/AddStaticRouteRequest.java deleted file mode 100644 index 7e298eb..0000000 --- a/src/main/java/com/cisd/tms/modules/device/dto/network/AddStaticRouteRequest.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.cisd.tms.modules.device.dto.network; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -@Data -@Schema(description = "添加静态路由请求参数") -public class AddStaticRouteRequest { - - @Schema(description = "网络设备名称(如 eth0、bond0)", example = "eth0") - private String deviceName; - @Schema(description = "目标网络CIDR", example = "192.168.1.0/24") - private String targetCidr; - @Schema(description = "下一跳地址(网关IP)", example = "192.168.1.1") - private String nextHop; -} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/device/dto/network/BondConfigRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/network/BondConfigRequest.java new file mode 100644 index 0000000..399d552 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/network/BondConfigRequest.java @@ -0,0 +1,77 @@ +package com.cisd.tms.modules.device.dto.network; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +@Data +@Schema(description = "Bond 配置请求") +public class BondConfigRequest { + + /** + * Bond 名称,例如 bond0 + */ + @Schema(description = "Bond 名称,例如 bond0", example = "bond0", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "Bond名称不能为空") + private String bondName; + + /** + * Bond 模式 + * 0: balance-rr + * 1: active-backup + * 2: balance-xor + * 4: 802.3ad + */ + @Schema( + description = "Bond 模式:0=balance-rr平衡轮询,1=active-backup主备,2=balance-xor平衡异或,4=802.3ad动态链路聚合", + example = "1", + requiredMode = Schema.RequiredMode.REQUIRED + ) + @NotNull(message = "Bond类型不能为空") + private Integer mode; + + /** + * 地址类型:IPv4 / IPv6 + */ + @Schema(description = "地址类型:IPv4 或 IPv6", example = "IPv4", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "地址类型不能为空") + private String addressType; + + /** + * 网络地址 + */ + @Schema(description = "网络地址,不包含前缀长度", example = "192.168.10.100", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "网络地址不能为空") + private String ipAddress; + + /** + * 前缀长度 + */ + @Schema(description = "前缀长度,IPv4 范围 0-32,IPv6 范围 0-128", example = "24", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "前缀长度不能为空") + private Integer prefixLength; + + /** + * 网关地址,可为空 + */ + @Schema(description = "网关地址,可为空", example = "192.168.10.1") + private String gateway; + + /** + * 选择的物理网卡 + */ + @Schema(description = "选择加入 Bond 的物理网卡列表", example = "[\"eth0\", \"eth1\"]", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "请选择至少一个物理网卡") + private List slaveList; + + /** + * Active / Inactive + */ + @Schema(description = "Bond 状态:Active=激活,Inactive=未激活", example = "Active", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "状态不能为空") + private String status; +} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/device/dto/network/BondCreateRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/network/BondCreateRequest.java index 94ae083..68aff31 100644 --- a/src/main/java/com/cisd/tms/modules/device/dto/network/BondCreateRequest.java +++ b/src/main/java/com/cisd/tms/modules/device/dto/network/BondCreateRequest.java @@ -1,32 +1,95 @@ package com.cisd.tms.modules.device.dto.network; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; import lombok.Data; +import java.util.List; + @Data @Schema(description = "创建Bond请求参数") public class BondCreateRequest { + /** - * Bond 名称,例如 "bond0" + * bond名称,例如 bond0 */ - @Schema(description = "Bond 名称", example = "bond0") + @Schema( + description = "Bond名称,例如 bond0、bond1", + example = "bond0", + requiredMode = Schema.RequiredMode.REQUIRED + ) + @NotBlank(message = "Bond名称不能为空") private String bondName; /** - * 模式 (例如: 0, 1, 2, 4) - * 0: balance-rr (轮询) - * 1: active-backup (主备) + * bond模式 + * 0: balance-rr + * 1: active-backup * 2: balance-xor - * 4: 802.3ad (动态链路聚合) + * 4: 802.3ad */ - @Schema(description = "Bond 模式(0: balance-rr 轮询, 1: active-backup 主备, 2: balance-xor, 4: 802.3ad 动态链路聚合" - ,example = "1") + @Schema( + description = "Bond模式:0=balance-rr平衡轮询,1=active-backup主备,2=balance-xor平衡异或,4=802.3ad动态链路聚合", + example = "1", + allowableValues = {"0", "1", "2", "4"}, + requiredMode = Schema.RequiredMode.REQUIRED + ) + @NotNull(message = "Bond类型不能为空") private Integer mode; /** - * 可选的 IPv4 配置 - * 如果前端传了该对象,创建 Bond 后会自动配置 IP + * 地址类型:IPv4 / IPv6 */ - @Schema(description = "可选的 IPv4 配置,如果提供该对象,创建 Bond 后会自动配置 IP") - private Ipv4ConfigRequest ipv4Config; + @Schema( + description = "地址类型:IPv4 或 IPv6", + example = "IPv4", + allowableValues = {"IPv4", "IPv6"}, + requiredMode = Schema.RequiredMode.REQUIRED + ) + @NotBlank(message = "地址类型不能为空") + private String addressType; + + /** + * 网络地址 + */ + @Schema( + description = "网络地址,不包含前缀长度", + example = "192.168.10.100", + requiredMode = Schema.RequiredMode.REQUIRED + ) + @NotBlank(message = "网络地址不能为空") + private String ipAddress; + + /** + * 前缀长度 + */ + @Schema( + description = "前缀长度,IPv4范围0-32,IPv6范围0-128", + example = "24", + requiredMode = Schema.RequiredMode.REQUIRED + ) + @NotNull(message = "前缀长度不能为空") + private Integer prefixLength; + + /** + * 网关地址,可为空 + */ + @Schema( + description = "网关地址,可为空", + example = "192.168.10.1" + ) + private String gateway; + + /** + * 选择的物理网卡,例如 eth0、eth1 + */ + @Schema( + description = "选择加入Bond的物理网卡列表", + example = "[\"eth0\", \"eth1\"]", + requiredMode = Schema.RequiredMode.REQUIRED + ) + @NotEmpty(message = "请选择至少一个物理网卡") + private List slaveList; } \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/device/dto/network/BondModifyModeRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/network/BondModifyModeRequest.java deleted file mode 100644 index 1d888e5..0000000 --- a/src/main/java/com/cisd/tms/modules/device/dto/network/BondModifyModeRequest.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.cisd.tms.modules.device.dto.network; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -@Data -@Schema(description = "修改bond模式请求参数") -public class BondModifyModeRequest { - - @Schema(description = "Bond 名称", example = "bond0") - private String bondName; - @Schema(description = "Bond 模式(0: balance-rr 轮询, 1: active-backup 主备, 2: balance-xor, 4: 802.3ad 动态链路聚合" - ,example = "1") - private Integer mode; -} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/device/dto/network/BondTableRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/network/BondTableRequest.java new file mode 100644 index 0000000..fd5bc55 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/network/BondTableRequest.java @@ -0,0 +1,37 @@ +package com.cisd.tms.modules.device.dto.network; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "Bond分页查询请求参数") +public class BondTableRequest { + + /** + * Bond 名称 + */ + @Schema(description = "Bond名称,支持按Bond名称筛选", example = "bond0") + private String bondName; + + /** + * 状态:Active / Inactive + */ + @Schema( + description = "Bond状态:Active=激活,Inactive=未激活", + example = "Active", + allowableValues = {"Active", "Inactive"} + ) + private String status; + + /** + * 页码 + */ + @Schema(description = "页码,从1开始", example = "1") + private Integer pageNum = 1; + + /** + * 每页条数 + */ + @Schema(description = "每页条数", example = "10") + private Integer pageSize = 10; +} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/device/dto/network/BondTableResponse.java b/src/main/java/com/cisd/tms/modules/device/dto/network/BondTableResponse.java new file mode 100644 index 0000000..51261ec --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/network/BondTableResponse.java @@ -0,0 +1,62 @@ +package com.cisd.tms.modules.device.dto.network; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "Bond分页查询响应数据") +public class BondTableResponse { + + /** + * bond名称 + */ + @Schema(description = "Bond名称", example = "bond0") + private String bondName; + + /** + * bond模型 + */ + @Schema( + description = "Bond模式", + example = "bond1-主备" + ) + private String bondMode; + + /** + * 地址类型:IPV4 / IPV6 + */ + @Schema( + description = "地址类型:IPV4 或 IPV6", + example = "IPV4", + allowableValues = {"IPV4", "IPV6", "-"} + ) + private String addressType; + + /** + * 地址 + */ + @Schema(description = "IP地址", example = "192.168.10.100") + private String address; + + /** + * 前缀长度 + */ + @Schema(description = "前缀长度", example = "24") + private String prefixLength; + + /** + * 网关 + */ + @Schema(description = "网关地址", example = "192.168.10.1") + private String gateway; + + /** + * 状态:Active / Inactive + */ + @Schema( + description = "Bond状态:Active=激活,Inactive=未激活", + example = "Active", + allowableValues = {"Active", "Inactive"} + ) + private String status; +} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/device/dto/network/DeleteDefaultRouteRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/network/DeleteDefaultRouteRequest.java deleted file mode 100644 index 8b3ca59..0000000 --- a/src/main/java/com/cisd/tms/modules/device/dto/network/DeleteDefaultRouteRequest.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.cisd.tms.modules.device.dto.network; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -@Data -@Schema(description = "删除默认路由请求参数") -public class DeleteDefaultRouteRequest { - @Schema(description = "网络设备名称(如 eth0、bond0)", example = "eth0") - private String deviceName; -} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/device/dto/network/DeleteStaticRouteRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/network/DeleteStaticRouteRequest.java deleted file mode 100644 index ddcf1d7..0000000 --- a/src/main/java/com/cisd/tms/modules/device/dto/network/DeleteStaticRouteRequest.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.cisd.tms.modules.device.dto.network; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -@Data -@Schema(description = "删除静态路由请求参数") -public class DeleteStaticRouteRequest { - - @Schema(description = "网络设备名称", example = "eth0") - private String deviceName; - - @Schema(description = "目标网络CIDR", example = "192.168.1.0/24") - private String targetCidr; - - @Schema(description = "下一跳地址(网关IP)", example = "192.168.1.1") - private String nextHop; -} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/device/dto/network/Ipv4ConfigRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/network/Ipv4ConfigRequest.java deleted file mode 100644 index 921b965..0000000 --- a/src/main/java/com/cisd/tms/modules/device/dto/network/Ipv4ConfigRequest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.cisd.tms.modules.device.dto.network; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -@Data -@Schema(description = "IPv4配置请求参数") -public class Ipv4ConfigRequest { - @Schema(description = "网络设备名称", example = "eth0") - private String deviceName; - @Schema(description = "IPv4地址", example = "192.168.1.100") - private String ipv4; - @Schema(description = "子网掩码长度", example = "24") - private String maskLength; - @Schema(description = "默认网关地址", example = "192.168.1.1") - private String gateway; -} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/device/dto/network/Ipv4InfoResponse.java b/src/main/java/com/cisd/tms/modules/device/dto/network/Ipv4InfoResponse.java deleted file mode 100644 index adb07f3..0000000 --- a/src/main/java/com/cisd/tms/modules/device/dto/network/Ipv4InfoResponse.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.cisd.tms.modules.device.dto.network; - - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -@Data -@Schema(description = "IPv4配置信息响应") -public class Ipv4InfoResponse { - @Schema(description = "IPv4地址", example = "192.168.1.100") - private String ipv4; - @Schema(description = "子网掩码长度", example = "24") - private String maskLength; - @Schema(description = "默认网关地址", example = "192.168.1.1") - private String gateway; - @Schema(description = "IP获取方式(如 static、dhcp)", example = "static") - private String method; -} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/network/Ipv6AddressItem.java b/src/main/java/com/cisd/tms/modules/device/dto/network/Ipv6AddressItem.java deleted file mode 100644 index e32fa7f..0000000 --- a/src/main/java/com/cisd/tms/modules/device/dto/network/Ipv6AddressItem.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.cisd.tms.modules.device.dto.network; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -@Data -@Schema(description = "单个IPv6地址及掩码") -public class Ipv6AddressItem { - @Schema(description = "IPv6地址", example = "2001:db8::2") - private String ipv6; - @Schema(description = "子网掩码前缀长度", example = "64") - private String maskLength; -} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/network/Ipv6ConfigRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/network/Ipv6ConfigRequest.java deleted file mode 100644 index df9f0aa..0000000 --- a/src/main/java/com/cisd/tms/modules/device/dto/network/Ipv6ConfigRequest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.cisd.tms.modules.device.dto.network; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -@Data -@Schema(description = "IPv6配置请求参数") -public class Ipv6ConfigRequest { - @Schema(description = "网络设备名称(如 eth0、bond0)", example = "eth0") - private String deviceName; - @Schema(description = "IPv6地址", example = "2001:db8::1") - private String ipv6; - @Schema(description = "默认网关地址", example = "2001:db8::1") - private String gateway; - @Schema(description = "子网前缀长度(如 64)", example = "64") - private String maskLength; -} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/network/Ipv6InfoResponse.java b/src/main/java/com/cisd/tms/modules/device/dto/network/Ipv6InfoResponse.java deleted file mode 100644 index c88cc32..0000000 --- a/src/main/java/com/cisd/tms/modules/device/dto/network/Ipv6InfoResponse.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.cisd.tms.modules.device.dto.network; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -import java.util.List; - -@Data -@Schema(description = "IPv6配置信息响应") -public class Ipv6InfoResponse { - @Schema(description = "IPv6地址及掩码列表") - private List ipv6List; - @Schema(description = "默认网关地址", example = "2001:db8::1") - private String gateway; - @Schema(description = "IP获取方式(如 static、dhcp、auto)", example = "static") - private String method; -} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/network/NetworkConfigureRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/network/NetworkConfigureRequest.java new file mode 100644 index 0000000..914f649 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/network/NetworkConfigureRequest.java @@ -0,0 +1,62 @@ +package com.cisd.tms.modules.device.dto.network; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +@Schema(description = "网卡配置请求参数") +public class NetworkConfigureRequest { + + @Schema( + description = "网卡名称,例如 eth0、ens33、bond0", + example = "eth0", + requiredMode = Schema.RequiredMode.REQUIRED + ) + @NotBlank(message = "网卡名称不能为空") + private String deviceName; + + @Schema( + description = "地址类型:IPv4 或 IPv6", + example = "IPv4", + allowableValues = {"IPv4", "IPv6"}, + requiredMode = Schema.RequiredMode.REQUIRED + ) + @NotBlank(message = "地址类型不能为空") + private String addressType; + + @Schema( + description = "网络地址,不包含前缀长度", + example = "192.168.10.100", + requiredMode = Schema.RequiredMode.REQUIRED + ) + @NotBlank(message = "网络地址不能为空") + private String ipAddress; + + @Schema( + description = "前缀长度,IPv4范围0-32,IPv6范围0-128", + example = "24", + requiredMode = Schema.RequiredMode.REQUIRED + ) + @NotNull(message = "前缀长度不能为空") + private Integer prefixLength; + + /** + * 网关地址 + */ + @Schema( + description = "网关地址,可为空", + example = "192.168.10.1" + ) + private String gateway; + + @Schema( + description = "网卡状态:Active=激活,Inactive=未激活", + example = "Active", + allowableValues = {"Active", "Inactive"}, + requiredMode = Schema.RequiredMode.REQUIRED + ) + @NotBlank(message = "网卡状态不能为空") + private String status; +} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/device/dto/network/NetworkDeleteRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/network/NetworkDeleteRequest.java new file mode 100644 index 0000000..03d80be --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/network/NetworkDeleteRequest.java @@ -0,0 +1,44 @@ +package com.cisd.tms.modules.device.dto.network; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +@Schema(description = "删除网卡IP配置请求参数") +public class NetworkDeleteRequest { + + @Schema( + description = "网卡名称,例如 eth0、ens33、bond0", + example = "eth0", + requiredMode = Schema.RequiredMode.REQUIRED + ) + @NotBlank(message = "网卡名称不能为空") + private String deviceName; + + @Schema( + description = "地址类型:IPv4 或 IPv6", + example = "IPv4", + allowableValues = {"IPv4", "IPv6"}, + requiredMode = Schema.RequiredMode.REQUIRED + ) + @NotBlank(message = "地址类型不能为空") + private String addressType; + + @Schema( + description = "需要删除的网络地址,不包含前缀长度", + example = "192.168.10.100", + requiredMode = Schema.RequiredMode.REQUIRED + ) + @NotBlank(message = "网络地址不能为空") + private String ipAddress; + + @Schema( + description = "前缀长度,IPv4范围0-32,IPv6范围0-128", + example = "24", + requiredMode = Schema.RequiredMode.REQUIRED + ) + @NotNull(message = "前缀长度不能为空") + private Integer prefixLength; +} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/device/dto/network/NetworkInfoResponse.java b/src/main/java/com/cisd/tms/modules/device/dto/network/NetworkInfoResponse.java deleted file mode 100644 index 80c9c05..0000000 --- a/src/main/java/com/cisd/tms/modules/device/dto/network/NetworkInfoResponse.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.cisd.tms.modules.device.dto.network; - - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -import java.util.List; - -@Data -@Schema(description = "网络接口信息响应参数") -public class NetworkInfoResponse { - - @Schema(description = "设备名称", example = "eth0") - private String deviceName; - @Schema(description = "接口类型", example = "ethernet") - private String type; - @Schema(description = "连接状态", example = "connected") - private String state; - @Schema(description = "NetworkManager中的连接名称(通常与deviceName相同)", example = "eth0") - private String connectionName; - @Schema(description = "IPv4 地址", example = "192.168.1.100") - private String ipv4; - @Schema(description = "IPv6 地址", example = "2001:db8::1") - private List ipv6; - @Schema(description = "MAC 地址", example = "00:11:22:33:44:55") - private String mac; - @Schema(description = "默认网关地址", example = "192.168.1.1") - private String gateway; - -} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/network/NetworkTableQueryRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/network/NetworkTableQueryRequest.java new file mode 100644 index 0000000..7d2c97b --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/network/NetworkTableQueryRequest.java @@ -0,0 +1,37 @@ +package com.cisd.tms.modules.device.dto.network; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "网卡配置分页查询请求参数") +public class NetworkTableQueryRequest { + + /** + * 网卡名条件,可选 + */ + @Schema(description = "网卡名称,支持按网卡名筛选", example = "eth0") + private String deviceName; + + /** + * 状态条件,可选 + */ + @Schema( + description = "网卡状态:Active=激活,Inactive=未激活", + example = "Active", + allowableValues = {"Active", "Inactive"} + ) + private String status; + + /** + * 当前页码,默认 1 + */ + @Schema(description = "当前页码,从1开始", example = "1") + private Integer pageNum = 1; + + /** + * 每页条数,默认 10 + */ + @Schema(description = "每页条数", example = "10") + private Integer pageSize = 10; +} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/device/dto/network/NetworkTableResponse.java b/src/main/java/com/cisd/tms/modules/device/dto/network/NetworkTableResponse.java new file mode 100644 index 0000000..f0eea3b --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/network/NetworkTableResponse.java @@ -0,0 +1,53 @@ +package com.cisd.tms.modules.device.dto.network; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "网卡配置分页查询响应数据") +public class NetworkTableResponse { + + /** + * 网卡名,如 eth0、eth1 + */ + @Schema(description = "网卡名称", example = "eth0") + private String deviceName; + + /** + * 地址类型,如 IPV4、IPV6、- + */ + @Schema( + description = "地址类型:IPV4、IPV6 或 -", + example = "IPV4", + allowableValues = {"IPV4", "IPV6", "-"} + ) + private String addressType; + + /** + * 地址,如 172.16.18.1 + */ + @Schema(description = "IP地址", example = "172.16.18.1") + private String address; + + /** + * 前缀长度,如 24、64 + */ + @Schema(description = "前缀长度", example = "24") + private String prefixLength; + + /** + * 网关,如 172.16.18.254 + */ + @Schema(description = "网关地址", example = "172.16.18.254") + private String gateway; + + /** + * 状态,如 Active、Inactive + */ + @Schema( + description = "网卡状态:Active=激活,Inactive=未激活", + example = "Active", + allowableValues = {"Active", "Inactive"} + ) + private String status; +} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/device/dto/network/RouteCreateRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/network/RouteCreateRequest.java new file mode 100644 index 0000000..dba92f7 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/network/RouteCreateRequest.java @@ -0,0 +1,81 @@ +package com.cisd.tms.modules.device.dto.network; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +@Schema(description = "新增路由请求参数") +public class RouteCreateRequest { + + /** + * HOST / NETWORK + */ + @Schema( + description = "目标类型:HOST=主机地址,NETWORK=网段地址", + example = "NETWORK", + allowableValues = {"HOST", "NETWORK"}, + requiredMode = Schema.RequiredMode.REQUIRED + ) + @NotBlank(message = "目标类型不能为空") + private String targetType; + + /** + * 目标地址,例如: + * HOST: 172.2.2.2 + * NETWORK: 192.168.10.0 + */ + @Schema( + description = "目标地址。HOST时填写主机IP,NETWORK时填写网段地址", + example = "192.168.10.0", + requiredMode = Schema.RequiredMode.REQUIRED + ) + @NotBlank(message = "目标地址不能为空") + private String targetAddress; + + /** + * 前缀长度: + * HOST 一般为 32 + * NETWORK 例如 24、64 + */ + @Schema( + description = "前缀长度。IPv4主机路由一般为32,IPv6主机路由一般为128,网段路由例如IPv4为24、IPv6为64", + example = "24", + requiredMode = Schema.RequiredMode.REQUIRED + ) + @NotNull(message = "前缀长度不能为空") + private Integer prefixLength; + + /** + * 网口,例如 eth0 + */ + @Schema( + description = "出接口名称,例如 eth0、ens33、bond0", + example = "eth0", + requiredMode = Schema.RequiredMode.REQUIRED + ) + @NotBlank(message = "网口不能为空") + private String interfaceName; + + /** + * 下一跳,可为空 + */ + @Schema( + description = "下一跳网关地址,可为空。为空时表示直连路由", + example = "192.168.10.1" + ) + private String nextHop; + + /** + * 地址类型:IPv4 / IPv6 + */ + @Schema( + description = "地址类型:IPv4 或 IPv6", + example = "IPv4", + allowableValues = {"IPv4", "IPv6"}, + requiredMode = Schema.RequiredMode.REQUIRED + ) + @NotBlank(message = "地址类型不能为空") + private String addressType; +} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/device/dto/network/RouteDeleteRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/network/RouteDeleteRequest.java new file mode 100644 index 0000000..3ca9cb2 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/network/RouteDeleteRequest.java @@ -0,0 +1,62 @@ +package com.cisd.tms.modules.device.dto.network; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +@Schema(description = "删除路由请求参数") +public class RouteDeleteRequest { + + @Schema( + description = "目标类型:HOST=主机地址,NETWORK=网段地址", + example = "NETWORK", + allowableValues = {"HOST", "NETWORK"}, + requiredMode = Schema.RequiredMode.REQUIRED + ) + @NotBlank(message = "目标类型不能为空") + private String targetType; + + @Schema( + description = "目标地址。HOST时填写主机IP,NETWORK时填写网段地址", + example = "192.168.10.0", + requiredMode = Schema.RequiredMode.REQUIRED + ) + @NotBlank(message = "目标地址不能为空") + private String targetAddress; + + @Schema( + description = "前缀长度。IPv4主机路由一般为32,IPv6主机路由一般为128,网段路由例如IPv4为24、IPv6为64", + example = "24", + requiredMode = Schema.RequiredMode.REQUIRED + ) + @NotNull(message = "前缀长度不能为空") + private Integer prefixLength; + + @Schema( + description = "出接口名称,例如 eth0、ens33、bond0", + example = "eth0", + requiredMode = Schema.RequiredMode.REQUIRED + ) + @NotBlank(message = "网口不能为空") + private String interfaceName; + + @Schema( + description = "地址类型:IPv4 或 IPv6", + example = "IPv4", + allowableValues = {"IPv4", "IPv6"}, + requiredMode = Schema.RequiredMode.REQUIRED + ) + @NotBlank(message = "地址类型不能为空") + private String addressType; + + /** + * 下一跳,可为空 + */ + @Schema( + description = "下一跳网关地址,可为空。为空时表示直连路由", + example = "192.168.10.1" + ) + private String nextHop; +} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/device/dto/network/RouteInfoResponse.java b/src/main/java/com/cisd/tms/modules/device/dto/network/RouteInfoResponse.java deleted file mode 100644 index 28ca406..0000000 --- a/src/main/java/com/cisd/tms/modules/device/dto/network/RouteInfoResponse.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.cisd.tms.modules.device.dto.network; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -@Data -@Schema(description = "路由信息响应") -public class RouteInfoResponse { - - @Schema(description = "目标网络或主机(CIDR格式或主机地址)", example = "0.0.0.0/0") - private String destination; - - @Schema(description = "下一跳地址(网关)", example = "192.168.1.1") - private String nextHop; - - @Schema(description = "出口网络接口名称", example = "eth0") - private String interfaceName; -} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/device/dto/network/RouteTableRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/network/RouteTableRequest.java new file mode 100644 index 0000000..be69b77 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/network/RouteTableRequest.java @@ -0,0 +1,38 @@ +package com.cisd.tms.modules.device.dto.network; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "路由分页查询请求参数") +public class RouteTableRequest { + + /** + * 目标类型: + * HOST:主机地址 + * NETWORK:网段地址 + */ + @Schema( + description = "目标类型:HOST=主机地址,NETWORK=网段地址", + example = "NETWORK", + allowableValues = {"HOST", "NETWORK"} + ) + private String targetType; + + /** + * 网卡状态: + * Active / Inactive + */ + @Schema( + description = "网卡状态:Active=激活,Inactive=未激活", + example = "Active", + allowableValues = {"Active", "Inactive"} + ) + private String status; + + @Schema(description = "当前页码,从1开始", example = "1") + private Integer pageNum = 1; + + @Schema(description = "每页条数", example = "10") + private Integer pageSize = 10; +} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/device/dto/network/RouteTableResponse.java b/src/main/java/com/cisd/tms/modules/device/dto/network/RouteTableResponse.java new file mode 100644 index 0000000..6e38915 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/network/RouteTableResponse.java @@ -0,0 +1,61 @@ +package com.cisd.tms.modules.device.dto.network; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "路由分页查询响应数据") +public class RouteTableResponse { + + /** + * 目标类型:主机地址 / 网段地址 + */ + @Schema( + description = "目标类型:HOST=主机地址,NETWORK=网段地址", + example = "NETWORK", + allowableValues = {"HOST", "NETWORK"} + ) + private String targetType; + + /** + * 目标地址 + */ + @Schema(description = "目标地址", example = "192.168.10.0") + private String targetAddress; + + /** + * 前缀长度 + */ + @Schema(description = "前缀长度", example = "24") + private String prefixLength; + + /** + * 网口 + */ + @Schema(description = "出接口名称", example = "eth0") + private String interfaceName; + + /** + * 下一跳,可为空 + */ + @Schema(description = "下一跳网关地址,可为空。为空时表示直连路由", example = "192.168.10.1") + private String nextHop; + + /** + * 状态:Active / Inactive + */ + @Schema( + description = "网口状态:Active=激活,Inactive=未激活", + example = "Active", + allowableValues = {"Active", "Inactive"} + ) + private String status; + + @Schema( + description = "网口状态:Active=激活,Inactive=未激活", + example = "Active", + allowableValues = {"Active", "Inactive"} + ) + private String addressType; + +} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/device/dto/network/RouteUpdateRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/network/RouteUpdateRequest.java new file mode 100644 index 0000000..c5b4f1b --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/network/RouteUpdateRequest.java @@ -0,0 +1,27 @@ +package com.cisd.tms.modules.device.dto.network; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +@Schema(description = "修改路由请求参数") +public class RouteUpdateRequest { + + @Schema( + description = "旧路由信息,用于定位需要被修改的原始路由", + requiredMode = Schema.RequiredMode.REQUIRED + ) + @Valid + @NotNull(message = "旧路由信息不能为空") + private RouteCreateRequest oldRoute; + + @Schema( + description = "新路由信息,用于替换旧路由", + requiredMode = Schema.RequiredMode.REQUIRED + ) + @Valid + @NotNull(message = "新路由信息不能为空") + private RouteCreateRequest newRoute; +} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/device/dto/network/SetDefaultRouteRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/network/SetDefaultRouteRequest.java deleted file mode 100644 index 634027b..0000000 --- a/src/main/java/com/cisd/tms/modules/device/dto/network/SetDefaultRouteRequest.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.cisd.tms.modules.device.dto.network; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -@Data -@Schema(description = "设置默认路由请求参数") -public class SetDefaultRouteRequest { - - @Schema(description = "网络设备名称(如 eth0、bond0)", example = "eth0") - private String deviceName; - - @Schema(description = "默认网关IP地址", example = "192.168.1.1") - private String gatewayIp; -} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/device/entity/PageResult.java b/src/main/java/com/cisd/tms/modules/device/entity/PageResult.java new file mode 100644 index 0000000..596906d --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/entity/PageResult.java @@ -0,0 +1,18 @@ +package com.cisd.tms.modules.device.entity; + +import lombok.Data; + +import java.util.List; + +@Data +public class PageResult { + + private long total; + + private List records; + + public PageResult(long total, List records) { + this.total = total; + this.records = records; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/service/NetworkConfigService.java b/src/main/java/com/cisd/tms/modules/device/service/NetworkConfigService.java index 509da7f..86efd55 100644 --- a/src/main/java/com/cisd/tms/modules/device/service/NetworkConfigService.java +++ b/src/main/java/com/cisd/tms/modules/device/service/NetworkConfigService.java @@ -3,6 +3,7 @@ package com.cisd.tms.modules.device.service; import com.cisd.tms.common.enums.ErrorCode; import com.cisd.tms.common.exception.BizException; import com.cisd.tms.modules.device.dto.network.*; +import com.cisd.tms.modules.device.entity.PageResult; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -26,690 +27,2637 @@ public class NetworkConfigService { "^((25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)\\.){3}(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)/(0|[1-9]|[1-2]\\d|3[0-2])$" ); - public List getNetworkInfo(){ - List networkInfo = new ArrayList<>(); + private static class OldConnectionSnapshot { + private final String uuid; + private final String autoconnect; + private final boolean active; - List lines = executeCommand("nmcli", "-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device"); - for (String line : lines){ - String[] parts = line.split(":", -1); - String device = parts[0]; + public OldConnectionSnapshot(String uuid, String autoconnect, boolean active) { + this.uuid = uuid; + this.autoconnect = autoconnect; + this.active = active; + } + } + + private static class BondConfigSnapshot { + private final String bondOptions; + private final String ipv4Method; + private final String ipv4Addresses; + private final String ipv4Gateway; + private final String ipv6Method; + private final String ipv6Addresses; + private final String ipv6Gateway; + private final List slaveList; + private final boolean active; + + public BondConfigSnapshot( + String bondOptions, + String ipv4Method, + String ipv4Addresses, + String ipv4Gateway, + String ipv6Method, + String ipv6Addresses, + String ipv6Gateway, + List slaveList, + boolean active + ) { + this.bondOptions = bondOptions; + this.ipv4Method = ipv4Method; + this.ipv4Addresses = ipv4Addresses; + this.ipv4Gateway = ipv4Gateway; + this.ipv6Method = ipv6Method; + this.ipv6Addresses = ipv6Addresses; + this.ipv6Gateway = ipv6Gateway; + this.slaveList = slaveList; + this.active = active; + } + } + + + private static class RouteConfigSnapshot { + private final String connectionName; + private final String interfaceName; + private final boolean ipv6; + private final String oldRoutes; + + public RouteConfigSnapshot( + String connectionName, + String interfaceName, + boolean ipv6, + String oldRoutes + ) { + this.connectionName = connectionName; + this.interfaceName = interfaceName; + this.ipv6 = ipv6; + this.oldRoutes = oldRoutes; + } + } + + + private static class BondSlaveSnapshot { + private final String uuid; + private final String name; + private final String interfaceName; + + public BondSlaveSnapshot(String uuid, String name, String interfaceName) { + this.uuid = uuid; + this.name = name; + this.interfaceName = interfaceName; + } + } + + + public PageResult getNetworkTablePage(NetworkTableQueryRequest req) { + List resultList = new ArrayList<>(); + String targetDeviceName = trim(req.getDeviceName()); + String targetStatus = trim(req.getStatus()); + + int pageNum = req.getPageNum(); + int pageSize = req.getPageSize(); + + List lines = executeCommand( + "nmcli", + "-t", + "-f", + "DEVICE,TYPE,STATE,CONNECTION", + "device" + ); + + Set bondSlaveDeviceSet = getBondSlaveDeviceSet(); + + for (String line : lines) { + if (line == null || line.trim().isEmpty()) { + continue; + } + + String[] parts = line.split("(? infoLines = executeCommand("nmcli", "-t", "-f", "IP4.ADDRESS,IP4.GATEWAY,IP6.ADDRESS,GENERAL.HWADDR", "device", "show", device); - Listipv6List = new ArrayList<>(); - for(String info : infoLines){ - int idx = info.indexOf(':'); //IP4.ADDRESS[1]:192.168.7.65/20 - String key = info.substring(0, idx); - String value = info.substring(idx + 1); - - if (key.startsWith("IP4.ADDRESS")){ - resp.setIpv4(value); - } else if (key.startsWith("IP6.ADDRESS")) { - ipv6List.add(value); - } else if (key.startsWith("IP4.GATEWAY")) { - resp.setGateway(value); - } else if ("GENERAL.HWADDR".equals(key)) { - resp.setMac(value); - } + if (!targetDeviceName.isEmpty() && !device.equals(targetDeviceName)) { + continue; } - resp.setIpv6(ipv6List); - networkInfo.add(resp); - } - return networkInfo; - } + String mappedStatus = "connected".equalsIgnoreCase(state) ? "Active" : "Inactive"; - public Ipv4InfoResponse getIpv4Info(String deviceName){ - String connectionName = getConnectionNameByDeviceName(deviceName.trim()); - if (connectionName == null){ - connectionName = deviceName; - } + if (bondSlaveDeviceSet.contains(device)) { + continue; + } - List lines; - try{ - lines = executeCommand("nmcli", "-g", "IPv4.ADDRESSES,IPv4.GATEWAY,IPv4.METHOD", "con", "show", connectionName); - } catch (RuntimeException e){ - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "读取网络配置信息失败,请检查网卡 [" + deviceName + "] 是否存在或已配置"); - } + if (!targetStatus.isEmpty() && !mappedStatus.equals(targetStatus)) { + continue; + } - Ipv4InfoResponse resp = new Ipv4InfoResponse(); + List ipv4List = new ArrayList<>(); + List ipv6List = new ArrayList<>(); + String ipv4Gateway = ""; + String ipv6Gateway = ""; - // 命令输出格式 - // IP/前缀 - // 网关 - // 模式 - - if (lines.size() > 0 && !lines.get(0).trim().isEmpty()) { - String ipAndMaskLength = lines.get(0).trim(); - if (ipAndMaskLength.contains("/")) { - String[] parts = ipAndMaskLength.split("/"); - resp.setIpv4(parts[0]); - resp.setMaskLength(parts[1]); + String connectionName; + if (connection != null && !connection.trim().isEmpty() && !"--".equals(connection.trim())) { + connectionName = connection.trim(); } else { - resp.setIpv4(ipAndMaskLength); + connectionName = getConNameByDeviceNameIncludeInactive(device); } - } - - if (lines.size() > 1 && !lines.get(1).trim().isEmpty()) { - resp.setGateway(lines.get(1).trim()); - } - - - if (lines.size() > 2 && !lines.get(2).trim().isEmpty()) { - String method = lines.get(2).trim(); - if ("manual".equalsIgnoreCase(method)) { - resp.setMethod("static"); - } else if ("auto".equalsIgnoreCase(method)) { - resp.setMethod("dhcp"); - } else { - resp.setMethod(method); - } - } - - return resp; - } - - - public void setIpv4Config(Ipv4ConfigRequest req){ - String ipv4 = trim(req.getIpv4()); - String deviceName = trim(req.getDeviceName()); - String mask = trim(req.getMaskLength()); - if (deviceName.isEmpty()) { - throw new IllegalArgumentException("网卡名称不能为空"); - } - if (ipv4.isEmpty()) { - throw new IllegalArgumentException("ip地址不能为空"); - } - if (mask.isEmpty()) { - throw new IllegalArgumentException("子网掩码长度不能为空"); - } - if (!IPV4_PATTERN.matcher(ipv4).matches()) { - throw new IllegalArgumentException("无效的 IP 地址格式: " + req.getIpv4()); - } - boolean hasGateway = req.getGateway() != null && !req.getGateway().trim().isEmpty(); - if (hasGateway && !IPV4_PATTERN.matcher(req.getGateway().trim()).matches()) { - throw new IllegalArgumentException("无效的网关地址格式: " + req.getGateway()); - } - - //判断连接是否存在,如果不存在则新建新连接 - boolean isNewConnection = false; - String connectionName = getConnectionNameByDeviceName(deviceName); - if (connectionName == null){ - connectionName = deviceName; - isNewConnection = true; // 标记为需要执行 add 创建配置 - } - - - // 转化掩码格式并判断掩码格式是否正确 - int maskLength = 0; - try { - String netmask = trim(req.getMaskLength()); - if (netmask.isEmpty()) { - throw new IllegalArgumentException("掩码不能为空"); - } - // 传的是数字/24 - if (!netmask.contains(".")) { - int prefix = Integer.parseInt(netmask); - if (prefix < 0 || prefix > 32) throw new IllegalArgumentException("非法的掩码前缀: " + prefix); - maskLength = prefix; - } - //传的是255.255.255.0 -// int maskInt = ipToInt(netmask); -// for (int i = 31; i >= 0; i --) { -// if ((maskInt & (1 << i)) != 0) { -// maskLength ++; -// } else { -// break; -// } -// } - } catch (Exception e) { - throw new IllegalArgumentException("无效的子网掩码格式: " + req.getMaskLength()); - } - - //判断ip和网关是否在同一个子网里面 - int ip1Int = ipToInt(req.getIpv4().trim()); - if (hasGateway){ - int ip2Int = ipToInt(req.getGateway().trim()); - int maskInt = (maskLength == 0) ? 0 : (0xFFFFFFFF << (32 - maskLength)); - if (!((ip1Int & maskInt) == (ip2Int & maskInt))){ - throw new IllegalArgumentException( - String.format("网关 (%s) 不在 IP (%s/%d) 所在的子网内", - req.getGateway(), req.getIpv4(), maskLength) + if ("connected".equalsIgnoreCase(state)) { + List infoLines = executeCommand( + "nmcli", + "-t", + "-f", + "IP4.ADDRESS,IP6.ADDRESS", + "device", + "show", + device ); - } - } + for (String info : infoLines) { + if (info == null || info.trim().isEmpty()) { + continue; + } - String cidr = req.getIpv4() + "/" + maskLength; - try { - if (isNewConnection) { - List cmdArgs = new ArrayList<>(); - cmdArgs.add("nmcli"); cmdArgs.add("con"); cmdArgs.add("add"); - cmdArgs.add("type"); cmdArgs.add("ethernet"); // 默认创建以太网类型 - cmdArgs.add("con-name"); cmdArgs.add(connectionName); - cmdArgs.add("ifname"); cmdArgs.add(deviceName); - cmdArgs.add("ipv4.method"); cmdArgs.add("manual"); - cmdArgs.add("ipv4.addresses"); cmdArgs.add(cidr); - if (hasGateway) { - cmdArgs.add("ipv4.gateway");cmdArgs.add(req.getGateway()); - cmdArgs.add("ipv4.never-default");cmdArgs.add("yes"); - } - executeCommand(cmdArgs.toArray(new String[0])); - } else { - if (hasGateway) { - executeCommand("nmcli", "con", "mod", connectionName, - "ipv4.method", "manual", - "ipv4.addresses", cidr, - "ipv4.gateway", req.getGateway(), - "ipv4.never-default", "yes"); - } else { - executeCommand("nmcli", "con", "mod", connectionName, - "ipv4.method", "manual", - "ipv4.addresses", cidr, - "ipv4.gateway", ""); + int idx = info.indexOf(':'); + if (idx == -1) { + continue; + } + + String key = info.substring(0, idx); + String value = info.substring(idx + 1).replace("\\:", ":"); + + if (key.startsWith("IP4.ADDRESS")) { + ipv4List.add(value); + } else if (key.startsWith("IP6.ADDRESS")) { + ipv6List.add(value); + } } } - executeCommand("nmcli", "con", "up", connectionName); - } catch (RuntimeException e) { + if (connectionName != null && !connectionName.trim().isEmpty()) { + try { + List conLines = executeCommand( + "nmcli", + "-g", + "ipv4.addresses,ipv4.gateway,ipv6.addresses,ipv6.gateway", + "con", + "show", + connectionName + ); - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "修改网络配置失败: " + e.getMessage()); - } - } + if (!"connected".equalsIgnoreCase(state)) { + if (conLines.size() > 0 && !conLines.get(0).trim().isEmpty()) { + ipv4List.addAll(splitAddressList(conLines.get(0))); + } + if (conLines.size() > 2 && !conLines.get(2).trim().isEmpty()) { + ipv6List.addAll(splitAddressList(conLines.get(2))); + } + } - public Ipv6InfoResponse getIpv6Info(String deviceName){ - deviceName = trim(deviceName); - String connectionName = getConnectionNameByDeviceName(deviceName); - if (connectionName == null){ - connectionName = deviceName; - } + if (conLines.size() > 1 && !conLines.get(1).trim().isEmpty()) { + ipv4Gateway = conLines.get(1).trim(); + } - List lines; - try{ - lines = executeCommand("nmcli", "-g", "IPv6.ADDRESSES,IPv6.GATEWAY,IPv6.METHOD", "con", "show", connectionName); - } catch (RuntimeException e){ - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "读取网络配置信息失败,请检查网卡 [" + deviceName + "] 是否存在或已配置"); - } + if (conLines.size() > 3 && !conLines.get(3).trim().isEmpty()) { + ipv6Gateway = conLines.get(3).trim().replace("\\:", ":"); + } + } catch (Exception e) { + log.warn("读取网卡 [{}] 的连接配置 [{}] 信息失败: {}", device, connectionName, e.getMessage()); + } + } - Ipv6InfoResponse resp = new Ipv6InfoResponse(); + boolean hasIpConfig = false; - // 命令输出格式 - // IP/前缀 - // 网关 - // 模式 - List addressList = new ArrayList<>(); - if (lines.size() > 0 && !lines.get(0).trim().isEmpty()) { - // 按逗号分割出所有的 IPv6 地址 - String rawAddresses = lines.get(0).trim(); - String[] addressArray = rawAddresses.split(","); - - for (String ipAndMaskLength : addressArray) { - ipAndMaskLength = ipAndMaskLength.trim(); - if (ipAndMaskLength.isEmpty()) { + for (String ipPrefix : ipv4List) { + if (ipPrefix == null || ipPrefix.trim().isEmpty()) { continue; } - Ipv6AddressItem item = new Ipv6AddressItem(); - if (ipAndMaskLength.contains("/")) { - String[] parts = ipAndMaskLength.split("/"); - item.setIpv6(parts[0].replace("\\:", ":")); - item.setMaskLength(parts[1]); - } else { - item.setIpv6(ipAndMaskLength.replace("\\:", ":")); + resultList.add(createTableItem(device, "IPV4", ipPrefix, ipv4Gateway, mappedStatus)); + hasIpConfig = true; + } + + for (String ipPrefix : ipv6List) { + if (ipPrefix == null || ipPrefix.trim().isEmpty()) { + continue; } - // 将解析好的单个 IP 对象放入集合 - addressList.add(item); + + resultList.add(createTableItem(device, "IPV6", ipPrefix, ipv6Gateway, mappedStatus)); + hasIpConfig = true; } - } - resp.setIpv6List(addressList); - - if (lines.size()> 1 && !lines.get(1).trim().isEmpty()) { - resp.setGateway(lines.get(1).trim().replace("\\:", ":")); - } - - if (lines.size() > 2 && !lines.get(2).trim().isEmpty()) { - String method = lines.get(2).trim(); - if ("manual".equalsIgnoreCase(method)) { - resp.setMethod("static"); - } else if ("auto".equalsIgnoreCase(method)) { - resp.setMethod("dhcp"); - } else { - resp.setMethod(method); + if (!hasIpConfig) { + NetworkTableResponse emptyItem = new NetworkTableResponse(); + emptyItem.setDeviceName(device); +// emptyItem.setAddressType("-"); +// emptyItem.setAddress("-"); +// emptyItem.setPrefixLength("-"); +// emptyItem.setGateway("-"); + emptyItem.setStatus(mappedStatus); + resultList.add(emptyItem); } } - return resp; + int total = resultList.size(); + int fromIndex = (pageNum - 1) * pageSize; + int toIndex = Math.min(fromIndex + pageSize, total); + + List pageList; + if (fromIndex >= total) { + pageList = new ArrayList<>(); + } else { + pageList = resultList.subList(fromIndex, toIndex); + } + + return new PageResult<>(total, pageList); } - public void setIpv6Config(Ipv6ConfigRequest req){ + + public void configureNetworkCard(NetworkConfigureRequest req) { String deviceName = trim(req.getDeviceName()); - if (req.getDeviceName() == null || req.getDeviceName().trim().isEmpty()) { - throw new IllegalArgumentException("网卡名称不能为空"); - } + String addressType = trim(req.getAddressType()); + String ipAddress = trim(req.getIpAddress()); + String gateway = trim(req.getGateway()); + Integer prefixLength = req.getPrefixLength(); + String status = trim(req.getStatus()); + boolean isIpv6 = verifyAddressType(addressType); + boolean hasGateway = !gateway.isEmpty(); - if (!isValidIpv6(req.getIpv6())) { - throw new IllegalArgumentException("无效的 IPv6 地址格式: " + req.getIpv6()); - } + validateIpAndPrefix(ipAddress, gateway, prefixLength, isIpv6); - //判断网关格式 - boolean hasGateway = req.getGateway() != null && !req.getGateway().trim().isEmpty(); - if (hasGateway && !isValidIpv6(req.getGateway())) { - throw new IllegalArgumentException("无效的 IPv6 网关格式: " + req.getGateway()); - } - - if (req.getMaskLength() == null || req.getMaskLength().isEmpty()) { - throw new IllegalArgumentException("掩码不能为空"); - } - - int maskLength = Integer.parseInt(req.getMaskLength()); - if (maskLength < 0 || maskLength > 128) { - throw new IllegalArgumentException("无效的 IPv6 前缀长度,必须在 0 到 128 之间"); - } - - // 判断连接是否存在,如果不存在则需要新建 boolean isNewConnection = false; - String connectionName = getConnectionNameByDeviceName(deviceName); - if (connectionName == null){ + String connectionName = getConNameByDeviceNameIncludeInactive(deviceName); + if (connectionName == null) { connectionName = deviceName; isNewConnection = true; } + String ipWithPrefix = ipAddress + "/" + prefixLength; + String protocol = isIpv6 ? "ipv6" : "ipv4"; - String ipWithPrefix = req.getIpv6().trim() + "/" + req.getMaskLength().trim(); try { + List cmdArgs; + if (isNewConnection) { - List cmdArgs = new ArrayList<>(); - cmdArgs.add("nmcli"); cmdArgs.add("con"); cmdArgs.add("add"); - cmdArgs.add("type"); cmdArgs.add("ethernet"); - cmdArgs.add("con-name"); cmdArgs.add(connectionName); - cmdArgs.add("ifname"); cmdArgs.add(deviceName); - cmdArgs.add("ipv6.method"); cmdArgs.add("manual"); - cmdArgs.add("ipv6.addresses"); cmdArgs.add(ipWithPrefix); - if (hasGateway) { - cmdArgs.add("ipv6.gateway"); cmdArgs.add(req.getGateway().trim()); - cmdArgs.add("ipv6.never-default");cmdArgs.add("yes"); - } - executeCommand(cmdArgs.toArray(new String[0])); + cmdArgs = new ArrayList<>(Arrays.asList( + "nmcli", + "con", + "add", + "type", + "ethernet", + "con-name", + connectionName, + "ifname", + deviceName, + protocol + ".method", + "manual", + protocol + ".addresses", + ipWithPrefix + )); } else { - if (hasGateway) { - executeCommand("nmcli", "con", "mod", connectionName, - "ipv6.method", "manual", - "ipv6.addresses", ipWithPrefix, - "ipv6.gateway", req.getGateway().trim(), - "ipv6.never-default","yes"); + cmdArgs = new ArrayList<>(Arrays.asList( + "nmcli", + "con", + "mod", + connectionName, + protocol + ".method", + "manual", + protocol + ".addresses", + ipWithPrefix + )); + } + + if (hasGateway) { + cmdArgs.add(protocol + ".gateway"); + cmdArgs.add(gateway); + + cmdArgs.add(protocol + ".never-default"); + cmdArgs.add("no"); + + // todo 后续改 + cmdArgs.add(protocol + ".route-metric"); + cmdArgs.add("200"); + } else { + cmdArgs.add(protocol + ".gateway"); + cmdArgs.add(""); + + cmdArgs.add(protocol + ".never-default"); + cmdArgs.add("yes"); + } + + executeCommand(cmdArgs.toArray(new String[0])); + + if ("Active".equalsIgnoreCase(status)) { + try { + executeCommand("nmcli", "dev", "reapply", deviceName); + } catch (RuntimeException e) { + log.warn("reapply 网卡 [{}] 失败,尝试重新激活连接 [{}]: {}", deviceName, connectionName, e.getMessage()); + executeCommand("nmcli", "con", "up", connectionName); + } + } else if ("Inactive".equalsIgnoreCase(status)) { + try { + executeCommand("nmcli", "con", "down", connectionName); + } catch (Exception e) { + log.warn("设置网卡为未激活时,down网卡操作抛出异常: {}", e.getMessage()); + } + } else { + throw new IllegalArgumentException("状态只支持 Active 或 Inactive"); + } + + } catch (RuntimeException e) { + throw new BizException( + ErrorCode.BIZ_ERROR.getCode(), + "配置网卡保存失败: " + e.getMessage() + ); + } + } + + public void deleteNetworkCardRecord(NetworkDeleteRequest req) { + String deviceName = trim(req.getDeviceName()); + String addressType = trim(req.getAddressType()); + String ipAddress = trim(req.getIpAddress()); + Integer prefixLength = req.getPrefixLength(); + + boolean isIpv6 = verifyAddressType(addressType); + + String protocol = isIpv6 ? "ipv6" : "ipv4"; + String addressesKey = isIpv6 ? "IPv6.ADDRESSES" : "IPv4.ADDRESSES"; + String methodKey = protocol + ".method"; + String nmcliAddressKey = protocol + ".addresses"; + + validateIpAndPrefix(ipAddress, null, prefixLength, isIpv6); + + String connectionName = getConNameByDeviceNameIncludeInactive(deviceName); + if (connectionName == null) { + throw new BizException( + ErrorCode.BIZ_ERROR.getCode(), + "未找到网卡 [" + deviceName + "] 对应的网络连接配置" + ); + } + + boolean wasActive = isConnectionActive(connectionName); + + String targetAddress = ipAddress + "/" + prefixLength; + + try { + List lines = executeCommand( + "nmcli", + "-g", + addressesKey, + "con", + "show", + connectionName + ); + + if (lines.isEmpty() || lines.get(0).trim().isEmpty()) { + throw new BizException( + ErrorCode.BIZ_ERROR.getCode(), + "当前网卡没有可删除的地址配置" + ); + } + + String rawAddresses = lines.get(0).trim().replace("\\:", ":"); + List addressList = new ArrayList<>(Arrays.asList(rawAddresses.split(","))); + + boolean removed = false; + List remainAddressList = new ArrayList<>(); + + for (String item : addressList) { + if (item == null || item.trim().isEmpty()) { + continue; + } + + String current = item.trim().replace("\\:", ":"); + + if (current.equalsIgnoreCase(targetAddress)) { + removed = true; } else { - executeCommand("nmcli", "con", "mod", connectionName, - "ipv6.method", "manual", - "ipv6.addresses", ipWithPrefix, - "ipv6.gateway", ""); + remainAddressList.add(current); } } - executeCommand("nmcli", "con", "up", connectionName); + if (!removed) { + throw new BizException( + ErrorCode.BIZ_ERROR.getCode(), + "未找到要删除的网络地址: " + targetAddress + ); + } + if (remainAddressList.isEmpty()) { + clearProtocolConfig(connectionName, protocol); + } else { + String newAddresses = String.join(",", remainAddressList); + + executeCommand( + "nmcli", + "con", + "mod", + connectionName, + methodKey, + "manual", + nmcliAddressKey, + newAddresses + ); + } + + if (wasActive) { + applyConnection(connectionName, deviceName); + } + + } catch (BizException e) { + throw e; } catch (RuntimeException e) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "修改 IPv6 网络配置失败: " + e.getMessage()); + throw new BizException( + ErrorCode.BIZ_ERROR.getCode(), + "删除网卡记录失败: " + e.getMessage() + ); } } - public List getAllBondName(){ - List bondNames = new ArrayList<>(); - List lines = executeCommand("nmcli", "-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device"); + + private NetworkTableResponse createTableItem(String device, String type, String ipWithPrefix, String gateway, String status) { + NetworkTableResponse item = new NetworkTableResponse(); + item.setDeviceName(device); + item.setAddressType(type); + item.setGateway(gateway != null ? gateway : ""); + item.setStatus(status); + + + if (ipWithPrefix.contains("/")) { + String[] parts = ipWithPrefix.split("/"); + item.setAddress(parts[0]); + item.setPrefixLength(parts[1]); + } else { + item.setAddress(ipWithPrefix); + item.setPrefixLength(""); + } + return item; + } + + + public PageResult getBondPage(BondTableRequest req) { + List resultList = new ArrayList<>(); + + String targetBondName = trim(req.getBondName()); + String targetStatus = trim(req.getStatus()); + + int pageNum = req.getPageNum(); + int pageSize = req.getPageSize(); + + List lines = executeCommand("nmcli", "-t", "-f", "NAME,TYPE,DEVICE", "con", "show"); for (String line : lines) { + if (line == null || line.trim().isEmpty()) { + continue; + } + String[] parts = line.split("(?= 2 && "bond".equals(parts[1])) { - bondNames.add(parts[0]); + if (parts.length < 3) { + continue; + } + + String bondName = parts[0].replace("\\:", ":"); + String type = parts[1]; + String device = parts[2].replace("\\:", ":"); + + if (!"bond".equals(type)) { + continue; + } + + if (!targetBondName.isEmpty() && !bondName.equals(targetBondName)) { + continue; + } + + Map bondStatusMap = getBondDeviceStatusMap(); + String status = bondStatusMap.getOrDefault(bondName, "Inactive"); + + if (!targetStatus.isEmpty() && !status.equals(targetStatus)) { + continue; + } + + String bondMode = getBondMode(bondName); + + List conLines = executeCommand( + "nmcli", + "-g", + "IPv4.ADDRESSES,IPv4.GATEWAY,IPv6.ADDRESSES,IPv6.GATEWAY", + "con", + "show", + bondName + ); + + String ipv4Addresses = getLineValue(conLines, 0); + String ipv4Gateway = getLineValue(conLines, 1); + String ipv6Addresses = getLineValue(conLines, 2).replace("\\:", ":"); + String ipv6Gateway = getLineValue(conLines, 3).replace("\\:", ":"); + + boolean hasIp = false; + + for (String ipWithPrefix : splitAddressList(ipv4Addresses)) { + resultList.add(createBondTableItem(bondName, bondMode, "IPV4", ipWithPrefix, ipv4Gateway, status)); + hasIp = true; + } + + for (String ipWithPrefix : splitAddressList(ipv6Addresses)) { + resultList.add(createBondTableItem(bondName, bondMode, "IPV6", ipWithPrefix, ipv6Gateway, status)); + hasIp = true; + } + + if (!hasIp) { + BondTableResponse empty = new BondTableResponse(); + empty.setBondName(bondName); + empty.setBondMode(bondMode); + empty.setStatus(status); + resultList.add(empty); } } - return bondNames; + + int total = resultList.size(); + int fromIndex = (pageNum - 1) * pageSize; + int toIndex = Math.min(fromIndex + pageSize, total); + + List pageList; + if (fromIndex >= total) { + pageList = new ArrayList<>(); + } else { + pageList = resultList.subList(fromIndex, toIndex); + } + + return new PageResult<>(total, pageList); } - public void createBond(BondCreateRequest req){ + + private Map getBondDeviceStatusMap() { + Map result = new HashMap<>(); + + List lines = executeCommand( + "nmcli", + "-t", + "-f", + "DEVICE,TYPE,STATE", + "device", + "status" + ); + + for (String line : lines) { + if (line == null || line.trim().isEmpty()) { + continue; + } + + String[] parts = line.split("(? slaveList = normalizeSlaveList(req.getSlaveList(), true); + + boolean isIpv6 = verifyAddressType(addressType); + + if (mode != 0 && mode != 1 && mode != 2 && mode != 4) { + throw new IllegalArgumentException("不支持当前Bond类型"); } + validateIpAndPrefix(ipAddress, gateway, prefixLength, isIpv6); + + List lines = executeCommand("nmcli", "-g", "NAME", "con", "show"); + for (String line : lines) { + if (bondName.equals(line.trim())) { + throw new BizException( + ErrorCode.BIZ_ERROR.getCode(), + "Bond连接名称 [" + bondName + "] 已存在,请勿重复创建" + ); + } + } + + checkSlaveDevicesExist(slaveList); + + String bondOptions = String.format("mode=%s,miimon=100", mapBondMode(mode)); + String protocol = isIpv6 ? "ipv6" : "ipv4"; + String ipWithPrefix = ipAddress + "/" + prefixLength; + + List createdSlaveConnectionNames = new ArrayList<>(); + List oldConnectionSnapshots = new ArrayList<>(); try { - List lines = executeCommand("nmcli", "-g", "NAME", "con", "show"); - for (String line : lines) { - if (bondName.equals(line.trim())) { - throw new RuntimeException("网络连接名称 [" + bondName + "] 已存在,请勿重复创建"); + executeCommand( + "nmcli", + "con", + "add", + "type", + "bond", + "con-name", + bondName, + "ifname", + bondName, + "bond.options", + bondOptions + ); + + List ipCmdArgs = new ArrayList<>(Arrays.asList( + "nmcli", + "con", + "mod", + bondName, + protocol + ".method", + "manual", + protocol + ".addresses", + ipWithPrefix + )); + + if (!gateway.isEmpty()) { + ipCmdArgs.add(protocol + ".gateway"); + ipCmdArgs.add(gateway); + ipCmdArgs.add(protocol + ".never-default"); + ipCmdArgs.add("no"); + ipCmdArgs.add(protocol + ".route-metric"); + ipCmdArgs.add("200"); + } else { + ipCmdArgs.add(protocol + ".gateway"); + ipCmdArgs.add(""); + ipCmdArgs.add(protocol + ".never-default"); + ipCmdArgs.add("yes"); + } + + executeCommand(ipCmdArgs.toArray(new String[0])); + + for (String slave : slaveList) { + if (slave == null || slave.trim().isEmpty()) { + continue; } + + String slaveName = slave.trim(); + String slaveConnectionName = bondName + "-slave-" + slaveName; + + // 避免旧连接占用物理网卡 + oldConnectionSnapshots.addAll(downOldConnectionByDevice(slaveName)); + + executeCommand( + "nmcli", + "con", + "add", + "type", + "bond-slave", + "con-name", + slaveConnectionName, + "ifname", + slaveName, + "master", + bondName + ); + + createdSlaveConnectionNames.add(slaveConnectionName); } + + executeCommand("nmcli", "con", "up", bondName); + } catch (RuntimeException e) { - if (e.getMessage().contains("已存在")) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), e.getMessage()); - }else{ - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "创建 Bond 失败: " + e.getMessage()); - } + rollbackCreateBond(bondName, createdSlaveConnectionNames, oldConnectionSnapshots); + + throw new BizException( + ErrorCode.BIZ_ERROR.getCode(), + "新增 Bond 失败,已尝试回滚配置: " + e.getMessage() + ); } + } + public void configBond(BondConfigRequest req) { + String bondName = trim(req.getBondName()); + String addressType = trim(req.getAddressType()); + String ipAddress = trim(req.getIpAddress()); + String gateway = trim(req.getGateway()); + Integer prefixLength = req.getPrefixLength(); + Integer mode = req.getMode(); + String status = trim(req.getStatus()); - String bondOptions = String.format("mode=%d,miimon=100", req.getMode()); + assertBondExists(bondName); + + boolean isIpv6 = verifyAddressType(addressType); + + List newSlaveList = normalizeSlaveList(req.getSlaveList(), true); + + validateIpAndPrefix(ipAddress, gateway, prefixLength, isIpv6); + checkSlaveDevicesExist(newSlaveList); + + BondConfigSnapshot snapshot = snapshotBondConfig(bondName); + + String protocol = isIpv6 ? "ipv6" : "ipv4"; + String ipWithPrefix = ipAddress + "/" + prefixLength; try { - executeCommand("nmcli", "con", "add", - "type", "bond", - "con-name", bondName, - "ifname", bondName, - "bond.options", bondOptions); - } catch (RuntimeException e) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "创建 Bond 失败: " + e.getMessage()); - } + String bondOptions = String.format("mode=%s,miimon=100", mapBondMode(mode)); - if (req.getIpv4Config() != null) { - req.getIpv4Config().setDeviceName(bondName); - try { - setIpv4Config(req.getIpv4Config()); - } catch (Exception e) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "Bond [" + bondName + "] 创建成功,但 IP 配置失败: " + e.getMessage()); + executeCommand( + "nmcli", + "con", + "mod", + bondName, + "bond.options", + bondOptions + ); + + List ipCmdArgs = new ArrayList<>(Arrays.asList( + "nmcli", + "con", + "mod", + bondName, + protocol + ".method", + "manual", + protocol + ".addresses", + ipWithPrefix + )); + + if (!gateway.isEmpty()) { + ipCmdArgs.add(protocol + ".gateway"); + ipCmdArgs.add(gateway); + ipCmdArgs.add(protocol + ".never-default"); + ipCmdArgs.add("no"); + ipCmdArgs.add(protocol + ".route-metric"); + ipCmdArgs.add("200"); + } else { + ipCmdArgs.add(protocol + ".gateway"); + ipCmdArgs.add(""); + ipCmdArgs.add(protocol + ".never-default"); + ipCmdArgs.add("yes"); } + + executeCommand(ipCmdArgs.toArray(new String[0])); + + syncBondSlaves(bondName, newSlaveList); + + if ("Active".equalsIgnoreCase(status)) { + executeCommand("nmcli", "con", "up", bondName); + } else if ("Inactive".equalsIgnoreCase(status)) { + try { + executeCommand("nmcli", "con", "down", bondName); + } catch (RuntimeException e) { + log.warn("停用 Bond [{}] 失败: {}", bondName, e.getMessage()); + } + } else { + throw new IllegalArgumentException("状态只支持 Active 或 Inactive"); + } + + } catch (BizException e) { + rollbackBondConfig(bondName, snapshot); + throw e; + } catch (RuntimeException e) { + rollbackBondConfig(bondName, snapshot); + throw new BizException( + ErrorCode.BIZ_ERROR.getCode(), + "配置 Bond [" + bondName + "] 失败,已尝试回滚配置: " + e.getMessage() + ); } } public void deleteBond(String bondName) { - if (bondName == null || bondName.trim().isEmpty()) { - throw new IllegalArgumentException("Bond 名称不能为空"); - } - - bondName = bondName.trim(); - - List uuidsToDelete = new ArrayList<>(); - boolean bondFound = false; - - try { - List lines = executeCommand("nmcli", "-t", "-f", "UUID,NAME,TYPE", "con", "show"); - - for (String line : lines) { - if (line.trim().isEmpty()) continue; - String[] parts = line.split("(?= 3) { - String uuid = parts[0]; - String name = parts[1].replace("\\:", ":"); - String type = parts[2]; - - // 匹配到 Bond 自身 - if (bondName.equals(name) && "bond".equals(type)) { - bondFound = true; - uuidsToDelete.add(uuid); - } - else if (type != null && type.contains("ethernet")) { - try { - List masterOutput = executeCommand("nmcli", "-g", "connection.master", "con", "show", uuid); - if (!masterOutput.isEmpty() && bondName.equals(masterOutput.get(0).trim())) { - uuidsToDelete.add(0, uuid); - } - } catch (Exception e) { - log.warn("查询连接 [{}] 的 master出现异常: {}", uuid, e.getMessage()); - } - } - } - } - - if (!bondFound) { - throw new RuntimeException("Bond [" + bondName + "] 不存在"); - } - - - for (String targetUuid : uuidsToDelete) { - //先停后删 - try { - executeCommand("nmcli", "con", "down", "uuid", targetUuid); - } catch (Exception e) { - log.warn("停用连接 [{}] 时出现异常: {}", targetUuid, e.getMessage()); - } - - executeCommand("nmcli", "con", "delete", "uuid", targetUuid); - } - - } catch (RuntimeException e) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "Bond [" + bondName + "] 及从属网卡删除失败: " + e.getMessage()); - } - - } - - - - public void addSlavesTOBond(BondAddSlavesRequest req) { - String bondName = req.getBondName(); - List slaves = req.getSlaveList(); - - - if (bondName == null || bondName.trim().isEmpty()) { - throw new IllegalArgumentException("Bond 名称不能为空"); - } - if (slaves == null || slaves.isEmpty()) { - throw new IllegalArgumentException("物理网卡列表不能为空"); - } - - List existingConnections; - List existingDevices; - try { - existingConnections = executeCommand("nmcli", "-t", "-f", "UUID,DEVICE,NAME,TYPE", "con", "show"); - existingDevices = executeCommand("nmcli", "-t", "-f", "DEVICE,TYPE", "dev"); - } catch (RuntimeException e) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "获取系统网络状态失败"); - } - - - //查询bond是否存在 - boolean bondExists = false; - for (String line : existingConnections) { - String[] parts = line.split("(?= 4) { - String name = parts[2].replace("\\:", ":"); - String type = parts[3]; - if (bondName.equals(name) && "bond".equals(type)) { - bondExists = true; - break; - } - } - } - if (!bondExists) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "目标 Bond [" + bondName + "] 不存在"); - } - - - for (String phyIf : slaves) { - if (phyIf == null || phyIf.trim().isEmpty()) continue; - boolean devExists = existingDevices.stream() - .map(line -> line.split("(? devName.equals(phyIf)); - - if (!devExists) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "当前物理网卡设备 [" + phyIf + "] 在系统中不存在"); - } - } - - - List uuidsToDown = new ArrayList<>(); - for (String phyIf : slaves) { - if (phyIf == null || phyIf.trim().isEmpty()) continue; - String slaveConnectionName = bondName + "-slave-" + phyIf; - - for (String line : existingConnections) { - if (line.trim().isEmpty()) continue; - String[] parts = line.split("(?= 3) { - String uuid = parts[0]; - String device = parts[1].replace("\\:", ":"); - String name = parts[2].replace("\\:", ":"); - - if (slaveConnectionName.equals(name) || phyIf.equals(device)) { - uuidsToDown.add(uuid); - } - } - } - } - - //先停止后删除 - for (String uuid : uuidsToDown) { - try { - executeCommand("nmcli", "con", "modify", "uuid", uuid, "connection.autoconnect", "no"); - } catch (RuntimeException e) { - log.warn("修改旧连接 [{}] 的自启属性失败,该连接可能不存在: {}", uuid, e.getMessage()); - } - - try { - executeCommand("nmcli", "con", "down", "uuid", uuid); - } catch (RuntimeException e) { - log.warn("暂停旧连接 [{}] 失败,该连接可能已经处于断开状态: {}", uuid, e.getMessage()); - } - } - - - List newlyAddedSlaveNames = new ArrayList<>(); // 记录已经成功添加的,用于添加失败回滚 - try { - for (String phyIf : slaves) { - if (phyIf == null || phyIf.trim().isEmpty()) continue; - String slaveConnectionName = bondName + "-slave-" + phyIf; - - executeCommand("nmcli", "con", "add", - "type", "bond-slave", - "con-name", slaveConnectionName, - "ifname", phyIf, - "master", bondName); - - newlyAddedSlaveNames.add(slaveConnectionName); - } - - executeCommand("nmcli", "con", "up", bondName); - //激活后删除 - for (String uuid : uuidsToDown) { - try { - executeCommand("nmcli", "con", "delete", "uuid", uuid); - } catch (Exception e) { - log.warn("Bond配置已生效,但清理废弃的旧连接 [{}] 失败: {}", uuid, e.getMessage()); - } - } - - } catch (RuntimeException e) { - log.error("将网卡加入 Bond 失败,触发回滚,清理刚创建的 slave 连接", e); - - for (String addedSlaveName : newlyAddedSlaveNames) { - try { - executeCommand("nmcli", "con", "delete", addedSlaveName); - } catch (Exception rollbackEx) { - log.error("回滚失败: 无法删除的从属连接 [{}]", addedSlaveName, rollbackEx); - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "将网卡加入 Bond 失败,并且无法删除刚创建的从属连接"); - } - } - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "添加物理网卡到 Bond 失败,已清理刚创建的配置"); - } - } - - public String removeSlaveFromBond(BondRemoveSlaveRequest req) { - String bondName = trim(req.getBondName()); - List slaves = req.getSlaveList(); + bondName = trim(bondName); if (bondName.isEmpty()) { - throw new IllegalArgumentException("Bond 名称不能为空"); - } - if (slaves == null || slaves.isEmpty()) { - throw new IllegalArgumentException("物理网卡名称不能为空"); + throw new IllegalArgumentException("Bond名称不能为空"); } - List lines; + List uuidToDeleteList = new ArrayList<>(); + String bondUuid = null; + try { - lines = executeCommand("nmcli", "-t", "-f", "UUID,DEVICE,NAME,TYPE", "con", "show"); - } catch (RuntimeException e) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "获取系统网络连接列表失败"); - } - - int deletedCount = 0; - - for (String phyIf : slaves) { - if (phyIf == null || phyIf.trim().isEmpty()) continue; - - String targetUuid = null; - String slaveName = bondName + "-slave-" + phyIf; + List lines = executeCommand( + "nmcli", + "-t", + "-f", + "UUID,NAME,TYPE", + "con", + "show" + ); for (String line : lines) { - if (line.trim().isEmpty()) { + if (line == null || line.trim().isEmpty()) { continue; } String[] parts = line.split("(?= 4) { - String uuid = parts[0]; - String device = parts[1].replace("\\:", ":"); - String name = parts[2].replace("\\:", ":"); + String uuid = parts[0]; + String name = parts[1].replace("\\:", ":"); + String type = parts[2]; - if (slaveName.equals(name) || phyIf.equals(device)) { - try { - List masterOutputs = executeCommand("nmcli", "-g", "connection.master", "con", "show", uuid); - String master = masterOutputs.isEmpty() ? "" : masterOutputs.get(0).trim(); - if (bondName.equals(master)) { - targetUuid = uuid; - break; - } - } catch (RuntimeException e) { - log.warn("查询网卡 [{}] master 属性失败", name); + if (bondName.equals(name) && "bond".equals(type)) { + bondUuid = uuid; + continue; + } + + if ("ethernet".equals(type) || "802-3-ethernet".equals(type)) { + try { + List masterLines = executeCommand( + "nmcli", + "-g", + "connection.master", + "con", + "show", + "uuid", + uuid + ); + + String master = masterLines.isEmpty() ? "" : masterLines.get(0).trim(); + + if (bondName.equals(master)) { + uuidToDeleteList.add(uuid); } + } catch (RuntimeException e) { + log.warn("查询连接 [{}] 的 master 失败: {}", uuid, e.getMessage()); } } } - if (targetUuid == null) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "未找到网卡 [" + phyIf + "] 对应的从属配置,移除操作已中断"); + if (bondUuid == null) { + throw new BizException( + ErrorCode.BIZ_ERROR.getCode(), + "Bond [" + bondName + "] 不存在" + ); } - try { + for (String uuid : uuidToDeleteList) { try { - executeCommand("nmcli", "con", "down", "uuid", targetUuid); - } catch (Exception e) { - log.warn("停用从属网卡连接 [{}] 失败,忽略并继续删除: {}", targetUuid, e.getMessage()); + executeCommand("nmcli", "con", "down", "uuid", uuid); + } catch (RuntimeException e) { + log.warn("停用 Bond 从属连接 [{}] 失败,继续删除: {}", uuid, e.getMessage()); } - executeCommand("nmcli", "con", "delete", "uuid", targetUuid); - deletedCount++; - } catch (RuntimeException e) { - log.error("移除网卡 [{}] 失败: {}", phyIf, e.getMessage()); - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), String.format("从 Bond [%s] 移除网卡 [%s] 失败: %s", bondName, phyIf, e.getMessage())); + try { + executeCommand("nmcli", "con", "delete", "uuid", uuid); + } catch (RuntimeException e) { + throw new BizException( + ErrorCode.BIZ_ERROR.getCode(), + "删除 Bond 从属连接失败: " + e.getMessage() + ); + } } - } - // 只要有成功删除的记录,就重新激活 Bond 使其生效 - if (deletedCount > 0) { try { - executeCommand("nmcli", "con", "up", bondName); + executeCommand("nmcli", "con", "down", "uuid", bondUuid); } catch (RuntimeException e) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "网卡移除成功,但重新激活 Bond [" + bondName + "] 失败: " + e.getMessage()); + log.warn("停用 Bond 主连接 [{}] 失败,继续删除: {}", bondName, e.getMessage()); + } + + executeCommand("nmcli", "con", "delete", "uuid", bondUuid); + + } catch (BizException e) { + throw e; + } catch (RuntimeException e) { + throw new BizException( + ErrorCode.BIZ_ERROR.getCode(), + "删除 Bond [" + bondName + "] 失败: " + e.getMessage() + ); + } + } + + + public PageResult getRoutePage(RouteTableRequest req) { + List resultList = new ArrayList<>(); + + String targetType = trim(req.getTargetType()); + String targetStatus = trim(req.getStatus()); + + int pageNum = req.getPageNum() != null && req.getPageNum() > 0 ? req.getPageNum() : 1; + int pageSize = req.getPageSize() != null && req.getPageSize() > 0 ? req.getPageSize() : 10; + + Map deviceStatusMap = getDeviceStatusMap(); + + readRouteLines(resultList, false, deviceStatusMap, targetType, targetStatus); + readRouteLines(resultList, true, deviceStatusMap, targetType, targetStatus); + + int total = resultList.size(); + int fromIndex = (pageNum - 1) * pageSize; + int toIndex = Math.min(fromIndex + pageSize, total); + + List pageList; + if (fromIndex >= total) { + pageList = new ArrayList<>(); + } else { + pageList = resultList.subList(fromIndex, toIndex); + } + + return new PageResult<>(total, pageList); + } + + public void addRoute(RouteCreateRequest req) { + String addressType = trim(req.getAddressType()); + String targetType = trim(req.getTargetType()); + String targetAddress = trim(req.getTargetAddress()); + Integer prefixLength = req.getPrefixLength(); + String interfaceName = trim(req.getInterfaceName()); + String nextHop = trim(req.getNextHop()); + + boolean isIpv6 = verifyAddressType(addressType); + + validateRouteRequest(targetType, targetAddress, prefixLength, interfaceName, nextHop, isIpv6); + + String connectionName = getConNameByDeviceNameIncludeInactive(interfaceName); + if (connectionName == null) { + throw new BizException( + ErrorCode.BIZ_ERROR.getCode(), + "未找到网口 [" + interfaceName + "] 对应的网络连接配置" + ); + } + + String targetCidr = targetAddress + "/" + prefixLength; + String routeValue = nextHop.isEmpty() ? targetCidr : targetCidr + " " + nextHop; + + String routeKey = isIpv6 ? "+ipv6.routes" : "+ipv4.routes"; + + // 修改前保存旧路由,失败时回滚 + String oldRoutes = getConnectionRoutes(connectionName, isIpv6); + + try { + executeCommand( + "nmcli", + "con", + "mod", + connectionName, + routeKey, + routeValue + ); + + applyConnection(connectionName, interfaceName); + + } catch (RuntimeException e) { + rollbackRoutes(connectionName, oldRoutes, interfaceName, isIpv6); + + throw new BizException( + ErrorCode.BIZ_ERROR.getCode(), + "新增路由失败,已回滚: " + e.getMessage() + ); + } + } + + public void updateRoute(RouteUpdateRequest req) { + RouteCreateRequest oldRoute = req.getOldRoute(); + RouteCreateRequest newRoute = req.getNewRoute(); + + if (oldRoute == null) { + throw new IllegalArgumentException("旧路由信息不能为空"); + } + if (newRoute == null) { + throw new IllegalArgumentException("新路由信息不能为空"); + } + + String oldAddressType = trim(oldRoute.getAddressType()); + String oldTargetType = trim(oldRoute.getTargetType()); + String oldTargetAddress = trim(oldRoute.getTargetAddress()); + Integer oldPrefixLength = oldRoute.getPrefixLength(); + String oldInterfaceName = trim(oldRoute.getInterfaceName()); + String oldNextHop = trim(oldRoute.getNextHop()); + + String newAddressType = trim(newRoute.getAddressType()); + String newTargetType = trim(newRoute.getTargetType()); + String newTargetAddress = trim(newRoute.getTargetAddress()); + Integer newPrefixLength = newRoute.getPrefixLength(); + String newInterfaceName = trim(newRoute.getInterfaceName()); + String newNextHop = trim(newRoute.getNextHop()); + + boolean oldIsIpv6 = verifyAddressType(oldAddressType); + boolean newIsIpv6 = verifyAddressType(newAddressType); + + validateRouteRequest( + oldTargetType, + oldTargetAddress, + oldPrefixLength, + oldInterfaceName, + oldNextHop, + oldIsIpv6 + ); + + validateRouteRequest( + newTargetType, + newTargetAddress, + newPrefixLength, + newInterfaceName, + newNextHop, + newIsIpv6 + ); + + String oldConnectionName = getConNameByDeviceNameIncludeInactive(oldInterfaceName); + if (oldConnectionName == null) { + throw new BizException( + ErrorCode.BIZ_ERROR.getCode(), + "未找到旧路由网口 [" + oldInterfaceName + "] 对应的网络连接配置" + ); + } + + String newConnectionName = getConNameByDeviceNameIncludeInactive(newInterfaceName); + if (newConnectionName == null) { + throw new BizException( + ErrorCode.BIZ_ERROR.getCode(), + "未找到新路由网口 [" + newInterfaceName + "] 对应的网络连接配置" + ); + } + + String oldRouteValue = buildRouteValue(oldTargetAddress, oldPrefixLength, oldNextHop); + String newRouteValue = buildRouteValue(newTargetAddress, newPrefixLength, newNextHop); + + String oldRouteKey = oldIsIpv6 ? "ipv6.routes" : "ipv4.routes"; + String newRouteKey = newIsIpv6 ? "ipv6.routes" : "ipv4.routes"; + + List snapshots = new ArrayList<>(); + + addRouteSnapshot( + snapshots, + oldConnectionName, + oldInterfaceName, + oldIsIpv6 + ); + + addRouteSnapshot( + snapshots, + newConnectionName, + newInterfaceName, + newIsIpv6 + ); + + try { + executeCommand( + "nmcli", + "con", + "mod", + oldConnectionName, + "-" + oldRouteKey, + oldRouteValue + ); + + executeCommand( + "nmcli", + "con", + "mod", + newConnectionName, + "+" + newRouteKey, + newRouteValue + ); + + applyRouteSnapshots(snapshots); + + } catch (RuntimeException e) { + rollbackRouteSnapshots(snapshots); + + throw new BizException( + ErrorCode.BIZ_ERROR.getCode(), + "修改路由失败,已尝试回滚: " + e.getMessage() + ); + } + } + + + public void deleteRoute(RouteDeleteRequest req) { + String addressType = trim(req.getAddressType()); + String targetType = trim(req.getTargetType()); + String targetAddress = trim(req.getTargetAddress()); + Integer prefixLength = req.getPrefixLength(); + String interfaceName = trim(req.getInterfaceName()); + String nextHop = trim(req.getNextHop()); + + boolean isIpv6 = verifyAddressType(addressType); + + validateRouteRequest( + targetType, + targetAddress, + prefixLength, + interfaceName, + nextHop, + isIpv6 + ); + + String connectionName = getConNameByDeviceNameIncludeInactive(interfaceName); + if (connectionName == null) { + throw new BizException( + ErrorCode.BIZ_ERROR.getCode(), + "未找到网口 [" + interfaceName + "] 对应的网络连接配置" + ); + } + + String routeValue = buildRouteValue(targetAddress, prefixLength, nextHop); + String routeKey = isIpv6 ? "-ipv6.routes" : "-ipv4.routes"; + + // 删除前保存旧路由配置,失败时回滚 + String oldRoutes = getConnectionRoutes(connectionName, isIpv6); + + try { + executeCommand( + "nmcli", + "con", + "mod", + connectionName, + routeKey, + routeValue + ); + + applyConnection(connectionName, interfaceName); + + } catch (RuntimeException e) { + rollbackRoutes(connectionName, oldRoutes, interfaceName, isIpv6); + + throw new BizException( + ErrorCode.BIZ_ERROR.getCode(), + "删除路由失败,已尝试回滚,请检查该路由是否存在: " + e.getMessage() + ); + } + } + + + private Set getBondSlaveDeviceSet() { + Set result = new HashSet<>(); + + List lines = executeCommand( + "nmcli", + "-t", + "-f", + "NAME,TYPE", + "con", + "show" + ); + + for (String line : lines) { + if (line == null || line.trim().isEmpty()) { + continue; + } + + String[] parts = line.split("(? resultList, + boolean isIpv6, + Map deviceStatusMap, + String targetType, + String targetStatus + ) { + List lines; + + try { + if (isIpv6) { + lines = executeCommand("ip", "-6", "route", "show"); + } else { + lines = executeCommand("ip", "route", "show"); + } + } catch (RuntimeException e) { + String type = isIpv6 ? "IPv6" : "IPv4"; + throw new BizException( + ErrorCode.BIZ_ERROR.getCode(), + "读取系统" + type + "路由表失败: " + e.getMessage() + ); + } + + for (String line : lines) { + if (line == null || line.trim().isEmpty()) { + continue; + } + + RouteTableResponse item = parseRouteLine(line.trim(), deviceStatusMap, isIpv6); + if (item == null) { + continue; + } + + if (!targetType.isEmpty() && !targetType.equals(item.getTargetType())) { + continue; + } + + if (!targetStatus.isEmpty() && !targetStatus.equals(item.getStatus())) { + continue; + } + + resultList.add(item); + } + } + + + private void applyRouteSnapshots(List snapshots) { + List appliedConnectionList = new ArrayList<>(); + + for (RouteConfigSnapshot snapshot : snapshots) { + String key = snapshot.connectionName + "|" + snapshot.interfaceName; + + if (appliedConnectionList.contains(key)) { + continue; + } + + applyConnection(snapshot.connectionName, snapshot.interfaceName); + appliedConnectionList.add(key); + } + } + + private void rollbackRouteSnapshots(List snapshots) { + if (snapshots == null || snapshots.isEmpty()) { + return; + } + + for (RouteConfigSnapshot snapshot : snapshots) { + String routeKey = snapshot.ipv6 ? "ipv6.routes" : "ipv4.routes"; + + try { + executeCommand( + "nmcli", + "con", + "mod", + snapshot.connectionName, + routeKey, + snapshot.oldRoutes == null ? "" : snapshot.oldRoutes + ); + } catch (RuntimeException e) { + log.warn( + "回滚连接 [{}] 的 [{}] 失败: {}", + snapshot.connectionName, + routeKey, + e.getMessage() + ); + } + } + + for (RouteConfigSnapshot snapshot : snapshots) { + try { + applyConnection(snapshot.connectionName, snapshot.interfaceName); + } catch (RuntimeException e) { + log.warn( + "回滚后应用连接 [{}] 失败: {}", + snapshot.connectionName, + e.getMessage() + ); + } + } + } + + private String buildRouteValue(String targetAddress, Integer prefixLength, String nextHop) { + String targetCidr = trim(targetAddress) + "/" + prefixLength; + nextHop = trim(nextHop); + + return nextHop.isEmpty() ? targetCidr : targetCidr + " " + nextHop; + } + + private void addRouteSnapshot( + List snapshots, + String connectionName, + String interfaceName, + boolean isIpv6 + ) { + for (RouteConfigSnapshot snapshot : snapshots) { + if (snapshot.connectionName.equals(connectionName) && snapshot.ipv6 == isIpv6) { + return; + } + } + + String oldRoutes = getConnectionRoutes(connectionName, isIpv6); + snapshots.add(new RouteConfigSnapshot(connectionName, interfaceName, isIpv6, oldRoutes)); + } + + + + + private void rollbackRoutes(String connectionName, String oldRoutes, String interfaceName, boolean isIpv6) { + String routeKey = isIpv6 ? "ipv6.routes" : "ipv4.routes"; + + try { + executeCommand( + "nmcli", + "con", + "mod", + connectionName, + routeKey, + oldRoutes == null ? "" : oldRoutes + ); + + try { + applyConnection(connectionName, interfaceName); + } catch (RuntimeException e) { + log.warn("回滚 [{}] 后应用连接 [{}] 失败: {}", routeKey, connectionName, e.getMessage()); + } + + } catch (RuntimeException e) { + log.warn("回滚连接 [{}] 的 [{}] 失败: {}", connectionName, routeKey, e.getMessage()); + } + } + + + private String getConnectionRoutes(String connectionName, boolean isIpv6) { + String routeKey = isIpv6 ? "ipv6.routes" : "ipv4.routes"; + + try { + List lines = executeCommand( + "nmcli", + "-g", + routeKey, + "con", + "show", + connectionName + ); + + if (lines.isEmpty() || lines.get(0) == null) { + return ""; + } + + return lines.get(0).trim().replace("\\:", ":"); + + } catch (RuntimeException e) { + log.warn("读取连接 [{}] 的 [{}] 配置失败: {}", connectionName, routeKey, e.getMessage()); + return ""; + } + } + + private void rollbackBondConfig(String bondName, BondConfigSnapshot snapshot) { + if (snapshot == null) { + return; + } + + try { + if (snapshot.bondOptions != null && !snapshot.bondOptions.trim().isEmpty()) { + executeCommand( + "nmcli", + "con", + "mod", + bondName, + "bond.options", + snapshot.bondOptions + ); + } + } catch (Exception e) { + log.warn("回滚 Bond [{}] 模式失败: {}", bondName, e.getMessage()); + } + + try { + List ipv4Args = new ArrayList<>(Arrays.asList( + "nmcli", + "con", + "mod", + bondName, + "ipv4.method", + snapshot.ipv4Method == null || snapshot.ipv4Method.trim().isEmpty() + ? "disabled" + : snapshot.ipv4Method, + "ipv4.addresses", + snapshot.ipv4Addresses == null ? "" : snapshot.ipv4Addresses, + "ipv4.gateway", + snapshot.ipv4Gateway == null ? "" : snapshot.ipv4Gateway + )); + + executeCommand(ipv4Args.toArray(new String[0])); + } catch (Exception e) { + log.warn("回滚 Bond [{}] IPv4 配置失败: {}", bondName, e.getMessage()); + } + + try { + List ipv6Args = new ArrayList<>(Arrays.asList( + "nmcli", + "con", + "mod", + bondName, + "ipv6.method", + snapshot.ipv6Method == null || snapshot.ipv6Method.trim().isEmpty() + ? "disabled" + : snapshot.ipv6Method, + "ipv6.addresses", + snapshot.ipv6Addresses == null ? "" : snapshot.ipv6Addresses, + "ipv6.gateway", + snapshot.ipv6Gateway == null ? "" : snapshot.ipv6Gateway + )); + + executeCommand(ipv6Args.toArray(new String[0])); + } catch (Exception e) { + log.warn("回滚 Bond [{}] IPv6 配置失败: {}", bondName, e.getMessage()); + } + + try { + syncBondSlaves(bondName, snapshot.slaveList); + } catch (Exception e) { + log.warn("回滚 Bond [{}] 从属网卡失败: {}", bondName, e.getMessage()); + } + + try { + if (snapshot.active) { + executeCommand("nmcli", "con", "up", bondName); + } else { + try { + executeCommand("nmcli", "con", "down", bondName); + } catch (Exception e) { + log.warn("回滚停用 Bond [{}] 失败: {}", bondName, e.getMessage()); + } + } + } catch (Exception e) { + log.warn("回滚 Bond [{}] 激活状态失败: {}", bondName, e.getMessage()); + } + } + + + private BondConfigSnapshot snapshotBondConfig(String bondName) { + List lines = executeCommand( + "nmcli", + "-g", + "bond.options,IPv4.METHOD,IPv4.ADDRESSES,IPv4.GATEWAY,IPv6.METHOD,IPv6.ADDRESSES,IPv6.GATEWAY", + "con", + "show", + bondName + ); + + String bondOptions = getLineValue(lines, 0); + String ipv4Method = getLineValue(lines, 1); + String ipv4Addresses = getLineValue(lines, 2).replace("\\:", ":"); + String ipv4Gateway = getLineValue(lines, 3); + String ipv6Method = getLineValue(lines, 4); + String ipv6Addresses = getLineValue(lines, 5).replace("\\:", ":"); + String ipv6Gateway = getLineValue(lines, 6).replace("\\:", ":"); + + List oldSlaveList = getBondSlaves(bondName); + boolean active = isConnectionActive(bondName); + + return new BondConfigSnapshot( + bondOptions, + ipv4Method, + ipv4Addresses, + ipv4Gateway, + ipv6Method, + ipv6Addresses, + ipv6Gateway, + oldSlaveList, + active + ); + } + + + private boolean verifyAddressType(String addressType){ + boolean isIpv6; + if ("IPv6".equalsIgnoreCase(addressType)) { + isIpv6 = true; + } else if ("IPv4".equalsIgnoreCase(addressType)) { + isIpv6 = false; + } else { + throw new IllegalArgumentException("地址类型只支持 IPv4 或 IPv6"); + } + return isIpv6; + } + + private List downOldConnectionByDevice(String deviceName) { + List snapshots = new ArrayList<>(); + + deviceName = trim(deviceName); + if (deviceName.isEmpty()) { + return snapshots; + } + + List activeUuids = executeCommand( + "nmcli", + "-t", + "-f", + "UUID", + "con", + "show", + "--active" + ); + + List lines = executeCommand( + "nmcli", + "-t", + "-f", + "UUID,NAME,TYPE,DEVICE", + "con", + "show" + ); + + for (String line : lines) { + if (line == null || line.trim().isEmpty()) { + continue; + } + + String[] parts = line.split("(? ifLines = executeCommand( + "nmcli", + "-g", + "connection.interface-name", + "con", + "show", + "uuid", + uuid + ); + + if (!ifLines.isEmpty() && ifLines.get(0) != null) { + interfaceName = ifLines.get(0).trim().replace("\\:", ":"); + } + } catch (RuntimeException e) { + log.warn("读取连接 [{}] interface-name 失败,uuid: {}, 原因: {}", name, uuid, e.getMessage()); + } + + boolean matchDevice = deviceName.equals(activeDevice) || deviceName.equals(interfaceName); + if (!matchDevice) { + continue; + } + + String autoconnect = "yes"; + try { + List autoLines = executeCommand( + "nmcli", + "-g", + "connection.autoconnect", + "con", + "show", + "uuid", + uuid + ); + + if (!autoLines.isEmpty() && !autoLines.get(0).trim().isEmpty()) { + autoconnect = autoLines.get(0).trim(); + } + } catch (RuntimeException e) { + log.warn("读取旧连接 [{}] 自启配置失败,uuid: {}, 原因: {}", name, uuid, e.getMessage()); + } + + boolean active = activeUuids.stream() + .anyMatch(activeUuid -> uuid.equals(activeUuid.trim())); + + snapshots.add(new OldConnectionSnapshot(uuid, autoconnect, active)); + + try { + executeCommand( + "nmcli", + "con", + "modify", + "uuid", + uuid, + "connection.autoconnect", + "no" + ); + } catch (RuntimeException e) { + log.warn("禁用旧连接 [{}] 自启失败,uuid: {}, 原因: {}", name, uuid, e.getMessage()); + } + + + if (active) { + try { + executeCommand("nmcli", "con", "down", "uuid", uuid); + } catch (RuntimeException e) { + log.warn("停用旧连接 [{}] 失败,uuid: {}, 原因: {}", name, uuid, e.getMessage()); + } + } + } + + return snapshots; + } + + + + private String mapBondMode(Integer mode) { + if (mode == null) { + throw new IllegalArgumentException("Bond类型不能为空"); + } + switch (mode) { + case 0: + return "balance-rr"; + case 1: + return "active-backup"; + case 2: + return "balance-xor"; + case 4: + return "802.3ad"; + default: + throw new IllegalArgumentException("不支持当前Bond类型"); + } + } + + private String getLineValue(List lines, int index) { + if (lines == null || lines.size() <= index || lines.get(index) == null) { + return ""; + } + return lines.get(index).trim(); + } + + private BondTableResponse createBondTableItem( + String bondName, + String bondMode, + String addressType, + String ipWithPrefix, + String gateway, + String status + ) { + BondTableResponse item = new BondTableResponse(); + item.setBondName(bondName); + item.setBondMode(bondMode); + item.setAddressType(addressType); + item.setGateway(gateway == null ? "" : gateway); + item.setStatus(status); + + String normalizedIp = ipWithPrefix == null ? "" : ipWithPrefix.trim().replace("\\:", ":"); + + if (normalizedIp.contains("/")) { + String[] parts = normalizedIp.split("/", 2); + item.setAddress(parts[0]); + item.setPrefixLength(parts[1]); + } else { + item.setAddress(normalizedIp); + item.setPrefixLength(""); + } + + return item; + } + + private List splitAddressList(String rawAddresses) { + List result = new ArrayList<>(); + + if (rawAddresses == null || rawAddresses.trim().isEmpty()) { + return result; + } + + String[] arr = rawAddresses.trim().replace("\\:", ":").split(","); + for (String item : arr) { + if (item != null && !item.trim().isEmpty()) { + result.add(item.trim()); + } + } + + return result; + } + + private String getConNameByDeviceNameIncludeInactive(String deviceName) { + deviceName = trim(deviceName); + if (deviceName.isEmpty()) { + return null; + } + + String activeConnectionName = getConnectionNameByDeviceName(deviceName); + if (activeConnectionName != null && !activeConnectionName.trim().isEmpty()) { + return activeConnectionName; + } + + try { + List lines = executeCommand( + "nmcli", + "-t", + "-f", + "UUID,NAME,TYPE,DEVICE", + "con", + "show" + ); + + for (String line : lines) { + if (line == null || line.trim().isEmpty()) { + continue; + } + + String[] parts = line.split("(? interfaceLines = executeCommand( + "nmcli", + "-g", + "connection.interface-name", + "con", + "show", + "uuid", + uuid + ); + + if (!interfaceLines.isEmpty() && interfaceLines.get(0) != null) { + interfaceName = interfaceLines.get(0).trim().replace("\\:", ":"); + } + } catch (RuntimeException e) { + log.warn( + "读取连接 [{}] 的 interface-name 失败,uuid: {}, type: {}, 原因: {}", + connectionName, + uuid, + type, + e.getMessage() + ); + } + + if (deviceName.equals(interfaceName)) { + return connectionName; + } + +// if ((interfaceName == null || interfaceName.isEmpty() || "--".equals(interfaceName)) +// && deviceName.equals(connectionName)) { +// return connectionName; +// } + } + + } catch (RuntimeException e) { + log.warn("根据设备名 [{}] 查找连接配置失败: {}", deviceName, e.getMessage()); + } + + return null; + } + + + + + private void rollbackCreateBond( + String bondName, + List slaveConnectionNames, + List oldConnectionSnapshots + ) { + + if (slaveConnectionNames != null) { + for (String slaveConnectionName : slaveConnectionNames) { + if (slaveConnectionName == null || slaveConnectionName.trim().isEmpty()) { + continue; + } + + try { + executeCommand("nmcli", "con", "down", slaveConnectionName); + } catch (Exception e) { + log.warn("回滚停用 Bond 从属连接失败: {}, 原因: {}", slaveConnectionName, e.getMessage()); + } + + try { + executeCommand("nmcli", "con", "delete", slaveConnectionName); + } catch (Exception e) { + log.warn("回滚删除 Bond 从属连接失败: {}, 原因: {}", slaveConnectionName, e.getMessage()); + } + } + } + + + if (bondName != null && !bondName.trim().isEmpty()) { + try { + executeCommand("nmcli", "con", "down", bondName); + } catch (Exception e) { + log.warn("回滚停用 Bond 主连接失败: {}, 原因: {}", bondName, e.getMessage()); + } + + try { + executeCommand("nmcli", "con", "delete", bondName); + } catch (Exception e) { + log.warn("回滚删除 Bond 主连接失败: {}, 原因: {}", bondName, e.getMessage()); + } + } + + + if (oldConnectionSnapshots != null) { + for (OldConnectionSnapshot snapshot : oldConnectionSnapshots) { + if (snapshot == null || snapshot.uuid == null || snapshot.uuid.trim().isEmpty()) { + continue; + } + + String uuid = snapshot.uuid.trim(); + String autoconnect = snapshot.autoconnect == null || snapshot.autoconnect.trim().isEmpty() + ? "yes" + : snapshot.autoconnect.trim(); + + try { + executeCommand( + "nmcli", + "con", + "modify", + "uuid", + uuid, + "connection.autoconnect", + autoconnect + ); + } catch (Exception e) { + log.warn("回滚恢复旧连接自启失败,uuid: {}, 原因: {}", uuid, e.getMessage()); + } + + if (snapshot.active) { + try { + executeCommand("nmcli", "con", "up", "uuid", uuid); + } catch (Exception e) { + log.warn("回滚恢复旧连接激活失败,uuid: {}, 原因: {}", uuid, e.getMessage()); + } + } + } + } + } + + + + + private void assertBondExists(String bondName) { + if (bondName == null || bondName.trim().isEmpty()) { + throw new IllegalArgumentException("Bond名称不能为空"); + } + + try { + List lines = executeCommand( + "nmcli", + "-g", + "connection.type", + "con", + "show", + bondName + ); + + String type = lines.isEmpty() ? "" : lines.get(0).trim(); + + if (!"bond".equals(type)) { + throw new BizException( + ErrorCode.BIZ_ERROR.getCode(), + "连接 [" + bondName + "] 不是 Bond 类型" + ); + } + } catch (BizException e) { + throw e; + } catch (RuntimeException e) { + throw new BizException( + ErrorCode.BIZ_ERROR.getCode(), + "Bond [" + bondName + "] 不存在" + ); + } + } + + + private void syncBondSlaves(String bondName, List newSlaveList) { + List oldSlaveList = normalizeSlaveList(getBondSlaves(bondName), false); + List normalizedNewSlaveList = normalizeSlaveList(newSlaveList, true); + + List needAddList = new ArrayList<>(); + for (String newSlave : normalizedNewSlaveList) { + if (!oldSlaveList.contains(newSlave)) { + needAddList.add(newSlave); + } + } + + List needRemoveList = new ArrayList<>(); + for (String oldSlave : oldSlaveList) { + if (!normalizedNewSlaveList.contains(oldSlave)) { + needRemoveList.add(oldSlave); + } + } + + if (!needAddList.isEmpty()) { + BondAddSlavesRequest addReq = new BondAddSlavesRequest(); + addReq.setBondName(bondName); + addReq.setSlaveList(needAddList); + addSlavesTOBond(addReq); + } + + if (!needRemoveList.isEmpty()) { + BondRemoveSlaveRequest removeReq = new BondRemoveSlaveRequest(); + removeReq.setBondName(bondName); + removeReq.setSlaveList(needRemoveList); + removeSlaveFromBond(removeReq); + } + } + + + private void checkSlaveDevicesExist(List slaveList) { + List deviceLines = executeCommand("nmcli", "-t", "-f", "DEVICE,TYPE", "dev"); + + for (String slave : slaveList) { + String slaveName = slave.trim(); + + boolean exists = false; + boolean isEthernet = false; + + for (String line : deviceLines) { + String[] parts = line.split("(? lines = executeCommand( + "nmcli", + "-t", + "-f", + "NAME", + "con", + "show", + "--active" + ); + + for (String line : lines) { + if (connectionName.equals(line.replace("\\:", ":").trim())) { + return true; + } + } + } catch (RuntimeException e) { + log.warn("判断连接 [{}] 是否激活失败: {}", connectionName, e.getMessage()); + } + + return false; + } + + + private void validateIpAndPrefix(String ipAddress, String gateway, Integer prefixLength, boolean isIpv6) { + if (isIpv6) { + if (!isValidIpv6(ipAddress)) { + throw new IllegalArgumentException("无效的 IPv6 地址格式: " + ipAddress); + } + if (gateway != null && !gateway.trim().isEmpty() && !isValidIpv6(gateway)) { + throw new IllegalArgumentException("无效的 IPv6 网关格式: " + gateway); + } + if (prefixLength == null || prefixLength < 0 || prefixLength > 128) { + throw new IllegalArgumentException("IPv6 前缀长度必须在 0-128 之间"); + } + } else { + if (!IPV4_PATTERN.matcher(ipAddress).matches()) { + throw new IllegalArgumentException("无效的 IPv4 地址格式: " + ipAddress); + } + if (gateway != null && !gateway.trim().isEmpty() && !IPV4_PATTERN.matcher(gateway).matches()) { + throw new IllegalArgumentException("无效的 IPv4 网关格式: " + gateway); + } + if (prefixLength == null || prefixLength < 0 || prefixLength > 32) { + throw new IllegalArgumentException("IPv4 前缀长度必须在 0-32 之间"); + } + } + } + + + private RouteTableResponse parseRouteLine( + String line, + Map deviceStatusMap, + boolean isIpv6 + ) { + String[] parts = line.split("\\s+"); + if (parts.length == 0) { + return null; + } + + String destination = parts[0]; + + // 默认路由不展示在这个静态路由列表里 + if ("default".equals(destination)) { + return null; + } + + // 过滤掉一些非普通静态路由 + if ("unreachable".equals(destination) + || "throw".equals(destination) + || "prohibit".equals(destination) + || "blackhole".equals(destination)) { + return null; + } + + String nextHop = ""; + String interfaceName = ""; + + for (int i = 1; i < parts.length; i++) { + if ("via".equals(parts[i]) && i + 1 < parts.length) { + nextHop = parts[i + 1]; + } else if ("dev".equals(parts[i]) && i + 1 < parts.length) { + interfaceName = parts[i + 1]; + } + } + + if (interfaceName.isEmpty()) { + return null; + } + + String targetAddress; + String prefixLength; + + if (destination.contains("/")) { + String[] destParts = destination.split("/", 2); + targetAddress = destParts[0]; + prefixLength = destParts[1]; + } else { + targetAddress = destination; + prefixLength = isIpv6 ? "128" : "32"; + } + + String hostPrefix = isIpv6 ? "128" : "32"; + String targetType = hostPrefix.equals(prefixLength) ? "HOST" : "NETWORK"; + + RouteTableResponse item = new RouteTableResponse(); + + item.setAddressType(isIpv6 ? "IPv6" : "IPv4"); + + item.setTargetType(targetType); + item.setTargetAddress(targetAddress); + item.setPrefixLength(prefixLength); + item.setInterfaceName(interfaceName); + item.setNextHop(nextHop); + item.setStatus(deviceStatusMap.getOrDefault(interfaceName, "Inactive")); + + return item; + } + + + private Map getDeviceStatusMap() { + Map statusMap = new HashMap<>(); + + List lines = executeCommand( + "nmcli", + "-t", + "-f", + "DEVICE,STATE", + "dev", + "status" + ); + + for (String line : lines) { + if (line == null || line.trim().isEmpty()) { + continue; + } + + String[] parts = line.split("(? 128) { + throw new IllegalArgumentException("IPv6 前缀长度必须在 0-128 之间"); + } + + if (("HOST".equalsIgnoreCase(targetType) || "主机地址".equals(targetType)) + && prefixLength != 128) { + throw new IllegalArgumentException("IPv6 主机地址的前缀长度必须为 128"); + } + + } else { + if (!IPV4_PATTERN.matcher(targetAddress).matches()) { + throw new IllegalArgumentException("无效的 IPv4 目标地址格式: " + targetAddress); + } + + if (nextHop != null && !nextHop.trim().isEmpty() + && !IPV4_PATTERN.matcher(nextHop.trim()).matches()) { + throw new IllegalArgumentException("无效的 IPv4 下一跳地址格式: " + nextHop); + } + + if (prefixLength == null || prefixLength < 0 || prefixLength > 32) { + throw new IllegalArgumentException("IPv4 前缀长度必须在 0-32 之间"); + } + + if (("HOST".equalsIgnoreCase(targetType) || "主机地址".equals(targetType)) + && prefixLength != 32) { + throw new IllegalArgumentException("IPv4 主机地址的前缀长度必须为 32"); + } + } + } + + private void applyConnection(String connectionName, String interfaceName) { + try { + executeCommand("nmcli", "dev", "reapply", interfaceName); + } catch (RuntimeException e) { + log.warn("reapply 网口 [{}] 失败,尝试重新激活连接 [{}]: {}", interfaceName, connectionName, e.getMessage()); + executeCommand("nmcli", "con", "up", connectionName); + } + } + + + public void addSlavesTOBond(BondAddSlavesRequest req) { + String bondName = trim(req.getBondName()); + List slaves = normalizeSlaveList(req.getSlaveList(), true); + + assertBondExists(bondName); + checkSlaveDevicesExist(slaves); + + List existingSlaveList = normalizeSlaveList(getBondSlaves(bondName), false); + + List needAddList = new ArrayList<>(); + for (String slave : slaves) { + if (!existingSlaveList.contains(slave)) { + needAddList.add(slave); + } + } + + if (needAddList.isEmpty()) { + return; + } + + List createdSlaveConnectionNames = new ArrayList<>(); + List oldConnectionSnapshots = new ArrayList<>(); + + try { + for (String slaveName : needAddList) { + String slaveConnectionName = bondName + "-slave-" + slaveName; + + checkConnectionNameNotExists(slaveConnectionName); + + oldConnectionSnapshots.addAll(downOldConnectionByDevice(slaveName)); + + executeCommand( + "nmcli", + "con", + "add", + "type", + "bond-slave", + "con-name", + slaveConnectionName, + "ifname", + slaveName, + "master", + bondName + ); + + createdSlaveConnectionNames.add(slaveConnectionName); + } + + executeCommand("nmcli", "con", "up", bondName); + + } catch (RuntimeException e) { + rollbackAddSlavesToBond(createdSlaveConnectionNames, oldConnectionSnapshots); + + throw new BizException( + ErrorCode.BIZ_ERROR.getCode(), + "添加物理网卡到 Bond 失败,已尝试回滚: " + e.getMessage() + ); + } + } + + + public String removeSlaveFromBond(BondRemoveSlaveRequest req) { + String bondName = trim(req.getBondName()); + List slaves = normalizeSlaveList(req.getSlaveList(), true); + + assertBondExists(bondName); + + List allSlaveSnapshots = getBondSlaveSnapshots(bondName); + List targetSlaveSnapshots = new ArrayList<>(); + + for (String slave : slaves) { + BondSlaveSnapshot target = null; + + for (BondSlaveSnapshot snapshot : allSlaveSnapshots) { + if (slave.equals(snapshot.interfaceName)) { + target = snapshot; + break; + } + } + + if (target == null) { + throw new BizException( + ErrorCode.BIZ_ERROR.getCode(), + "未找到网卡 [" + slave + "] 对应的从属配置,移除操作已中断" + ); + } + + targetSlaveSnapshots.add(target); + } + + List deletedSlaveSnapshots = new ArrayList<>(); + + try { + for (BondSlaveSnapshot snapshot : targetSlaveSnapshots) { + try { + executeCommand("nmcli", "con", "down", "uuid", snapshot.uuid); + } catch (Exception e) { + log.warn("停用从属网卡连接 [{}] 失败,忽略并继续删除: {}", snapshot.name, e.getMessage()); + } + + executeCommand("nmcli", "con", "delete", "uuid", snapshot.uuid); + deletedSlaveSnapshots.add(snapshot); + } + + if (!deletedSlaveSnapshots.isEmpty()) { + try { + executeCommand("nmcli", "con", "up", bondName); + } catch (RuntimeException e) { + rollbackDeletedBondSlaves(bondName, deletedSlaveSnapshots); + throw new BizException( + ErrorCode.BIZ_ERROR.getCode(), + "网卡移除成功,但重新激活 Bond [" + bondName + "] 失败,已尝试恢复: " + e.getMessage() + ); + } + } + + return String.format("成功从 Bond [%s] 中移除了 %d 个网卡", bondName, deletedSlaveSnapshots.size()); + + } catch (BizException e) { + throw e; + } catch (RuntimeException e) { + rollbackDeletedBondSlaves(bondName, deletedSlaveSnapshots); + + throw new BizException( + ErrorCode.BIZ_ERROR.getCode(), + "从 Bond [" + bondName + "] 移除网卡失败,已尝试恢复: " + e.getMessage() + ); + } + } + + private List getBondSlaveSnapshots(String bondName) { + List result = new ArrayList<>(); + + bondName = trim(bondName); + if (bondName.isEmpty()) { + return result; + } + + List lines = executeCommand( + "nmcli", + "-t", + "-f", + "UUID,NAME,TYPE,DEVICE", + "con", + "show" + ); + + String bondUuid = null; + + for (String line : lines) { + if (line == null || line.trim().isEmpty()) { + continue; + } + + String[] parts = line.split("(? detailLines = executeCommand( + "nmcli", + "-g", + "connection.master,connection.interface-name", + "con", + "show", + "uuid", + uuid + ); + + if (detailLines.size() > 0 && detailLines.get(0) != null) { + master = detailLines.get(0).trim().replace("\\:", ":"); + } + + if (detailLines.size() > 1 && detailLines.get(1) != null) { + interfaceName = detailLines.get(1).trim().replace("\\:", ":"); + } + } catch (RuntimeException e) { + log.warn("读取连接 [{}] 的 master/interface-name 失败,uuid: {}, 原因: {}", name, uuid, e.getMessage()); + continue; + } + + boolean belongToBond = bondName.equals(master) + || (bondUuid != null && bondUuid.equals(master)); + + if (!belongToBond) { + continue; + } + + String slaveName = interfaceName; + + if (slaveName == null || slaveName.trim().isEmpty() || "--".equals(slaveName.trim())) { + slaveName = activeDevice; + } + + if (slaveName == null || slaveName.trim().isEmpty() || "--".equals(slaveName.trim())) { + String prefix = bondName + "-slave-"; + if (name.startsWith(prefix)) { + slaveName = name.substring(prefix.length()); + } + } + + if (slaveName == null || slaveName.trim().isEmpty() || "--".equals(slaveName.trim())) { + log.warn("Bond [{}] 的从属连接 [{}] 无法识别物理网卡名", bondName, name); + continue; + } + + result.add(new BondSlaveSnapshot(uuid, name, slaveName)); + } + + return result; + } + + + private void rollbackDeletedBondSlaves(String bondName, List deletedSlaveSnapshots) { + if (deletedSlaveSnapshots == null || deletedSlaveSnapshots.isEmpty()) { + return; + } + + for (BondSlaveSnapshot snapshot : deletedSlaveSnapshots) { + if (snapshot == null || snapshot.interfaceName == null || snapshot.interfaceName.trim().isEmpty()) { + continue; + } + + try { + executeCommand( + "nmcli", + "con", + "add", + "type", + "bond-slave", + "con-name", + snapshot.name, + "ifname", + snapshot.interfaceName, + "master", + bondName + ); + } catch (Exception e) { + log.warn( + "恢复 Bond [{}] 从属连接 [{}] 失败,物理网卡 [{}],原因: {}", + bondName, + snapshot.name, + snapshot.interfaceName, + e.getMessage() + ); + } + } + + try { + executeCommand("nmcli", "con", "up", bondName); + } catch (Exception e) { + log.warn("恢复 Bond [{}] 激活失败: {}", bondName, e.getMessage()); + } + } + + private void rollbackAddSlavesToBond( + List createdSlaveConnectionNames, + List oldConnectionSnapshots + ) { + if (createdSlaveConnectionNames != null) { + for (String slaveConnectionName : createdSlaveConnectionNames) { + if (slaveConnectionName == null || slaveConnectionName.trim().isEmpty()) { + continue; + } + + try { + executeCommand("nmcli", "con", "down", slaveConnectionName); + } catch (Exception e) { + log.warn("回滚停用 Bond 从属连接 [{}] 失败: {}", slaveConnectionName, e.getMessage()); + } + + try { + executeCommand("nmcli", "con", "delete", slaveConnectionName); + } catch (Exception e) { + log.warn("回滚删除 Bond 从属连接 [{}] 失败: {}", slaveConnectionName, e.getMessage()); + } + } + } + + rollbackOldConnections(oldConnectionSnapshots); + } + + private void checkConnectionNameNotExists(String connectionName) { + try { + executeCommand("nmcli", "con", "show", connectionName); + + throw new BizException( + ErrorCode.BIZ_ERROR.getCode(), + "网络连接名称 [" + connectionName + "] 已存在,请先清理旧配置" + ); + } catch (BizException e) { + throw e; + } catch (RuntimeException e) { + + } + } + + + private void rollbackOldConnections(List oldConnectionSnapshots) { + if (oldConnectionSnapshots == null) { + return; + } + + for (OldConnectionSnapshot snapshot : oldConnectionSnapshots) { + if (snapshot == null || snapshot.uuid == null || snapshot.uuid.trim().isEmpty()) { + continue; + } + + String uuid = snapshot.uuid.trim(); + String autoconnect = snapshot.autoconnect == null || snapshot.autoconnect.trim().isEmpty() + ? "yes" + : snapshot.autoconnect.trim(); + + try { + executeCommand( + "nmcli", + "con", + "modify", + "uuid", + uuid, + "connection.autoconnect", + autoconnect + ); + } catch (Exception e) { + log.warn("恢复旧连接自启失败,uuid: {}, 原因: {}", uuid, e.getMessage()); + } + + if (snapshot.active) { + try { + executeCommand("nmcli", "con", "up", "uuid", uuid); + } catch (Exception e) { + log.warn("恢复旧连接激活失败,uuid: {}, 原因: {}", uuid, e.getMessage()); + } + } + } + } + + + private List normalizeSlaveList(List slaveList, boolean required) { + List result = new ArrayList<>(); + + if (slaveList == null || slaveList.isEmpty()) { + if (required) { + throw new IllegalArgumentException("请选择至少一个物理网卡"); + } + return result; + } + + for (String slave : slaveList) { + String slaveName = trim(slave); + + if (slaveName.isEmpty()) { + if (required) { + throw new IllegalArgumentException("物理网卡名称不能为空"); + } + continue; + } + + if (!result.contains(slaveName)) { + result.add(slaveName); + } + } + + if (required && result.isEmpty()) { + throw new IllegalArgumentException("请选择至少一个物理网卡"); + } + + return result; } public List getBondSlaves(String bondName) { @@ -800,282 +2748,6 @@ public class NetworkConfigService { } - public void setBondMode(BondModifyModeRequest req) { - String bondName = req.getBondName(); - Integer mode = req.getMode(); - - if (bondName == null || bondName.trim().isEmpty()) { - throw new IllegalArgumentException("Bond 名称不能为空"); - } - bondName = bondName.trim(); - if (mode == null) { - throw new IllegalArgumentException("新的 Bond 模式不能为空"); - } - - HashMap modeMap = new HashMap<>(); - modeMap.put(0, "balance-rr"); - modeMap.put(1, "active-backup"); - modeMap.put(2, "balance-xor"); - modeMap.put(4, "802.3ad"); - - String newMode = modeMap.get(mode); - if (newMode == null) { - throw new IllegalArgumentException("不支持当前Bond模式"); - } - - String bondOptions = String.format("mode=%s,miimon=100", newMode); - try { - executeCommand("nmcli", "con", "mod", bondName, "bond.options", bondOptions); - } catch (RuntimeException e) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "修改 Bond [" + bondName + "] 模式配置失败: " + e.getMessage()); - } - - - //重启激活服务 - try { - executeCommand("nmcli", "con", "down", bondName); - executeCommand("nmcli", "con", "up", bondName); - } catch (RuntimeException e) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "重新激活 Bond [" + bondName + "] 失败" + e.getMessage()); - } - - } - - - - public List getRoutingTable() { - List routeList = new ArrayList<>(); - List lines; - - try { - lines = executeCommand("ip", "route", "show"); - } catch (RuntimeException e) { - throw new RuntimeException("读取系统路由表失败", e); - } - - for (String line : lines) { - if (line.trim().isEmpty()) { - continue; - } - - String[] parts = line.trim().split("\\s+"); - if (parts.length == 0) { - continue; - } - - RouteInfoResponse route = new RouteInfoResponse(); - route.setDestination(parts[0]); - for (int i = 1; i < parts.length; i ++ ) { - if ("via".equals(parts[i]) && i + 1 < parts.length) { - route.setNextHop(parts[i + 1]); - } - else if ("dev".equals(parts[i]) && i + 1 < parts.length) { - route.setInterfaceName(parts[i + 1]); - } - } - - if (route.getNextHop() == null) { - route.setNextHop(""); - } - if (route.getInterfaceName() == null) { - route.setInterfaceName(""); - } - - routeList.add(route); - } - - return routeList; - } - - - public void setDefaultRoute(SetDefaultRouteRequest req) { - String deviceName = req.getDeviceName(); - String gatewayIp = req.getGatewayIp(); - - - if (deviceName == null || deviceName.trim().isEmpty()) { - throw new IllegalArgumentException("网卡名称不能为空"); - } - if (gatewayIp == null || gatewayIp.trim().isEmpty()) { - throw new IllegalArgumentException("网关 IP 不能为空"); - } - - if (!IPV4_PATTERN.matcher(req.getGatewayIp().trim()).matches()) { - throw new IllegalArgumentException("无效的网关地址格式: " + req.getGatewayIp()); - } - - String connectionName = getConnectionNameByDeviceName(req.getDeviceName().trim()); - if (connectionName == null){ - connectionName = req.getDeviceName(); - } - - - try { - executeCommand("nmcli", "con", "mod", connectionName, - "ipv4.gateway", gatewayIp, - //TODO待修改(多张网卡配置了网关,需调整 Metric 优先级)。 - "ipv4.route-metric", "50", - "ipv4.never-default", "no"); - } catch (RuntimeException e) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), String.format("为网卡 [%s] 设置网关失败: %s", deviceName, e.getMessage())); - } - - - try { - executeCommand("nmcli", "con", "down", connectionName); - executeCommand("nmcli", "con", "up", connectionName); - } catch (RuntimeException e) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), String.format("网卡 [%s] 激活配置失败: %s", deviceName, e.getMessage())); - } - - } - - - public void addStaticRoute(AddStaticRouteRequest req) { - String deviceName = req.getDeviceName(); - String targetCidr = req.getTargetCidr(); - String nextHop = req.getNextHop(); - - if (deviceName == null || deviceName.trim().isEmpty()) { - throw new IllegalArgumentException("网卡名称不能为空"); - } - if (targetCidr == null || targetCidr.trim().isEmpty()) { - throw new IllegalArgumentException("目标网段(CIDR)不能为空"); - } - - if (!CIDR_PATTERN.matcher(targetCidr.trim()).matches()) { - throw new IllegalArgumentException("非法的CIDR目标网段格式"); - } - - if (nextHop != null && !IPV4_PATTERN.matcher(nextHop.trim()).matches()) { - throw new IllegalArgumentException("非法的下一跳IP格式"); - } -// if (nextHop == null || nextHop.trim().isEmpty()) { -// throw new IllegalArgumentException("下一跳 IP 不能为空"); -// } - String connectionName = getConnectionNameByDeviceName(req.getDeviceName().trim()); - if (connectionName == null){ - connectionName = req.getDeviceName(); - } - - String routeValue = ""; - if (nextHop == null || nextHop.trim().isEmpty()) { - routeValue = String.format("%s", targetCidr.trim()); - } - else{ - routeValue = String.format("%s %s", targetCidr.trim(), nextHop.trim()); - } - - - try { - executeCommand("nmcli", "con", "mod", connectionName, "+ipv4.routes", routeValue); - } catch (RuntimeException e) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), String.format("为网卡 [%s] 添加静态路由失败: %s", deviceName, e.getMessage())); - } - - try { - executeCommand("nmcli", "con", "down", connectionName); - executeCommand("nmcli", "con", "up", connectionName); - } catch (RuntimeException e) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), String.format("网卡 [%s] 激活配置失败: %s", deviceName, e.getMessage())); - } - - } - - - public void deleteDefaultRoute(DeleteDefaultRouteRequest req) { - String deviceName = req.getDeviceName(); - - if (deviceName == null || deviceName.trim().isEmpty()) { - throw new IllegalArgumentException("网卡名称不能为空"); - } - - String connectionName = getConnectionNameByDeviceName(req.getDeviceName().trim()); - if (connectionName == null){ - connectionName = req.getDeviceName(); - } - - try { - executeCommand("nmcli", "con", "mod", connectionName, "ipv4.gateway", ""); - } catch (RuntimeException e) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), String.format("清空网卡 [%s] 的网关失败: %s", deviceName, e.getMessage())); - } - - try { - executeCommand("nmcli", "con", "down", connectionName); - executeCommand("nmcli", "con", "up", connectionName); - } catch (RuntimeException e) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), String.format("网卡 [%s] 激活配置失败: %s", deviceName, e.getMessage())); - } - - } - - - - public void deleteStaticRoute(DeleteStaticRouteRequest req) { - String deviceName = req.getDeviceName(); - String targetCidr = req.getTargetCidr(); - String nextHop = req.getNextHop(); - - if (deviceName == null || deviceName.trim().isEmpty()) { - throw new IllegalArgumentException("网卡名称不能为空"); - } - if (targetCidr == null || targetCidr.trim().isEmpty()) { - throw new IllegalArgumentException("目标网段(CIDR)不能为空"); - } -// if (nextHop == null || nextHop.trim().isEmpty()) { -// throw new IllegalArgumentException("下一跳 IP 不能为空"); -// } - - if (!CIDR_PATTERN.matcher(targetCidr.trim()).matches()) { - throw new IllegalArgumentException("非法的CIDR目标网段格式"); - } - - if (nextHop != null && !IPV4_PATTERN.matcher(nextHop.trim()).matches()) { - throw new IllegalArgumentException("非法的下一跳IP格式"); - } - - String connectionName = getConnectionNameByDeviceName(req.getDeviceName()); - if (connectionName == null){ - connectionName = req.getDeviceName(); - } - - String routeValue = ""; - if (nextHop == null || nextHop.trim().isEmpty()) { - routeValue = String.format("%s", targetCidr.trim()); - } - else{ - routeValue = String.format("%s %s", targetCidr.trim(), nextHop.trim()); - } - - - try { - executeCommand("nmcli", "con", "mod", connectionName, "-ipv4.routes", routeValue); - } catch (RuntimeException e) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), String.format("为网卡 [%s] 删除静态路由失败,请检查该路由是否存在: %s", deviceName, e.getMessage())); - } - - - try { - executeCommand("nmcli", "con", "down", connectionName); - executeCommand("nmcli", "con", "up", connectionName); - } catch (RuntimeException e) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), String.format("网卡 [%s] 激活配置失败: %s", deviceName, e.getMessage())); - } - - } - - -// private boolean isValidIpv4(String ip) { -// if (ip == null || ip.isEmpty()) return false; -// try { -// ipToInt(ip); -// return true; -// } catch (IllegalArgumentException e) { -// return false; -// } -// } - private boolean isValidIpv6(String ip) { if (ip == null || ip.isEmpty()) return false; try { @@ -1086,27 +2758,6 @@ public class NetworkConfigService { } } - private int ipToInt(String ipv4) { - String[] parts = ipv4.split("\\."); - if (parts.length != 4) { - throw new IllegalArgumentException("非法的 IPv4 地址: " + ipv4); - } - int result = 0; - for (int i = 0; i < 4; i++) { - String part = parts[i]; - int octet = Integer.parseInt(part); - if (part.length() > 1 && part.startsWith("0")) { - throw new IllegalArgumentException("非法的 IPv4 地址,存在前导零: " + part); - } - if (octet < 0 || octet > 255) { - throw new IllegalArgumentException("非法的 IPv4 地址段: " + octet); - } - result |= (octet << (24 - (8 * i))); - } - return result; - } - - private String getConnectionNameByDeviceName(String deviceName) { if (deviceName == null || deviceName.trim().isEmpty()) return null; @@ -1173,62 +2824,3 @@ public class NetworkConfigService { return value == null ? "" : value.trim(); } } - - -//private void applyNmcliConnectionConfig(String deviceName, boolean isIpv6, String ipWithPrefix, String gateway) { -// String protocol = isIpv6 ? "ipv6" : "ipv4"; -// boolean hasGateway = gateway != null && !gateway.trim().isEmpty(); -// -// boolean isNewConnection = false; -// String connectionName = getConnectionNameByDeviceName(deviceName); -// if (connectionName == null) { -// connectionName = deviceName; -// isNewConnection = true; -// } -// -// if (isNewConnection) { -// // 新建连接 -// List cmdArgs = new ArrayList<>(Arrays.asList( -// "nmcli", "con", "add", -// "type", "ethernet", -// "con-name", connectionName, -// "ifname", deviceName, -// protocol + ".method", "manual", -// protocol + ".addresses", ipWithPrefix -// )); -// -// if (hasGateway) { -// cmdArgs.add(protocol + ".gateway"); -// cmdArgs.add(gateway.trim()); -// // 设置了网关,但明确告诉系统:不要把它当默认路由! -// cmdArgs.add(protocol + ".never-default"); -// cmdArgs.add("yes"); -// } -// executeCommand(cmdArgs.toArray(new String[0])); -// -// } else { -// // 修改连接 -// List cmdArgs = new ArrayList<>(Arrays.asList( -// "nmcli", "con", "mod", connectionName, -// protocol + ".method", "manual", -// protocol + ".addresses", ipWithPrefix -// )); -// -// if (hasGateway) { -// cmdArgs.add(protocol + ".gateway"); -// cmdArgs.add(gateway.trim()); -// // 修改时,同样加上这个限制 -// cmdArgs.add(protocol + ".never-default"); -// cmdArgs.add("yes"); -// } else { -// // 如果没有网关,清空旧网关 -// cmdArgs.add(protocol + ".gateway"); -// cmdArgs.add(""); -// } -// -// executeCommand(cmdArgs.toArray(new String[0])); -// } -// -// // 激活连接 -// executeCommand("nmcli", "con", "up", connectionName); -//} diff --git a/src/main/java/com/cisd/tms/modules/device/service/impl/TimeConfigServiceImpl.java b/src/main/java/com/cisd/tms/modules/device/service/impl/TimeConfigServiceImpl.java index 5321c44..98a6181 100644 --- a/src/main/java/com/cisd/tms/modules/device/service/impl/TimeConfigServiceImpl.java +++ b/src/main/java/com/cisd/tms/modules/device/service/impl/TimeConfigServiceImpl.java @@ -4,6 +4,10 @@ import com.cisd.tms.common.enums.ErrorCode; import com.cisd.tms.common.exception.BizException; import com.cisd.tms.modules.device.dto.TimeConfigRequest; import com.cisd.tms.modules.device.service.TimeConfigService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; @@ -12,15 +16,12 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.time.LocalDateTime; +import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.time.format.ResolverStyle; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor @@ -40,6 +41,10 @@ public class TimeConfigServiceImpl implements TimeConfigService { throw new IllegalArgumentException("Invalid or empty timezone format"); } + if (!ZoneId.getAvailableZoneIds().contains(timezone)) { + throw new IllegalArgumentException( "Invalid timezone"); + } + String datetime = request.getDatetime(); if (datetime == null) { throw new IllegalArgumentException("Invalid or empty datetime format. Expected: YYYY-MM-DD HH:MM"); diff --git a/src/main/java/com/cisd/tms/modules/log/repository/impl/BackupRecordRepositoryImpl.java b/src/main/java/com/cisd/tms/modules/log/repository/impl/BackupRecordRepositoryImpl.java index 4bb3d7f..e35c6f2 100644 --- a/src/main/java/com/cisd/tms/modules/log/repository/impl/BackupRecordRepositoryImpl.java +++ b/src/main/java/com/cisd/tms/modules/log/repository/impl/BackupRecordRepositoryImpl.java @@ -2,7 +2,6 @@ package com.cisd.tms.modules.log.repository.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; -import com.baomidou.mybatisplus.core.toolkit.StringUtils; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.cisd.tms.modules.log.dto.BackupRecordPageRequest; import com.cisd.tms.modules.log.entity.BackupRecordEntity; @@ -33,8 +32,11 @@ public class BackupRecordRepositoryImpl implements BackupRecordRepository { public IPage findPage(BackupRecordPageRequest req){ IPage page = new Page<>(req.getPageNum(), req.getPageSize()); LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(StringUtils.isNotBlank(req.getBackupType()), - BackupRecordEntity::getBackupType, req.getBackupType()); + + if (req.getBackupType() != null){ + wrapper.eq(BackupRecordEntity::getBackupType, req.getBackupType()); + } + if (req.getOperationDate() != null) { LocalDateTime startOfDay = req.getOperationDate().atStartOfDay(); @@ -45,7 +47,6 @@ public class BackupRecordRepositoryImpl implements BackupRecordRepository { } IPage entityPage = backupRecordMapper.selectPage(page, wrapper); - return entityPage; } diff --git a/src/main/java/com/cisd/tms/modules/log/repository/impl/OperationAuditLogRepositoryImpl.java b/src/main/java/com/cisd/tms/modules/log/repository/impl/OperationAuditLogRepositoryImpl.java index 69f7f53..d7583bc 100644 --- a/src/main/java/com/cisd/tms/modules/log/repository/impl/OperationAuditLogRepositoryImpl.java +++ b/src/main/java/com/cisd/tms/modules/log/repository/impl/OperationAuditLogRepositoryImpl.java @@ -41,8 +41,7 @@ public class OperationAuditLogRepositoryImpl implements OperationAuditLogReposit .eq(req.getOperationResult() != null, OperationAuditLogEntity::getOperationResult, req.getOperationResult()) .eq(req.getAuditStatus() != null, OperationAuditLogEntity::getAuditStatus, req.getAuditStatus()) .eq(req.getAuditResult() != null, OperationAuditLogEntity::getAuditResult, req.getAuditResult()) - .eq(StringUtils.isNotBlank(req.getRemoteIp()), OperationAuditLogEntity::getRemoteIp, req.getRemoteIp()) - .eq(StringUtils.isNotBlank(req.getKeyword()), OperationAuditLogEntity::getSummary, req.getKeyword()); + .eq(StringUtils.isNotBlank(req.getRemoteIp()), OperationAuditLogEntity::getRemoteIp, req.getRemoteIp()); if (req.getDateFrom() != null) { wrapper.ge(OperationAuditLogEntity::getOccurredAt, req.getDateFrom()); diff --git a/src/main/java/com/cisd/tms/modules/log/service/AuditBackupService.java b/src/main/java/com/cisd/tms/modules/log/service/AuditBackupService.java index 23b7763..1c45b7d 100644 --- a/src/main/java/com/cisd/tms/modules/log/service/AuditBackupService.java +++ b/src/main/java/com/cisd/tms/modules/log/service/AuditBackupService.java @@ -84,7 +84,7 @@ public class AuditBackupService { private void executeBackup() { log.info("开始执行审计日志定期备份任务"); String timeStr = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss")); - String zipFileName = "audit_backup_" + timeStr + ".zip"; + String zipFileName = "audit_logs_backup_" + timeStr + ".zip"; String zipFilePath = ZIPFILEPATH + zipFileName; File zipFile = new File(zipFilePath); @@ -220,7 +220,7 @@ public class AuditBackupService { log.info("开始执行审计日志手动备份任务"); String timeStr = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss")); - String zipFileName = "audit_backup_" + timeStr + ".zip"; + String zipFileName = "audit_logs_backup_" + timeStr + ".zip"; String zipFilePath = ZIPFILEPATH + zipFileName; File zipFile = new File(zipFilePath); diff --git a/src/test/java/com/cisd/tms/modules/log/aspect/OperationAuditAspectTest.java b/src/test/java/com/cisd/tms/modules/log/aspect/OperationAuditAspectTest.java index c117696..15d9fd5 100644 --- a/src/test/java/com/cisd/tms/modules/log/aspect/OperationAuditAspectTest.java +++ b/src/test/java/com/cisd/tms/modules/log/aspect/OperationAuditAspectTest.java @@ -3,15 +3,10 @@ package com.cisd.tms.modules.log.aspect; import com.cisd.tms.common.config.WebMvcConfig; import com.cisd.tms.modules.device.controller.NetworkConfigController; import com.cisd.tms.modules.device.service.NetworkConfigService; -import com.cisd.tms.modules.log.dto.OperationAuditCommand; import com.cisd.tms.modules.log.service.OperationAuditService; import com.cisd.tms.security.internal.InternalApiAuthInterceptor; import com.cisd.tms.security.internal.InternalApiReplayInterceptor; import com.cisd.tms.security.openapi.OpenApiSignAuthInterceptor; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -22,12 +17,6 @@ import org.springframework.context.annotation.Import; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; -import java.util.Collections; - -import static org.mockito.ArgumentMatchers.eq; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - @WebMvcTest(controllers = NetworkConfigController.class, excludeAutoConfiguration = {SecurityAutoConfiguration.class}, excludeFilters = @ComponentScan.Filter( @@ -55,44 +44,5 @@ class OperationAuditAspectTest { @MockitoBean private OpenApiSignAuthInterceptor openApiSignAuthInterceptor; - @Test - void testAuditLog_OnSuccess() throws Exception { - Mockito.when(networkConfigService.getNetworkInfo()) - .thenReturn(Collections.emptyList()); - mockMvc.perform(get("/api/v1/device/network-config/network-info") - .requestAttr(OperationAuditAspect.ATTR_ROLE_CODE, "SYS_ADMIN") - .requestAttr(OperationAuditAspect.ATTR_AUTH_LEVEL, "FULL")) - .andExpect(status().isOk()); - - ArgumentCaptor cmdCaptor = ArgumentCaptor.forClass(OperationAuditCommand.class); - - Mockito.verify(auditService, Mockito.times(1)) - .record(cmdCaptor.capture(), eq(true)); - - OperationAuditCommand actualCmd = cmdCaptor.getValue(); - Assertions.assertEquals("NETWORK", actualCmd.getModuleCode(), "模块名应从注解中获取"); - Assertions.assertEquals("get", actualCmd.getActionType(), "动作类型应从注解中获取"); - Assertions.assertEquals("获取网络信息", actualCmd.getSummary(), "摘要应从注解中获取"); - Assertions.assertEquals("SUCCESS", actualCmd.getOperationResult(), "方法执行成功,结果应为 SUCCESS"); - } - - @Test - void testAuditLog_OnException() throws Exception { - Mockito.when(networkConfigService.getNetworkInfo()) - .thenThrow(new RuntimeException("获取网卡信息超时报错啦!")); - try { - mockMvc.perform(get("/api/v1/device/network-config/network-info")); - } catch (Exception e) { - } - - ArgumentCaptor cmdCaptor = ArgumentCaptor.forClass(OperationAuditCommand.class); - Mockito.verify(auditService, Mockito.times(1)) - .record(cmdCaptor.capture(), eq(true)); - - OperationAuditCommand actualCmd = cmdCaptor.getValue(); - Assertions.assertEquals("NETWORK", actualCmd.getModuleCode()); - Assertions.assertEquals("FAILED", actualCmd.getOperationResult(), "抛出异常,结果应为 FAILED"); - Assertions.assertEquals("获取网卡信息超时报错啦!", actualCmd.getErrorMessage(), "应该截获到异常信息"); - } } \ No newline at end of file diff --git a/src/test/java/com/cisd/tms/security/internal/ReplayProtectedEndpointsTest.java b/src/test/java/com/cisd/tms/security/internal/ReplayProtectedEndpointsTest.java index c1389bd..69f4156 100644 --- a/src/test/java/com/cisd/tms/security/internal/ReplayProtectedEndpointsTest.java +++ b/src/test/java/com/cisd/tms/security/internal/ReplayProtectedEndpointsTest.java @@ -2,19 +2,16 @@ package com.cisd.tms.security.internal; import com.cisd.tms.modules.auth.controller.AuthAdminController; import com.cisd.tms.modules.auth.controller.AuthController; -import com.cisd.tms.modules.device.controller.CryptoCardController; -import com.cisd.tms.modules.device.controller.DeviceController; -import com.cisd.tms.modules.device.controller.IpWhitelistController; -import com.cisd.tms.modules.device.controller.NetworkConfigController; -import com.cisd.tms.modules.device.controller.TimeConfigController; +import com.cisd.tms.modules.device.controller.*; import com.cisd.tms.modules.init.controller.InitController; import com.cisd.tms.modules.mk.controller.LmkController; import com.cisd.tms.modules.upgrade.controller.UpgradeController; -import java.lang.reflect.Method; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.core.annotation.AnnotatedElementUtils; +import java.lang.reflect.Method; + class ReplayProtectedEndpointsTest { @Test @@ -56,36 +53,6 @@ class ReplayProtectedEndpointsTest { Assertions.assertNull(AnnotatedElementUtils.findMergedAnnotation(IpWhitelistController.class, ReplayProtected.class)); Assertions.assertNull(AnnotatedElementUtils.findMergedAnnotation(LmkController.class, ReplayProtected.class)); - Assertions.assertNotNull(effectiveAnnotation(NetworkConfigController.class, "setIpv4Config", - com.cisd.tms.modules.device.dto.network.Ipv4ConfigRequest.class)); - Assertions.assertNotNull(effectiveAnnotation(NetworkConfigController.class, "setIpv6Config", - com.cisd.tms.modules.device.dto.network.Ipv6ConfigRequest.class)); - Assertions.assertNotNull(effectiveAnnotation(NetworkConfigController.class, "createBond", - com.cisd.tms.modules.device.dto.network.BondCreateRequest.class)); - Assertions.assertNotNull(effectiveAnnotation(NetworkConfigController.class, "deleteBond", String.class)); - Assertions.assertNotNull(effectiveAnnotation(NetworkConfigController.class, "addSlavesTOBond", - com.cisd.tms.modules.device.dto.network.BondAddSlavesRequest.class)); - Assertions.assertNotNull(effectiveAnnotation(NetworkConfigController.class, "removeSlaveFromBond", - com.cisd.tms.modules.device.dto.network.BondRemoveSlaveRequest.class)); - Assertions.assertNotNull(effectiveAnnotation(NetworkConfigController.class, "setBondMode", - com.cisd.tms.modules.device.dto.network.BondModifyModeRequest.class)); - Assertions.assertNotNull(effectiveAnnotation(NetworkConfigController.class, "setDefaultRoute", - com.cisd.tms.modules.device.dto.network.SetDefaultRouteRequest.class)); - Assertions.assertNotNull(effectiveAnnotation(NetworkConfigController.class, "addStaticRoute", - com.cisd.tms.modules.device.dto.network.AddStaticRouteRequest.class)); - Assertions.assertNotNull(effectiveAnnotation(NetworkConfigController.class, "deleteDefaultRoute", - com.cisd.tms.modules.device.dto.network.DeleteDefaultRouteRequest.class)); - Assertions.assertNotNull(effectiveAnnotation(NetworkConfigController.class, "deleteStaticRoute", - com.cisd.tms.modules.device.dto.network.DeleteStaticRouteRequest.class)); - - Assertions.assertNull(effectiveAnnotation(NetworkConfigController.class, "getNetworkInfo")); - Assertions.assertNull(effectiveAnnotation(NetworkConfigController.class, "getIpv4Info", String.class)); - Assertions.assertNull(effectiveAnnotation(NetworkConfigController.class, "getIpv6Info", String.class)); - Assertions.assertNull(effectiveAnnotation(NetworkConfigController.class, "getAllBondName")); - Assertions.assertNull(effectiveAnnotation(NetworkConfigController.class, "getBondSlaves", String.class)); - Assertions.assertNull(effectiveAnnotation(NetworkConfigController.class, "getBondMode", String.class)); - Assertions.assertNull(effectiveAnnotation(NetworkConfigController.class, "getRoutingTable")); - Assertions.assertNotNull(effectiveAnnotation(IpWhitelistController.class, "addWhitelist", com.cisd.tms.modules.device.dto.network.IpWhitelistRequest.class)); Assertions.assertNotNull(effectiveAnnotation(IpWhitelistController.class, "updateWhitelist",