feat(k8s): M14 Namespace 管理看板(资源配额 + 命名空间 CRUD)
- 后端: ListNamespacesDetailed/CreateNamespace/DeleteNamespace/ListResourceQuotas/ListLimitRanges - 后端: GetNamespacesDetail handler (6 API calls aggregate, not N×3) - 后端: CreateNamespaceHandler + DeleteNamespaceHandler (system NS protection) - 前端: NamespaceManagementPage.tsx (~280行) — 表格+概览卡片+创建/删除弹框 - 路由: GET/POST/DELETE /:clusterCode/namespaces + GET /namespace/:ns/detail - 验证: 9/9 通过 (urllib), TS 零错误, Vite 构建成功
This commit is contained in:
1 parent
c7b61968bc
commit
8240a88a84
33 files changed
+2029
-71
No files matched your search
@@ -40,6 +40,7 @@
|
|||||||
| **Node 详情页** | ✅ GetNodeDetail (M10) | ✅ NodeDetailPage | ✅ curl 验证 |
|
| **Node 详情页** | ✅ GetNodeDetail (M10) | ✅ NodeDetailPage | ✅ curl 验证 |
|
||||||
| **StatefulSet/DaemonSet 详情页** | ✅ GetStatefulSetDetail/GetDaemonSetDetail + YAML路由修复 (M11) | ✅ WorkloadViewPage 重写 (M11) | ✅ curl 验证 |
|
| **StatefulSet/DaemonSet 详情页** | ✅ GetStatefulSetDetail/GetDaemonSetDetail + YAML路由修复 (M11) | ✅ WorkloadViewPage 重写 (M11) | ✅ curl 验证 |
|
||||||
| **多集群资源对比看板** | ✅ GetMultiClusterComparison (M13) | ✅ ClusterComparePage (M13) | ✅ urllib 验证 |
|
| **多集群资源对比看板** | ✅ GetMultiClusterComparison (M13) | ✅ ClusterComparePage (M13) | ✅ urllib 验证 |
|
||||||
|
| **Namespace 管理看板** | ✅ GetNamespacesDetail + CRUD (M14) | ✅ NamespaceManagementPage (M14) | ✅ urllib 验证 |
|
||||||
|
|
||||||
### ❌ 缺失 / 进行中
|
### ❌ 缺失 / 进行中
|
||||||
|
|
||||||
|
|||||||
+45
@@ -329,3 +329,48 @@ pending → in_progress → done / blocked
|
|||||||
1. 更新本文件状态
|
1. 更新本文件状态
|
||||||
2. 更新 PROJECT_EVALUATION.md 对应行
|
2. 更新 PROJECT_EVALUATION.md 对应行
|
||||||
3. git commit message 格式: `feat(k8s): M1 SyncCluster metrics 采集`
|
3. git commit message 格式: `feat(k8s): M1 SyncCluster metrics 采集`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### M14: Namespace 管理看板(资源配额 + 命名空间 CRUD)[done]
|
||||||
|
**目标**: 从仅返回命名空间名列表升级为完整的命名空间管理看板——含资源统计、配额状态、创建/删除操作
|
||||||
|
|
||||||
|
**后端改动**:
|
||||||
|
- `pkg/kubernetesclient/client.go`:
|
||||||
|
- `ListNamespacesDetailed()` — 返回完整 metadata(status/age/labels/annotations)
|
||||||
|
- `GetNamespace(name)` — 单个命名空间详情
|
||||||
|
- `CreateNamespace(name, labels)` — 创建命名空间
|
||||||
|
- `DeleteNamespace(name)` — 删除命名空间
|
||||||
|
- `ListResourceQuotas(namespace)` — 支持空 namespace 全集群模式
|
||||||
|
- `ListLimitRanges(namespace)` — 支持空 namespace 全集群模式
|
||||||
|
- `handlers/kubernetes.go`:
|
||||||
|
- `GetNamespacesDetail` — 全量命名空间 + 资源计数 + 配额状态(6 次 API 调用聚合,非 N×3)
|
||||||
|
- `GetNamespaceDetailHandler` — 单命名空间详情
|
||||||
|
- `CreateNamespaceHandler` — 创建(系统命名空间保护)
|
||||||
|
- `DeleteNamespaceHandler` — 删除(kube-system/default/devops 保护)
|
||||||
|
- `routes.go`:
|
||||||
|
- `GET /:clusterCode/namespaces/detail` — 全量命名空间详情
|
||||||
|
- `GET /:clusterCode/namespace/:namespace/detail` — 单命名空间详情
|
||||||
|
- `POST /:clusterCode/namespaces` — 创建命名空间
|
||||||
|
- `DELETE /:clusterCode/namespaces/:namespace` — 删除命名空间
|
||||||
|
|
||||||
|
**前端改动**:
|
||||||
|
- 新增 `NamespaceManagementPage.tsx` (~280行):
|
||||||
|
- 概览卡片:命名空间总数 / Deployments / Pods / Services / 配额限制数
|
||||||
|
- 命名空间表格:名称(可点击跳转)/ 状态 Badge / 年龄 / Dep/Pod/Svc 计数 / 配额状态 / 标签 / 操作(删除)
|
||||||
|
- 创建命名空间弹框(名称 + DNS-1123 提示)
|
||||||
|
- 删除确认弹框(红色警告 + 二次确认)
|
||||||
|
- 系统命名空间保护(kube-system/default/devops 不可删除)
|
||||||
|
- 搜索过滤
|
||||||
|
- `api.ts`: 新增 4 个方法(getNamespacesDetail/getNamespaceDetail/createNamespace/deleteNamespace)
|
||||||
|
- `App.tsx`: 新增路由 `kubernetes/:clusterCode/namespaces`
|
||||||
|
- `ClusterOverviewPage.tsx`: 命名空间区域新增「管理全部」链接
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- [x] `GET /:clusterCode/namespaces/detail` 返回 9 个命名空间(含 status/age/labels/resource_counts)
|
||||||
|
- [x] 每个命名空间包含 7 个字段(name/status/age/labels/resource_counts/has_resource_quota/has_limit_range)
|
||||||
|
- [x] 创建命名空间 API 返回 200 + 创建后列表中出现
|
||||||
|
- [x] 删除命名空间 API 返回 200 + 删除后列表中消失
|
||||||
|
- [x] 系统命名空间删除被拦截(403)
|
||||||
|
- [x] SPA 路由 `/kubernetes/:code/namespaces` 返回 200
|
||||||
|
- [x] TypeScript 零编译错误 + Vite 构建成功
|
||||||
+70
@@ -478,3 +478,73 @@ cd frontend && npx tsc --noEmit && npm run build 2>&1 | tail -3
|
|||||||
- [x] 前端 4 类图表正确渲染(柱状图×2 + 雷达图 + 饼图)
|
- [x] 前端 4 类图表正确渲染(柱状图×2 + 雷达图 + 饼图)
|
||||||
- [x] 详细对比表格可点击跳转集群概览
|
- [x] 详细对比表格可点击跳转集群概览
|
||||||
- [x] totals 聚合正确(cluster_count/node_count/pod_count/deployment_count/service_count)
|
- [x] totals 聚合正确(cluster_count/node_count/pod_count/deployment_count/service_count)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## M14: Namespace 管理看板(资源配额 + 命名空间 CRUD)
|
||||||
|
|
||||||
|
### 验证用例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# U1: 登录获取 Token
|
||||||
|
TOKEN=$(python3 -c "
|
||||||
|
import urllib.request, json
|
||||||
|
login_data = json.dumps({'username': 'admin', 'password': 'admin123'}).encode()
|
||||||
|
req = urllib.request.Request('http://127.0.0.1:8080/api/v1/auth/login', data=login_data, method='POST')
|
||||||
|
req.add_header('Content-Type', 'application/json')
|
||||||
|
resp = urllib.request.urlopen(req)
|
||||||
|
print(json.loads(resp.read())['data']['token'])
|
||||||
|
")
|
||||||
|
|
||||||
|
# U2: 命名空间详情 API(含资源计数+配额状态)
|
||||||
|
curl -s "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/namespaces/detail" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" | jq '.data | length'
|
||||||
|
# 期望: 返回所有命名空间(含 status/age/labels/resource_counts)
|
||||||
|
|
||||||
|
# U3: 字段完整性
|
||||||
|
curl -s "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/namespaces/detail" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" | jq '.data[0] | keys'
|
||||||
|
# 期望: 含 name/status/age/labels/resource_counts/has_resource_quota/has_limit_range
|
||||||
|
|
||||||
|
# U4: 创建命名空间
|
||||||
|
curl -s -X POST "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/namespaces" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
||||||
|
-d '{"name":"test-ns-m14","labels":{"env":"test"}}'
|
||||||
|
# 期望: code=200, message="命名空间创建成功"
|
||||||
|
|
||||||
|
# U5: 创建后验证存在于列表
|
||||||
|
curl -s "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/namespaces/detail" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" | jq '[.data[] | select(.name=="test-ns-m14")] | length'
|
||||||
|
# 期望: 1
|
||||||
|
|
||||||
|
# U6: 删除命名空间
|
||||||
|
curl -s -X DELETE "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/namespaces/test-ns-m14" \
|
||||||
|
-H "Authorization: Bearer $TOKEN"
|
||||||
|
# 期望: code=200, message="命名空间已删除"
|
||||||
|
|
||||||
|
# U7: 系统命名空间删除保护
|
||||||
|
curl -s -X DELETE "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/namespaces/kube-system" \
|
||||||
|
-H "Authorization: Bearer $TOKEN"
|
||||||
|
# 期望: code=403, message="禁止删除系统命名空间: kube-system"
|
||||||
|
|
||||||
|
# U8: SPA 路由
|
||||||
|
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1/kubernetes/prod-cluster/namespaces
|
||||||
|
# 期望: 200
|
||||||
|
|
||||||
|
# U9: TypeScript + Vite 构建
|
||||||
|
cd frontend && npx tsc --noEmit && npm run build 2>&1 | tail -3
|
||||||
|
# 期望: 零错误 + built in Ns
|
||||||
|
```
|
||||||
|
|
||||||
|
### 通过标准
|
||||||
|
- [x] U2: Namespaces Detail API 返回完整命名空间列表(含 status/age/resource_counts)
|
||||||
|
- [x] U3: 每个命名空间包含 7 个字段(name/status/age/labels/resource_counts/has_resource_quota/has_limit_range)
|
||||||
|
- [x] U4: 创建命名空间 API 返回 200
|
||||||
|
- [x] U5: 创建后命名空间出现在列表中
|
||||||
|
- [x] U6: 删除命名空间 API 返回 200
|
||||||
|
- [x] U7: 系统命名空间删除被拦截(403)
|
||||||
|
- [x] U8: SPA 路由返回 200
|
||||||
|
- [x] U9: TypeScript 零编译错误 + Vite 构建成功
|
||||||
|
- [x] 前端命名空间管理页表格正确渲染(名称/状态/年龄/资源计数/配额/操作)
|
||||||
|
- [x] 创建/删除弹框功能正常
|
||||||
|
- [x] 集群概览页「管理全部」链接跳转正常
|
||||||
+151
@@ -0,0 +1,151 @@
|
|||||||
|
# AIOps Agent
|
||||||
|
|
||||||
|
轻量主机采集代理,定时采集 CPU/内存/磁盘/网络/端口/进程 等指标,通过 HTTP 上报至 AIOps 后端。
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
agent/
|
||||||
|
├── main.go # 入口:CLI 解析、采集编排、上报循环
|
||||||
|
├── Makefile # 本地构建(单架构)
|
||||||
|
│
|
||||||
|
├── collector/ # 采集器 —— 从 /proc /sys netstat 读原始数据
|
||||||
|
│ ├── cpu.go # CPU 使用率、核数、负载
|
||||||
|
│ ├── mem.go # 内存 total/used/cached
|
||||||
|
│ ├── swap.go # Swap 分区
|
||||||
|
│ ├── disk.go # 磁盘(按挂载点)
|
||||||
|
│ ├── diskio.go # 磁盘 IO(读写速率/IOPS)
|
||||||
|
│ ├── network.go # TCP 连接状态、网卡流量
|
||||||
|
│ ├── port.go # 监听端口发现
|
||||||
|
│ ├── proctop.go # 进程 TOP N(按 CPU 排序)
|
||||||
|
│ ├── fd.go # 系统文件句柄使用量
|
||||||
|
│ ├── context.go # 上下文切换/中断次数
|
||||||
|
│ ├── health_probe.go # 健康探针(HTTP GET / TCP / CMD)
|
||||||
|
│ ├── system.go # 系统信息(OS/Kernel/Uptime)
|
||||||
|
│ └── util.go # 通用工具(读文件、解析等)
|
||||||
|
│
|
||||||
|
├── model/ # 数据模型
|
||||||
|
│ ├── model.go # Report 结构体(一次采集的完整报文)
|
||||||
|
│ ├── model_ext.go # NetworkStat / DiskIOStat / SwapStat 等
|
||||||
|
│ └── model_proctop.go # ProcessStat(含 Cmdline/User/CPU%/Mem%/FD)
|
||||||
|
│
|
||||||
|
├── client/ # HTTP 客户端(连接池复用、Keep-Alive)
|
||||||
|
│ └── http.go
|
||||||
|
├── reporter/ # 上报器 —— 将 Report JSON 序列化后 POST 到后端
|
||||||
|
│ └── http.go
|
||||||
|
├── task/ # 任务系统(后端下发命令,Agent 执行并回传结果)
|
||||||
|
│ ├── client.go
|
||||||
|
│ └── executor.go
|
||||||
|
│
|
||||||
|
├── deploy/ # 部署文件
|
||||||
|
│ ├── install.sh # 一键安装脚本(自动检测架构)
|
||||||
|
│ └── aiops-agent.service # systemd 服务模板
|
||||||
|
│
|
||||||
|
├── bin/ # 编译产物(Docker 多架构构建后拷贝)
|
||||||
|
├── go.mod / go.sum # Go 依赖
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐ 采集 ┌──────────────────────────────┐
|
||||||
|
│ /proc/cpu │──────────▶│ collector/ │
|
||||||
|
│ /proc/mem │ │ cpu / mem / disk / net / ... │
|
||||||
|
│ netstat │ └──────────┬───────────────────┘
|
||||||
|
│ /sys/... │ │ Go struct
|
||||||
|
└─────────────┘ ▼
|
||||||
|
┌────────────────┐
|
||||||
|
│ model.Report │ ← 一次采集的完整数据
|
||||||
|
└───────┬────────┘
|
||||||
|
│ JSON 序列化
|
||||||
|
▼
|
||||||
|
┌────────────────┐
|
||||||
|
│ reporter/ │ ← HTTP POST /api/v1/agent/report
|
||||||
|
└───────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ AIOps Backend │
|
||||||
|
│ (10.10.10.75:8080) │
|
||||||
|
└─────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- **采集层**:从 `/proc`、`/sys`、`netstat` 等系统接口读取原始数据
|
||||||
|
- **模型层**:统一的数据结构 `Report`,涵盖系统指标、进程 TOP、端口、FD、探针等
|
||||||
|
- **上报层**:共享 HTTP 连接池,JSON 序列化后 POST 到后端
|
||||||
|
- **任务层**:独立协程轮询后端任务(脚本执行、探针配置拉取)
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 输出全部上报指标(Zabbix-agent -p 风格),调试用
|
||||||
|
./aiops-agent output
|
||||||
|
|
||||||
|
# 按 CPU 占用输出 TOP 5 进程
|
||||||
|
./aiops-agent output -top-n 5
|
||||||
|
|
||||||
|
# 正常采集上报(daemon)
|
||||||
|
./aiops-agent -server http://10.10.10.75:8080 -token <TOKEN> -interval 30
|
||||||
|
```
|
||||||
|
|
||||||
|
## 参数
|
||||||
|
|
||||||
|
| 参数 | 默认 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `-server` | (空) | 后端地址,daemon 模式必填 |
|
||||||
|
| `-token` | (空) | Bearer 鉴权令牌 |
|
||||||
|
| `-hostname` | 系统主机名 | 上报标识 |
|
||||||
|
| `-interval` | `10` | 采集间隔(秒),建议 30~60 |
|
||||||
|
| `-top-n` | `3` | 进程 TOP N,按 CPU 占用排序 |
|
||||||
|
|
||||||
|
## output 输出
|
||||||
|
|
||||||
|
格式:`key [type|value]`,type=`s`(string) `u`(uint) `d`(double)。
|
||||||
|
|
||||||
|
```text
|
||||||
|
agent.hostname [s|opsx-server]
|
||||||
|
system.hostname [s|opsx-server]
|
||||||
|
agent.version [s|v2.1.0]
|
||||||
|
agent.ip [s|10.10.10.75]
|
||||||
|
system.os [s|Ubuntu 24.04 LTS]
|
||||||
|
system.kernel [s|6.8.0-90-generic]
|
||||||
|
system.uptime [u|603932]
|
||||||
|
cpu.usage_percent [d|0.00]
|
||||||
|
cpu.core_count [u|8]
|
||||||
|
cpu.load1 / load5 / load15 [d|...]
|
||||||
|
memory.total_gb / used_gb / usage_percent [d|...]
|
||||||
|
swap.total_gb / used_gb / usage_percent [d|...]
|
||||||
|
disk[/].total_gb / used_gb / usage_percent [d|...] (每挂载点一行)
|
||||||
|
ports.discovery[*] [s|{JSON}] (Zabbix LLD 格式)
|
||||||
|
net.listen[PORT].process / .protocol [s|...]
|
||||||
|
net.tcp.established / time_wait / close_wait [u|...]
|
||||||
|
net.rx_bps / tx_bps [d|...]
|
||||||
|
disk_io.read_bps / write_bps / read_iops / write_iops
|
||||||
|
proc.top[N].name / pid / user / cpu_pct / mem_pct / fd_count / threads
|
||||||
|
fd.allocated / max / usage_pct [u|...]
|
||||||
|
ctx.ctx_per_sec / irq_per_sec [d|...]
|
||||||
|
agent.heartbeat.version / uptime_sec / self_rss_mb / report_seq
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# x86_64
|
||||||
|
curl -o /usr/local/bin/aiops-agent http://10.10.10.75:8080/agent/aiops-agent-linux-amd64
|
||||||
|
|
||||||
|
# ARM64
|
||||||
|
curl -o /usr/local/bin/aiops-agent http://10.10.10.75:8080/agent/aiops-agent-linux-arm64
|
||||||
|
|
||||||
|
# 一键安装(自动检测架构并注册 systemd)
|
||||||
|
curl -fsSL http://10.10.10.75:8080/agent/install.sh | sudo bash -s -- --server http://10.10.10.75:8080 --token <TOKEN>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 本地构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd agent
|
||||||
|
make # CGO_ENABLED=0 静态编译当前平台
|
||||||
|
```
|
||||||
|
|
||||||
|
Docker 多架构构建由顶层 `backend/Dockerfile` 的 `agent-builder` stage 完成(linux-amd64 / linux-arm64 / darwin-arm64)。
|
||||||
+13
-2
@@ -18,8 +18,9 @@ import (
|
|||||||
|
|
||||||
// cpuSample 保存某一时刻 CPU 的累计 jiffies,用于两次采样计算差值
|
// cpuSample 保存某一时刻 CPU 的累计 jiffies,用于两次采样计算差值
|
||||||
type cpuSample struct {
|
type cpuSample struct {
|
||||||
total uint64 // 自系统启动以来 CPU 所有状态的累计 jiffies
|
total uint64 // 自系统启动以来 CPU 所有状态的累计 jiffies
|
||||||
idle uint64 // idle + iowait 的累计 jiffies
|
idle uint64 // idle + iowait 的累计 jiffies
|
||||||
|
iowait uint64 // iowait 单独累计 jiffies(用于计算 iowait 占比)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CPUCollector CPU 指标采集器。
|
// CPUCollector CPU 指标采集器。
|
||||||
@@ -50,12 +51,18 @@ func (c *CPUCollector) Collect() (model.CPUStat, error) {
|
|||||||
if c.prev != nil {
|
if c.prev != nil {
|
||||||
totalDelta := cur.total - c.prev.total
|
totalDelta := cur.total - c.prev.total
|
||||||
idleDelta := cur.idle - c.prev.idle
|
idleDelta := cur.idle - c.prev.idle
|
||||||
|
iowaitDelta := cur.iowait - c.prev.iowait
|
||||||
if totalDelta > 0 {
|
if totalDelta > 0 {
|
||||||
// 使用率 = (总增量 - 空闲增量) / 总增量 * 100
|
// 使用率 = (总增量 - 空闲增量) / 总增量 * 100
|
||||||
stat.UsagePercent = float64(totalDelta-idleDelta) / float64(totalDelta) * 100.0
|
stat.UsagePercent = float64(totalDelta-idleDelta) / float64(totalDelta) * 100.0
|
||||||
if stat.UsagePercent < 0 {
|
if stat.UsagePercent < 0 {
|
||||||
stat.UsagePercent = 0
|
stat.UsagePercent = 0
|
||||||
}
|
}
|
||||||
|
// iowait = iowait增量 / 总增量 * 100
|
||||||
|
stat.IOWait = float64(iowaitDelta) / float64(totalDelta) * 100.0
|
||||||
|
if stat.IOWait < 0 {
|
||||||
|
stat.IOWait = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.prev = &cur
|
c.prev = &cur
|
||||||
@@ -100,6 +107,10 @@ func readCPUSample() (cpuSample, error) {
|
|||||||
if i == 3 || i == 4 {
|
if i == 3 || i == 4 {
|
||||||
sample.idle += x
|
sample.idle += x
|
||||||
}
|
}
|
||||||
|
// iowait(索引4) 单独记录
|
||||||
|
if i == 4 {
|
||||||
|
sample.iowait += x
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return sample, nil
|
return sample, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-5
@@ -76,12 +76,22 @@ func (d *DiskCollector) Collect() ([]model.DiskStat, error) {
|
|||||||
}
|
}
|
||||||
used := total - free
|
used := total - free
|
||||||
|
|
||||||
|
// inode 使用率
|
||||||
|
var inodePct float64
|
||||||
|
if fs.Files > 0 {
|
||||||
|
inodePct = float64(fs.Files-fs.Ffree) / float64(fs.Files) * 100.0
|
||||||
|
if inodePct < 0 {
|
||||||
|
inodePct = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
disks = append(disks, model.DiskStat{
|
disks = append(disks, model.DiskStat{
|
||||||
Mount: m.mountpoint,
|
Mount: m.mountpoint,
|
||||||
Device: m.device,
|
Device: m.device,
|
||||||
TotalGB: bytesToGB(total),
|
TotalGB: bytesToGB(total),
|
||||||
UsedGB: bytesToGB(used),
|
UsedGB: bytesToGB(used),
|
||||||
UsagePercent: float64(used) / float64(total) * 100.0,
|
UsagePercent: float64(used) / float64(total) * 100.0,
|
||||||
|
InodeUsagePct: inodePct,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return disks, nil
|
return disks, nil
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Package collector — 磁盘 IO 指标采集器
|
// Package collector — 磁盘 IO 指标采集器
|
||||||
// 通过读取 /proc/diskstats 获取磁盘读写 BPS 和 IOPS。
|
// 通过读取 /proc/diskstats 获取磁盘读写 BPS、IOPS 和 util(磁盘繁忙程度)。
|
||||||
//
|
//
|
||||||
// 署名 Hermes
|
// 署名 Hermes
|
||||||
package collector
|
package collector
|
||||||
@@ -23,6 +23,7 @@ type diskIOSample struct {
|
|||||||
writeSects uint64
|
writeSects uint64
|
||||||
readOps uint64
|
readOps uint64
|
||||||
writeOps uint64
|
writeOps uint64
|
||||||
|
ioTicks uint64 // io_ticks (field 12) — 磁盘忙碌的毫秒累计
|
||||||
}
|
}
|
||||||
|
|
||||||
// DiskIOCollector 磁盘 IO 采集器
|
// DiskIOCollector 磁盘 IO 采集器
|
||||||
@@ -40,7 +41,7 @@ func NewDiskIOCollector() *DiskIOCollector {
|
|||||||
func (c *DiskIOCollector) Collect() (model.DiskIOStat, error) {
|
func (c *DiskIOCollector) Collect() (model.DiskIOStat, error) {
|
||||||
stat := model.DiskIOStat{}
|
stat := model.DiskIOStat{}
|
||||||
|
|
||||||
rSects, wSects, rOps, wOps, err := readDiskStats()
|
rSects, wSects, rOps, wOps, ioTicks, err := readDiskStats()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return stat, fmt.Errorf("读取磁盘 IO 失败: %w", err)
|
return stat, fmt.Errorf("读取磁盘 IO 失败: %w", err)
|
||||||
}
|
}
|
||||||
@@ -55,6 +56,7 @@ func (c *DiskIOCollector) Collect() (model.DiskIOStat, error) {
|
|||||||
writeSects: wSects,
|
writeSects: wSects,
|
||||||
readOps: rOps,
|
readOps: rOps,
|
||||||
writeOps: wOps,
|
writeOps: wOps,
|
||||||
|
ioTicks: ioTicks,
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.prev != nil && now.After(c.prev.time) {
|
if c.prev != nil && now.After(c.prev.time) {
|
||||||
@@ -65,6 +67,15 @@ func (c *DiskIOCollector) Collect() (model.DiskIOStat, error) {
|
|||||||
stat.WriteBPS = float64((cur.writeSects-c.prev.writeSects)*sectorSize) / elapsed
|
stat.WriteBPS = float64((cur.writeSects-c.prev.writeSects)*sectorSize) / elapsed
|
||||||
stat.ReadIOPS = float64(cur.readOps-c.prev.readOps) / elapsed
|
stat.ReadIOPS = float64(cur.readOps-c.prev.readOps) / elapsed
|
||||||
stat.WriteIOPS = float64(cur.writeOps-c.prev.writeOps) / elapsed
|
stat.WriteIOPS = float64(cur.writeOps-c.prev.writeOps) / elapsed
|
||||||
|
// util% = io_ticks_delta_ms / (elapsed_ms) * 100
|
||||||
|
ioDelta := cur.ioTicks - c.prev.ioTicks
|
||||||
|
stat.Util = float64(ioDelta) / (elapsed * 1000.0) * 100.0
|
||||||
|
if stat.Util > 100 {
|
||||||
|
stat.Util = 100
|
||||||
|
}
|
||||||
|
if stat.Util < 0 {
|
||||||
|
stat.Util = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.prev = cur
|
c.prev = cur
|
||||||
@@ -73,11 +84,11 @@ func (c *DiskIOCollector) Collect() (model.DiskIOStat, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// readDiskStats 读取 /proc/diskstats,汇总所有物理磁盘的读写数据
|
// readDiskStats 读取 /proc/diskstats,汇总所有物理磁盘的读写数据
|
||||||
// fields[2]=设备名, fields[3]=读完成次数, fields[5]=读扇区数, fields[7]=写完成次数, fields[9]=写扇区数
|
// fields[2]=设备名, fields[3]=读完成次数, fields[5]=读扇区数, fields[7]=写完成次数, fields[9]=写扇区数, fields[12]=io_ticks
|
||||||
func readDiskStats() (rSects, wSects, rOps, wOps uint64, err error) {
|
func readDiskStats() (rSects, wSects, rOps, wOps, ioTicks uint64, err error) {
|
||||||
f, err := os.Open("/proc/diskstats")
|
f, err := os.Open("/proc/diskstats")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, 0, 0, err
|
return 0, 0, 0, 0, 0, err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
@@ -103,6 +114,7 @@ func readDiskStats() (rSects, wSects, rOps, wOps uint64, err error) {
|
|||||||
// field 5 = 读扇区数 (rd_sectors)
|
// field 5 = 读扇区数 (rd_sectors)
|
||||||
// field 7 = 写完成次数 (wr_ios)
|
// field 7 = 写完成次数 (wr_ios)
|
||||||
// field 9 = 写扇区数 (wr_sectors)
|
// field 9 = 写扇区数 (wr_sectors)
|
||||||
|
// field 12 = io_ticks (milliseconds spent doing I/O)
|
||||||
if v, e := strconv.ParseUint(fields[3], 10, 64); e == nil {
|
if v, e := strconv.ParseUint(fields[3], 10, 64); e == nil {
|
||||||
rOps += v
|
rOps += v
|
||||||
}
|
}
|
||||||
@@ -115,7 +127,10 @@ func readDiskStats() (rSects, wSects, rOps, wOps uint64, err error) {
|
|||||||
if v, e := strconv.ParseUint(fields[9], 10, 64); e == nil {
|
if v, e := strconv.ParseUint(fields[9], 10, 64); e == nil {
|
||||||
wSects += v
|
wSects += v
|
||||||
}
|
}
|
||||||
|
if v, e := strconv.ParseUint(fields[12], 10, 64); e == nil {
|
||||||
|
ioTicks += v
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return rSects, wSects, rOps, wOps, scanner.Err()
|
return rSects, wSects, rOps, wOps, ioTicks, scanner.Err()
|
||||||
}
|
}
|
||||||
+13
-3
@@ -37,6 +37,8 @@ func (m *MemCollector) Collect() (model.MemoryStat, error) {
|
|||||||
cachedKB := values["Cached"]
|
cachedKB := values["Cached"]
|
||||||
availableKB := values["MemAvailable"]
|
availableKB := values["MemAvailable"]
|
||||||
freeKB := values["MemFree"]
|
freeKB := values["MemFree"]
|
||||||
|
swapTotalKB := values["SwapTotal"]
|
||||||
|
swapFreeKB := values["SwapFree"]
|
||||||
|
|
||||||
// 若内核未提供 MemAvailable(较老版本),则按传统方式估算可用内存
|
// 若内核未提供 MemAvailable(较老版本),则按传统方式估算可用内存
|
||||||
if availableKB == 0 {
|
if availableKB == 0 {
|
||||||
@@ -48,10 +50,18 @@ func (m *MemCollector) Collect() (model.MemoryStat, error) {
|
|||||||
usedKB = totalKB - availableKB
|
usedKB = totalKB - availableKB
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Swap used
|
||||||
|
swapUsedKB := uint64(0)
|
||||||
|
if swapTotalKB > swapFreeKB {
|
||||||
|
swapUsedKB = swapTotalKB - swapFreeKB
|
||||||
|
}
|
||||||
|
|
||||||
stat := model.MemoryStat{
|
stat := model.MemoryStat{
|
||||||
TotalGB: float64(totalKB) * kbToGB,
|
TotalGB: float64(totalKB) * kbToGB,
|
||||||
UsedGB: float64(usedKB) * kbToGB,
|
UsedGB: float64(usedKB) * kbToGB,
|
||||||
CachedGB: float64(buffersKB+cachedKB) * kbToGB,
|
CachedGB: float64(buffersKB+cachedKB) * kbToGB,
|
||||||
|
AvailableGB: float64(availableKB) * kbToGB,
|
||||||
|
SwapUsedGB: float64(swapUsedKB) * kbToGB,
|
||||||
}
|
}
|
||||||
if totalKB > 0 {
|
if totalKB > 0 {
|
||||||
stat.UsagePercent = float64(usedKB) / float64(totalKB) * 100.0
|
stat.UsagePercent = float64(usedKB) / float64(totalKB) * 100.0
|
||||||
|
|||||||
+25
-14
@@ -18,9 +18,11 @@ import (
|
|||||||
|
|
||||||
// netSample 保存上一次的网络采样数据,用于计算速率
|
// netSample 保存上一次的网络采样数据,用于计算速率
|
||||||
type netSample struct {
|
type netSample struct {
|
||||||
time time.Time
|
time time.Time
|
||||||
rxBytes uint64
|
rxBytes uint64
|
||||||
txBytes uint64
|
txBytes uint64
|
||||||
|
rxErrors uint64
|
||||||
|
txErrors uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
// NetworkCollector 网络指标采集器
|
// NetworkCollector 网络指标采集器
|
||||||
@@ -34,7 +36,7 @@ func NewNetworkCollector() *NetworkCollector {
|
|||||||
return &NetworkCollector{}
|
return &NetworkCollector{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect 采集网络指标(并发连接数、TIME_WAIT、网卡 rx/tx B/s)
|
// Collect 采集网络指标(并发连接数、TIME_WAIT、网卡 rx/tx B/s、错误包)
|
||||||
func (c *NetworkCollector) Collect() (model.NetworkStat, error) {
|
func (c *NetworkCollector) Collect() (model.NetworkStat, error) {
|
||||||
stat := model.NetworkStat{}
|
stat := model.NetworkStat{}
|
||||||
|
|
||||||
@@ -46,8 +48,8 @@ func (c *NetworkCollector) Collect() (model.NetworkStat, error) {
|
|||||||
}
|
}
|
||||||
stat.TCPCloseWait = countCloseWait()
|
stat.TCPCloseWait = countCloseWait()
|
||||||
|
|
||||||
// 2. 采集网卡流量(取所有非 lo 网卡合计)
|
// 2. 采集网卡流量 + 错误包(取所有非 lo 网卡合计)
|
||||||
curRx, curTx, nicName, err := readNetDevTotal()
|
curRx, curTx, curRxErr, curTxErr, nicName, err := readNetDevTotal()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return stat, fmt.Errorf("读取网卡流量失败: %w", err)
|
return stat, fmt.Errorf("读取网卡流量失败: %w", err)
|
||||||
}
|
}
|
||||||
@@ -57,7 +59,7 @@ func (c *NetworkCollector) Collect() (model.NetworkStat, error) {
|
|||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
cur := &netSample{time: now, rxBytes: curRx, txBytes: curTx}
|
cur := &netSample{time: now, rxBytes: curRx, txBytes: curTx, rxErrors: curRxErr, txErrors: curTxErr}
|
||||||
|
|
||||||
if c.prev != nil && now.After(c.prev.time) {
|
if c.prev != nil && now.After(c.prev.time) {
|
||||||
elapsed := now.Sub(c.prev.time).Seconds()
|
elapsed := now.Sub(c.prev.time).Seconds()
|
||||||
@@ -66,6 +68,11 @@ func (c *NetworkCollector) Collect() (model.NetworkStat, error) {
|
|||||||
stat.NetTxBPS = float64(cur.txBytes-c.prev.txBytes) / elapsed
|
stat.NetTxBPS = float64(cur.txBytes-c.prev.txBytes) / elapsed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 错误包:返回当前累计值(供前端判断是否激增)
|
||||||
|
stat.NetRxErrors = int64(cur.rxErrors)
|
||||||
|
stat.NetTxErrors = int64(cur.txErrors)
|
||||||
|
|
||||||
c.prev = cur
|
c.prev = cur
|
||||||
|
|
||||||
return stat, nil
|
return stat, nil
|
||||||
@@ -120,11 +127,11 @@ func readSockStat() (tcpSockStat, error) {
|
|||||||
return result, scanner.Err()
|
return result, scanner.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// readNetDevTotal 读取 /proc/net/dev,取第一个非 lo 且活跃的物理网卡总流量
|
// readNetDevTotal 读取 /proc/net/dev,取第一个非 lo 且活跃的物理网卡总流量 + 错误包
|
||||||
func readNetDevTotal() (rxBytes, txBytes uint64, nicName string, err error) {
|
func readNetDevTotal() (rxBytes, txBytes, rxErrors, txErrors uint64, nicName string, err error) {
|
||||||
f, err := os.Open("/proc/net/dev")
|
f, err := os.Open("/proc/net/dev")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, "", err
|
return 0, 0, 0, 0, "", err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
@@ -149,26 +156,30 @@ func readNetDevTotal() (rxBytes, txBytes uint64, nicName string, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fields := strings.Fields(parts[1])
|
fields := strings.Fields(parts[1])
|
||||||
if len(fields) < 10 {
|
if len(fields) < 11 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// fields[0]=rx_bytes, fields[8]=tx_bytes
|
// fields[0]=rx_bytes, fields[2]=rx_errors, fields[8]=tx_bytes, fields[10]=tx_errors
|
||||||
rb, _ := strconv.ParseUint(fields[0], 10, 64)
|
rb, _ := strconv.ParseUint(fields[0], 10, 64)
|
||||||
tb, _ := strconv.ParseUint(fields[8], 10, 64)
|
tb, _ := strconv.ParseUint(fields[8], 10, 64)
|
||||||
|
re, _ := strconv.ParseUint(fields[2], 10, 64)
|
||||||
|
te, _ := strconv.ParseUint(fields[10], 10, 64)
|
||||||
|
|
||||||
if rb > 0 || tb > 0 {
|
if rb > 0 || tb > 0 {
|
||||||
nicName = name
|
nicName = name
|
||||||
rxBytes += rb
|
rxBytes += rb
|
||||||
txBytes += tb
|
txBytes += tb
|
||||||
|
rxErrors += re
|
||||||
|
txErrors += te
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if nicName == "" {
|
if nicName == "" {
|
||||||
return 0, 0, "eth0", nil
|
return 0, 0, 0, 0, "eth0", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return rxBytes, txBytes, nicName, scanner.Err()
|
return rxBytes, txBytes, rxErrors, txErrors, nicName, scanner.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// countCloseWait 从 /proc/net/tcp 统计 CLOSE_WAIT 连接数
|
// countCloseWait 从 /proc/net/tcp 统计 CLOSE_WAIT 连接数
|
||||||
|
|||||||
+71
-2
@@ -2,6 +2,8 @@
|
|||||||
// 本文件实现监听 TCP 端口的自动发现:
|
// 本文件实现监听 TCP 端口的自动发现:
|
||||||
// - 扫描 /proc/net/tcp 与 /proc/net/tcp6 中状态为 LISTEN(0A) 的套接字;
|
// - 扫描 /proc/net/tcp 与 /proc/net/tcp6 中状态为 LISTEN(0A) 的套接字;
|
||||||
// - 通过遍历 /proc/*/fd 反查套接字 inode 归属的进程。
|
// - 通过遍历 /proc/*/fd 反查套接字 inode 归属的进程。
|
||||||
|
// - 内置系统进程排除列表(sshd、rpcbind、master 等永远不上报)。
|
||||||
|
// - 支持远程端口策略:whitelist(全量上报,默认)/ blacklist(只上报白名单端口)。
|
||||||
//
|
//
|
||||||
// 署名 Logan.Li
|
// 署名 Logan.Li
|
||||||
package collector
|
package collector
|
||||||
@@ -15,14 +17,47 @@ import (
|
|||||||
"ezops/agent/model"
|
"ezops/agent/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 系统级进程 — 永远不采集其监听端口
|
||||||
|
var systemProcesses = map[string]bool{
|
||||||
|
"sshd": true,
|
||||||
|
"rpcbind": true,
|
||||||
|
"master": true, // Postfix
|
||||||
|
"systemd": true,
|
||||||
|
"systemd-resolve": true,
|
||||||
|
"systemd-journal": true,
|
||||||
|
"dbus-daemon": true,
|
||||||
|
"rsyslogd": true,
|
||||||
|
"irqbalance": true,
|
||||||
|
"polkitd": true,
|
||||||
|
"accounts-daemon": true,
|
||||||
|
"NetworkManager": true,
|
||||||
|
"udisksd": true,
|
||||||
|
"upowerd": true,
|
||||||
|
"rtkit-daemon": true,
|
||||||
|
"switcheroo-contr": true,
|
||||||
|
"power-profiles-": true, // power-profiles-daemon
|
||||||
|
"wpa_supplicant": true,
|
||||||
|
"avahi-daemon": true,
|
||||||
|
"cron": true,
|
||||||
|
"atd": true,
|
||||||
|
"agetty": true,
|
||||||
|
}
|
||||||
|
|
||||||
// PortCollector 监听端口自动发现采集器
|
// PortCollector 监听端口自动发现采集器
|
||||||
type PortCollector struct{}
|
type PortCollector struct {
|
||||||
|
Policy *model.PortPolicy // 可选:后端下发的端口过滤策略
|
||||||
|
}
|
||||||
|
|
||||||
// NewPortCollector 创建端口采集器实例
|
// NewPortCollector 创建端口采集器实例
|
||||||
func NewPortCollector() *PortCollector {
|
func NewPortCollector() *PortCollector {
|
||||||
return &PortCollector{}
|
return &PortCollector{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetPolicy 设置端口过滤策略(daemon 模式下从后端轮询获得)
|
||||||
|
func (p *PortCollector) SetPolicy(policy *model.PortPolicy) {
|
||||||
|
p.Policy = policy
|
||||||
|
}
|
||||||
|
|
||||||
// listenSocket 描述一个处于 LISTEN 状态的套接字
|
// listenSocket 描述一个处于 LISTEN 状态的套接字
|
||||||
type listenSocket struct {
|
type listenSocket struct {
|
||||||
port int
|
port int
|
||||||
@@ -47,7 +82,7 @@ func (p *PortCollector) Collect() ([]model.PortStat, error) {
|
|||||||
// 2. 构建 inode -> 进程信息 映射(仅 Linux 上可用)
|
// 2. 构建 inode -> 进程信息 映射(仅 Linux 上可用)
|
||||||
inodeMap := p.buildInodeMap()
|
inodeMap := p.buildInodeMap()
|
||||||
|
|
||||||
// 3. 组装结果并按 (端口, 协议, PID) 去重
|
// 3. 组装结果并按 (端口, 协议, PID) 去重,应用过滤策略
|
||||||
seen := make(map[string]bool)
|
seen := make(map[string]bool)
|
||||||
ports := make([]model.PortStat, 0, len(sockets))
|
ports := make([]model.PortStat, 0, len(sockets))
|
||||||
for _, s := range sockets {
|
for _, s := range sockets {
|
||||||
@@ -55,6 +90,22 @@ func (p *PortCollector) Collect() ([]model.PortStat, error) {
|
|||||||
if info, ok := inodeMap[s.inode]; ok {
|
if info, ok := inodeMap[s.inode]; ok {
|
||||||
pid, name = info.pid, info.name
|
pid, name = info.pid, info.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 过滤逻辑 ──
|
||||||
|
// 1) 永远排除系统级进程端口
|
||||||
|
if isSystemProcess(name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) 黑名单模式:只上报在白名单中的端口
|
||||||
|
if p.Policy != nil && p.Policy.IsBlacklistMode() {
|
||||||
|
if !p.Policy.InWhitelist(s.port) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 白名单模式(默认):全量上报已排除系统进程的端口
|
||||||
|
// ── 过滤逻辑结束 ──
|
||||||
|
|
||||||
key := strconv.Itoa(s.port) + "|" + s.protocol + "|" + strconv.Itoa(pid)
|
key := strconv.Itoa(s.port) + "|" + s.protocol + "|" + strconv.Itoa(pid)
|
||||||
if seen[key] {
|
if seen[key] {
|
||||||
continue
|
continue
|
||||||
@@ -200,3 +251,21 @@ func readProcessName(pid string) string {
|
|||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isSystemProcess 判断进程名是否为系统进程(永远不上报的端口)
|
||||||
|
func isSystemProcess(name string) bool {
|
||||||
|
if name == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// 精确匹配
|
||||||
|
if systemProcesses[name] {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// 前缀匹配(如 power-profiles-daemon)
|
||||||
|
for prefix := range systemProcesses {
|
||||||
|
if strings.HasPrefix(name, prefix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -79,6 +79,9 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch cmd {
|
switch cmd {
|
||||||
|
case "version", "v", "-v", "--version":
|
||||||
|
fmt.Printf("AIOps Agent %s (built %s) go=%s\n", version, buildTime, runtime.Version())
|
||||||
|
return
|
||||||
case "output":
|
case "output":
|
||||||
runOutput(cfg)
|
runOutput(cfg)
|
||||||
return
|
return
|
||||||
@@ -279,10 +282,25 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pullPortPolicy := func() {
|
||||||
|
policy, err := taskClient.PollPortPolicy(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 静默忽略 — 端口策略为可选配置
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if policy != nil {
|
||||||
|
portCollector.SetPolicy(policy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const pollInterval = 5 * time.Second
|
const pollInterval = 5 * time.Second
|
||||||
go func() {
|
go func() {
|
||||||
pollAndExecute()
|
pollAndExecute()
|
||||||
pullProbes()
|
pullProbes()
|
||||||
|
pullPortPolicy()
|
||||||
ticker := time.NewTicker(pollInterval)
|
ticker := time.NewTicker(pollInterval)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
for {
|
for {
|
||||||
@@ -290,6 +308,7 @@ func main() {
|
|||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
pollAndExecute()
|
pollAndExecute()
|
||||||
pullProbes()
|
pullProbes()
|
||||||
|
pullPortPolicy()
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -404,6 +423,7 @@ func runOutput(cfg Config) {
|
|||||||
|
|
||||||
// CPU
|
// CPU
|
||||||
pd("cpu.usage_percent", report.CPU.UsagePercent)
|
pd("cpu.usage_percent", report.CPU.UsagePercent)
|
||||||
|
pd("cpu.iowait", report.CPU.IOWait)
|
||||||
pu("cpu.core_count", report.CPU.CoreCount)
|
pu("cpu.core_count", report.CPU.CoreCount)
|
||||||
pd("cpu.load1", report.CPU.Load1)
|
pd("cpu.load1", report.CPU.Load1)
|
||||||
pd("cpu.load5", report.CPU.Load5)
|
pd("cpu.load5", report.CPU.Load5)
|
||||||
@@ -413,6 +433,8 @@ func runOutput(cfg Config) {
|
|||||||
pd("memory.total_gb", report.Memory.TotalGB)
|
pd("memory.total_gb", report.Memory.TotalGB)
|
||||||
pd("memory.used_gb", report.Memory.UsedGB)
|
pd("memory.used_gb", report.Memory.UsedGB)
|
||||||
pd("memory.cached_gb", report.Memory.CachedGB)
|
pd("memory.cached_gb", report.Memory.CachedGB)
|
||||||
|
pd("memory.available_gb", report.Memory.AvailableGB)
|
||||||
|
pd("memory.swap_used_gb", report.Memory.SwapUsedGB)
|
||||||
pd("memory.usage_percent", report.Memory.UsagePercent)
|
pd("memory.usage_percent", report.Memory.UsagePercent)
|
||||||
|
|
||||||
// Swap
|
// Swap
|
||||||
@@ -428,6 +450,7 @@ func runOutput(cfg Config) {
|
|||||||
pd(prefix+".total_gb", d.TotalGB)
|
pd(prefix+".total_gb", d.TotalGB)
|
||||||
pd(prefix+".used_gb", d.UsedGB)
|
pd(prefix+".used_gb", d.UsedGB)
|
||||||
pd(prefix+".usage_percent", d.UsagePercent)
|
pd(prefix+".usage_percent", d.UsagePercent)
|
||||||
|
pd(prefix+".inode_usage_pct", d.InodeUsagePct)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 端口 (监听)
|
// 端口 (监听)
|
||||||
@@ -457,14 +480,18 @@ func runOutput(cfg Config) {
|
|||||||
pu("net.tcp.time_wait", report.Network.TCPTimeWait)
|
pu("net.tcp.time_wait", report.Network.TCPTimeWait)
|
||||||
pu("net.tcp.close_wait", report.Network.TCPCloseWait)
|
pu("net.tcp.close_wait", report.Network.TCPCloseWait)
|
||||||
pu("net.tcp.total", report.Network.TCPTotal)
|
pu("net.tcp.total", report.Network.TCPTotal)
|
||||||
|
pu("net.connection.total", report.Network.TCPEstablished+report.Network.TCPTimeWait+report.Network.TCPCloseWait)
|
||||||
pd("net.rx_bps", report.Network.NetRxBPS)
|
pd("net.rx_bps", report.Network.NetRxBPS)
|
||||||
pd("net.tx_bps", report.Network.NetTxBPS)
|
pd("net.tx_bps", report.Network.NetTxBPS)
|
||||||
|
pu64("net.rx_errors", report.Network.NetRxErrors)
|
||||||
|
pu64("net.tx_errors", report.Network.NetTxErrors)
|
||||||
|
|
||||||
// 磁盘 IO
|
// 磁盘 IO
|
||||||
pd("disk_io.read_bps", report.DiskIO.ReadBPS)
|
pd("disk_io.read_bps", report.DiskIO.ReadBPS)
|
||||||
pd("disk_io.write_bps", report.DiskIO.WriteBPS)
|
pd("disk_io.write_bps", report.DiskIO.WriteBPS)
|
||||||
pd("disk_io.read_iops", report.DiskIO.ReadIOPS)
|
pd("disk_io.read_iops", report.DiskIO.ReadIOPS)
|
||||||
pd("disk_io.write_iops", report.DiskIO.WriteIOPS)
|
pd("disk_io.write_iops", report.DiskIO.WriteIOPS)
|
||||||
|
pd("disk_io.util", report.DiskIO.Util)
|
||||||
|
|
||||||
// 进程 TOP
|
// 进程 TOP
|
||||||
for i, pr := range report.ProcessTop {
|
for i, pr := range report.ProcessTop {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ type Report struct {
|
|||||||
// CPUStat CPU 相关指标
|
// CPUStat CPU 相关指标
|
||||||
type CPUStat struct {
|
type CPUStat struct {
|
||||||
UsagePercent float64 `json:"usage_percent"` // CPU 总体使用率(百分比)
|
UsagePercent float64 `json:"usage_percent"` // CPU 总体使用率(百分比)
|
||||||
|
IOWait float64 `json:"iowait"` // CPU 等待 IO 的时间占比(百分比)
|
||||||
CoreCount int `json:"core_count"` // CPU 逻辑核心数
|
CoreCount int `json:"core_count"` // CPU 逻辑核心数
|
||||||
Load1 float64 `json:"load1"` // 1 分钟平均负载
|
Load1 float64 `json:"load1"` // 1 分钟平均负载
|
||||||
Load5 float64 `json:"load5"` // 5 分钟平均负载
|
Load5 float64 `json:"load5"` // 5 分钟平均负载
|
||||||
@@ -39,16 +40,19 @@ type MemoryStat struct {
|
|||||||
TotalGB float64 `json:"total_gb"` // 总内存(GB)
|
TotalGB float64 `json:"total_gb"` // 总内存(GB)
|
||||||
UsedGB float64 `json:"used_gb"` // 已用内存(GB)
|
UsedGB float64 `json:"used_gb"` // 已用内存(GB)
|
||||||
CachedGB float64 `json:"cached_gb"` // 缓存(Buffers + Cached,GB)
|
CachedGB float64 `json:"cached_gb"` // 缓存(Buffers + Cached,GB)
|
||||||
|
AvailableGB float64 `json:"available_gb"` // 可用内存(GB),含可回收缓存 — 真实剩余可用
|
||||||
|
SwapUsedGB float64 `json:"swap_used_gb"` // Swap 已用量(GB)— OOM 金标准
|
||||||
UsagePercent float64 `json:"usage_percent"` // 内存使用率(百分比)
|
UsagePercent float64 `json:"usage_percent"` // 内存使用率(百分比)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DiskStat 单个挂载点的磁盘使用情况
|
// DiskStat 单个挂载点的磁盘使用情况
|
||||||
type DiskStat struct {
|
type DiskStat struct {
|
||||||
Mount string `json:"mount"` // 挂载点
|
Mount string `json:"mount"` // 挂载点
|
||||||
Device string `json:"device"` // 设备名
|
Device string `json:"device"` // 设备名
|
||||||
TotalGB float64 `json:"total_gb"` // 总容量(GB)
|
TotalGB float64 `json:"total_gb"` // 总容量(GB)
|
||||||
UsedGB float64 `json:"used_gb"` // 已用容量(GB)
|
UsedGB float64 `json:"used_gb"` // 已用容量(GB)
|
||||||
UsagePercent float64 `json:"usage_percent"` // 使用率(百分比)
|
UsagePercent float64 `json:"usage_percent"` // 使用率(百分比)
|
||||||
|
InodeUsagePct float64 `json:"inode_usage_pct"` // inode 使用率(百分比)— 防止文件数爆炸
|
||||||
}
|
}
|
||||||
|
|
||||||
// PortStat 监听端口及其归属进程
|
// PortStat 监听端口及其归属进程
|
||||||
|
|||||||
+41
-16
@@ -9,6 +9,8 @@ type NetworkStat struct {
|
|||||||
NicName string `json:"nic_name"` // 主网卡名
|
NicName string `json:"nic_name"` // 主网卡名
|
||||||
NetRxBPS float64 `json:"net_rx_bps"` // 网卡接收速率 (bytes/s)
|
NetRxBPS float64 `json:"net_rx_bps"` // 网卡接收速率 (bytes/s)
|
||||||
NetTxBPS float64 `json:"net_tx_bps"` // 网卡发送速率 (bytes/s)
|
NetTxBPS float64 `json:"net_tx_bps"` // 网卡发送速率 (bytes/s)
|
||||||
|
NetRxErrors int64 `json:"net_rx_errors"` // 网卡接收错误包累计 — 物理层故障告警
|
||||||
|
NetTxErrors int64 `json:"net_tx_errors"` // 网卡发送错误包累计 — 物理层故障告警
|
||||||
}
|
}
|
||||||
|
|
||||||
// DiskIOStat 磁盘 IO 相关指标
|
// DiskIOStat 磁盘 IO 相关指标
|
||||||
@@ -17,6 +19,7 @@ type DiskIOStat struct {
|
|||||||
WriteBPS float64 `json:"write_bps"` // 写入速率 (bytes/s)
|
WriteBPS float64 `json:"write_bps"` // 写入速率 (bytes/s)
|
||||||
ReadIOPS float64 `json:"read_iops"` // 读取 IOPS (count/s)
|
ReadIOPS float64 `json:"read_iops"` // 读取 IOPS (count/s)
|
||||||
WriteIOPS float64 `json:"write_iops"` // 写入 IOPS (count/s)
|
WriteIOPS float64 `json:"write_iops"` // 写入 IOPS (count/s)
|
||||||
|
Util float64 `json:"util"` // 磁盘繁忙程度(百分比)— 判断磁盘是否瓶颈的核心指标
|
||||||
}
|
}
|
||||||
|
|
||||||
// SwapStat 交换分区指标(OOM 前兆判断)
|
// SwapStat 交换分区指标(OOM 前兆判断)
|
||||||
@@ -28,34 +31,34 @@ type SwapStat struct {
|
|||||||
|
|
||||||
// SocketFDStat 系统级 FD 使用情况
|
// SocketFDStat 系统级 FD 使用情况
|
||||||
type SocketFDStat struct {
|
type SocketFDStat struct {
|
||||||
Allocated int `json:"allocated"` // 已分配的 file handles (fs.file-nr 第一列)
|
Allocated int `json:"allocated"` // 已分配的 file handles (fs.file-nr 第一列)
|
||||||
Max int64 `json:"max"` // 系统上限 (fs.file-max)
|
Max int64 `json:"max"` // 系统上限 (fs.file-max)
|
||||||
UsagePct float64 `json:"usage_pct"`
|
UsagePct float64 `json:"usage_pct"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContextSwitchStat 上下文切换(决定是否开中断合并/CPU 隔离)
|
// ContextSwitchStat 上下文切换(决定是否开中断合并/CPU 隔离)
|
||||||
type ContextSwitchStat struct {
|
type ContextSwitchStat struct {
|
||||||
CTXPerSec float64 `json:"ctx_per_sec"` // 每秒上下文切换次数
|
CTXPerSec float64 `json:"ctx_per_sec"` // 每秒上下文切换次数
|
||||||
IRQPerSec float64 `json:"irq_per_sec"` // 每秒中断次数
|
IRQPerSec float64 `json:"irq_per_sec"` // 每秒中断次数
|
||||||
}
|
}
|
||||||
|
|
||||||
// AgentHeartbeat Agent 自检心跳(随每次上报附带,极小 payload)
|
// AgentHeartbeat Agent 自检心跳(随每次上报附带,极小 payload)
|
||||||
type AgentHeartbeat struct {
|
type AgentHeartbeat struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
UptimeSec int64 `json:"uptime_sec"`
|
UptimeSec int64 `json:"uptime_sec"`
|
||||||
LastErr string `json:"last_err,omitempty"`
|
LastErr string `json:"last_err,omitempty"`
|
||||||
SelfCPU float64 `json:"self_cpu_pct"`
|
SelfCPU float64 `json:"self_cpu_pct"`
|
||||||
SelfRSSMB float64 `json:"self_rss_mb"`
|
SelfRSSMB float64 `json:"self_rss_mb"`
|
||||||
ReportSeq uint64 `json:"report_seq"` // 单调递增序列号,用于丢包检测
|
ReportSeq uint64 `json:"report_seq"` // 单调递增序列号,用于丢包检测
|
||||||
}
|
}
|
||||||
|
|
||||||
// SystemStat 系统信息(启动时采集一次)
|
// SystemStat 系统信息(启动时采集一次)
|
||||||
type SystemStat struct {
|
type SystemStat struct {
|
||||||
OSVersion string `json:"os_version"` // OS 版本 (如 "Ubuntu 22.04")
|
OSVersion string `json:"os_version"` // OS 版本 (如 "Ubuntu 22.04")
|
||||||
Kernel string `json:"kernel"` // 内核版本
|
Kernel string `json:"kernel"` // 内核版本
|
||||||
Uptime int64 `json:"uptime"` // 运行秒数
|
Uptime int64 `json:"uptime"` // 运行秒数
|
||||||
UlimitNofile int64 `json:"ulimit_nofile"` // ulimit -n (最大文件描述符)
|
UlimitNofile int64 `json:"ulimit_nofile"` // ulimit -n (最大文件描述符)
|
||||||
UlimitNproc int64 `json:"ulimit_nproc"` // ulimit -u (最大进程数)
|
UlimitNproc int64 `json:"ulimit_nproc"` // ulimit -u (最大进程数)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ──── v2.1 健康探针 ────
|
// ──── v2.1 健康探针 ────
|
||||||
@@ -80,8 +83,30 @@ type ProbeResult struct {
|
|||||||
ProbeID uint `json:"probe_id"`
|
ProbeID uint `json:"probe_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
ProbeType string `json:"probe_type"`
|
ProbeType string `json:"probe_type"`
|
||||||
Status string `json:"status"` // up / down
|
Status string `json:"status"` // up / down
|
||||||
LatencyMs int `json:"latency_ms"`
|
LatencyMs int `json:"latency_ms"`
|
||||||
StatusCode int `json:"status_code,omitempty"`
|
StatusCode int `json:"status_code,omitempty"`
|
||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PortPolicy 后端下发的端口过滤策略 // Logan.Li
|
||||||
|
type PortPolicy struct {
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
PortFilterMode string `json:"port_filter_mode"` // whitelist / blacklist
|
||||||
|
PortWhitelist []int `json:"port_whitelist"` // 端口号数组
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsBlacklistMode 判断是否为黑名单模式(只上报选中端口)
|
||||||
|
func (p *PortPolicy) IsBlacklistMode() bool {
|
||||||
|
return p.PortFilterMode == "blacklist"
|
||||||
|
}
|
||||||
|
|
||||||
|
// InWhitelist 检查端口是否在白名单中
|
||||||
|
func (p *PortPolicy) InWhitelist(port int) bool {
|
||||||
|
for _, w := range p.PortWhitelist {
|
||||||
|
if w == port {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ const (
|
|||||||
pollPath = "/api/v1/agent/tasks/poll"
|
pollPath = "/api/v1/agent/tasks/poll"
|
||||||
resultPathFmt = "/api/v1/agent/tasks/%s/result"
|
resultPathFmt = "/api/v1/agent/tasks/%s/result"
|
||||||
probePath = "/api/v1/agent/health-probes/config"
|
probePath = "/api/v1/agent/health-probes/config"
|
||||||
|
portPolicyPath = "/api/v1/agent/port-policy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client 负责与后端任务/探针接口通信。
|
// Client 负责与后端任务/探针接口通信。
|
||||||
@@ -74,3 +75,18 @@ func (c *Client) PollProbes(ctx context.Context) ([]model.ProbeConfig, error) {
|
|||||||
}
|
}
|
||||||
return wrapper.Data, nil
|
return wrapper.Data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PollPortPolicy 拉取本机端口过滤策略(可选配置,拉取失败静默忽略) // Logan.Li
|
||||||
|
func (c *Client) PollPortPolicy(ctx context.Context) (*model.PortPolicy, error) {
|
||||||
|
path := portPolicyPath + "?hostname=" + url.QueryEscape(c.hostname)
|
||||||
|
|
||||||
|
var wrapper struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data model.PortPolicy `json:"data"`
|
||||||
|
}
|
||||||
|
if err := c.client.DoJSON(ctx, "GET", path, nil, &wrapper); err != nil {
|
||||||
|
return nil, fmt.Errorf("端口策略拉取失败: %w", err)
|
||||||
|
}
|
||||||
|
return &wrapper.Data, nil
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
@@ -62,6 +62,7 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
|
|||||||
report := models.AgentReport{
|
report := models.AgentReport{
|
||||||
Hostname: req.Hostname,
|
Hostname: req.Hostname,
|
||||||
CPUUsage: req.CPU.UsagePercent,
|
CPUUsage: req.CPU.UsagePercent,
|
||||||
|
CPUIOwait: req.CPU.IOWait,
|
||||||
CPUCores: req.CPU.CoreCount,
|
CPUCores: req.CPU.CoreCount,
|
||||||
Load1: req.CPU.Load1,
|
Load1: req.CPU.Load1,
|
||||||
Load5: req.CPU.Load5,
|
Load5: req.CPU.Load5,
|
||||||
@@ -69,6 +70,8 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
|
|||||||
MemTotal: req.Memory.TotalGB,
|
MemTotal: req.Memory.TotalGB,
|
||||||
MemUsed: req.Memory.UsedGB,
|
MemUsed: req.Memory.UsedGB,
|
||||||
MemUsage: req.Memory.UsagePercent,
|
MemUsage: req.Memory.UsagePercent,
|
||||||
|
MemAvailable: req.Memory.AvailableGB,
|
||||||
|
MemSwapUsed: req.Memory.SwapUsedGB,
|
||||||
DiskInfo: string(diskJSON),
|
DiskInfo: string(diskJSON),
|
||||||
PortInfo: string(portJSON),
|
PortInfo: string(portJSON),
|
||||||
NetInfo: string(netJSON),
|
NetInfo: string(netJSON),
|
||||||
@@ -110,7 +113,7 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
|
|||||||
// 直接 SQL 更新 asset_servers 监控字段
|
// 直接 SQL 更新 asset_servers 监控字段
|
||||||
// 若 AgentVersion 非空则同步写入,否则保留数据库中已有版本号(防止旧版Agent空字段覆盖已设置的版本)
|
// 若 AgentVersion 非空则同步写入,否则保留数据库中已有版本号(防止旧版Agent空字段覆盖已设置的版本)
|
||||||
updateSQL := `UPDATE asset_servers SET
|
updateSQL := `UPDATE asset_servers SET
|
||||||
cpu_usage = ?, memory_usage = ?, disk_usage = ?,
|
cpu_usage = ?, cpu_iowait = ?, memory_usage = ?, disk_usage = ?,
|
||||||
cpu = ?, memory = ?, disk = ?,
|
cpu = ?, memory = ?, disk = ?,
|
||||||
status = ?, agent_status = ?` + func() string {
|
status = ?, agent_status = ?` + func() string {
|
||||||
if req.AgentVersion != "" {
|
if req.AgentVersion != "" {
|
||||||
@@ -121,7 +124,7 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
|
|||||||
hostname = CASE WHEN hostname != ? AND ip = ? THEN ? ELSE hostname END,
|
hostname = CASE WHEN hostname != ? AND ip = ? THEN ? ELSE hostname END,
|
||||||
updated_at = NOW()`
|
updated_at = NOW()`
|
||||||
args := []interface{}{
|
args := []interface{}{
|
||||||
req.CPU.UsagePercent, req.Memory.UsagePercent, diskAvg,
|
req.CPU.UsagePercent, req.CPU.IOWait, req.Memory.UsagePercent, diskAvg,
|
||||||
req.CPU.CoreCount, memMB, diskGB,
|
req.CPU.CoreCount, memMB, diskGB,
|
||||||
"running", "online",
|
"running", "online",
|
||||||
}
|
}
|
||||||
@@ -528,3 +531,94 @@ func avgDiskUsage(disks []models.DiskMetric) float64 {
|
|||||||
}
|
}
|
||||||
return math.Round(sum/float64(len(disks))*100) / 100
|
return math.Round(sum/float64(len(disks))*100) / 100
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 端口监控策略 ── // Logan.Li
|
||||||
|
|
||||||
|
// GetPortPolicyForAgent Agent 拉取本机端口策略(无需鉴权,通过 hostname 查询)
|
||||||
|
// GET /api/v1/agent/port-policy?hostname=xxx
|
||||||
|
func (h *AgentHandler) GetPortPolicyForAgent(c *gin.Context) {
|
||||||
|
hostname := c.Query("hostname")
|
||||||
|
if hostname == "" {
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: models.AgentPortPolicyDTO{
|
||||||
|
PortFilterMode: "blacklist",
|
||||||
|
PortWhitelist: []int{},
|
||||||
|
}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var policy models.AgentPortPolicy
|
||||||
|
if err := h.db.Where("hostname = ?", hostname).First(&policy).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: models.AgentPortPolicyDTO{
|
||||||
|
Hostname: hostname,
|
||||||
|
PortFilterMode: "blacklist",
|
||||||
|
PortWhitelist: []int{},
|
||||||
|
}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var whitelist []int
|
||||||
|
json.Unmarshal([]byte(policy.PortWhitelist), &whitelist)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: models.AgentPortPolicyDTO{
|
||||||
|
Hostname: policy.Hostname,
|
||||||
|
PortFilterMode: policy.PortFilterMode,
|
||||||
|
PortWhitelist: whitelist,
|
||||||
|
}})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPortPolicy 前端获取主机的端口监控策略
|
||||||
|
// GET /api/v1/agent/port-policies/:hostname
|
||||||
|
func (h *AgentHandler) GetPortPolicy(c *gin.Context) {
|
||||||
|
hostname := c.Param("hostname")
|
||||||
|
|
||||||
|
var policy models.AgentPortPolicy
|
||||||
|
var whitelist []int
|
||||||
|
if err := h.db.Where("hostname = ?", hostname).First(&policy).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: models.AgentPortPolicyDTO{
|
||||||
|
Hostname: hostname,
|
||||||
|
PortFilterMode: "blacklist",
|
||||||
|
PortWhitelist: []int{},
|
||||||
|
}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json.Unmarshal([]byte(policy.PortWhitelist), &whitelist)
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: models.AgentPortPolicyDTO{
|
||||||
|
Hostname: policy.Hostname,
|
||||||
|
PortFilterMode: policy.PortFilterMode,
|
||||||
|
PortWhitelist: whitelist,
|
||||||
|
}})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SavePortPolicy 更新/创建主机的端口监控策略
|
||||||
|
// PUT /api/v1/agent/port-policies/:hostname
|
||||||
|
func (h *AgentHandler) SavePortPolicy(c *gin.Context) {
|
||||||
|
hostname := c.Param("hostname")
|
||||||
|
|
||||||
|
var dto models.AgentPortPolicyDTO
|
||||||
|
if err := c.ShouldBindJSON(&dto); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求格式错误: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
whitelistJSON := "[]"
|
||||||
|
if len(dto.PortWhitelist) > 0 {
|
||||||
|
b, _ := json.Marshal(dto.PortWhitelist)
|
||||||
|
whitelistJSON = string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
policy := models.AgentPortPolicy{
|
||||||
|
Hostname: hostname,
|
||||||
|
PortFilterMode: dto.PortFilterMode,
|
||||||
|
PortWhitelist: whitelistJSON,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.db.Where(models.AgentPortPolicy{Hostname: hostname}).
|
||||||
|
Assign(policy).
|
||||||
|
FirstOrCreate(&policy).Error; err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "保存失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "端口策略已保存"})
|
||||||
|
}
|
||||||
@@ -287,7 +287,7 @@ func (h *DashboardHandler) GetRecentReleases(c *gin.Context) {
|
|||||||
// @Success 200 {object} models.Response{data=[]models.MetricData}
|
// @Success 200 {object} models.Response{data=[]models.MetricData}
|
||||||
// @Router /dashboard/metrics [get]
|
// @Router /dashboard/metrics [get]
|
||||||
func (h *DashboardHandler) GetMetrics(c *gin.Context) {
|
func (h *DashboardHandler) GetMetrics(c *gin.Context) {
|
||||||
hours := 24
|
hours := 6 // 默认6小时,减少扫描行数
|
||||||
if hv := c.Query("hours"); hv != "" {
|
if hv := c.Query("hours"); hv != "" {
|
||||||
if parsed, err := strconv.Atoi(hv); err == nil && parsed > 0 && parsed <= 168 {
|
if parsed, err := strconv.Atoi(hv); err == nil && parsed > 0 && parsed <= 168 {
|
||||||
hours = parsed
|
hours = parsed
|
||||||
|
|||||||
@@ -1740,6 +1740,272 @@ func (h *KubernetesHandler) ListNamespaces(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: namespaces})
|
c.JSON(http.StatusOK, models.Response{Code: 200, Data: namespaces})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Namespace Management (M14 — 资源配额 + CRUD)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// NamespaceDetail is the enriched namespace info for the management page
|
||||||
|
type NamespaceDetail struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Age string `json:"age"`
|
||||||
|
Labels map[string]string `json:"labels"`
|
||||||
|
Annotations map[string]string `json:"annotations,omitempty"`
|
||||||
|
ResourceCounts map[string]int `json:"resource_counts"`
|
||||||
|
HasResourceQuota bool `json:"has_resource_quota"`
|
||||||
|
HasLimitRange bool `json:"has_limit_range"`
|
||||||
|
ResourceQuotas []k8sclient.ResourceQuotaItem `json:"resource_quotas,omitempty"`
|
||||||
|
LimitRanges []k8sclient.LimitRangeItem `json:"limit_ranges,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNamespacesDetail returns all namespaces with metadata, resource counts and quota info
|
||||||
|
func (h *KubernetesHandler) GetNamespacesDetail(c *gin.Context) {
|
||||||
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
|
if clusterID == 0 {
|
||||||
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
|
if clusterID == 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "cluster not specified"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nsList, err := client.ListNamespacesDetailed()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Efficient: fetch all resources once, group by namespace
|
||||||
|
depCounts := map[string]int{}
|
||||||
|
if deps, err := client.ListDeployments(); err == nil {
|
||||||
|
for _, d := range deps.Items {
|
||||||
|
depCounts[d.Metadata.Namespace]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
podCounts := map[string]int{}
|
||||||
|
if pods, err := client.ListPods(); err == nil {
|
||||||
|
for _, p := range pods.Items {
|
||||||
|
podCounts[p.Metadata.Namespace]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
svcCounts := map[string]int{}
|
||||||
|
if svcs, err := client.ListServices(); err == nil {
|
||||||
|
for _, s := range svcs.Items {
|
||||||
|
svcCounts[s.Metadata.Namespace]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// All-namespace ResourceQuotas
|
||||||
|
quotaByNS := map[string][]k8sclient.ResourceQuotaItem{}
|
||||||
|
if quotas, err := client.ListResourceQuotas(""); err == nil {
|
||||||
|
for _, q := range quotas {
|
||||||
|
quotaByNS[q.Metadata.Namespace] = append(quotaByNS[q.Metadata.Namespace], q)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// All-namespace LimitRanges
|
||||||
|
lrByNS := map[string][]k8sclient.LimitRangeItem{}
|
||||||
|
if lrs, err := client.ListLimitRanges(""); err == nil {
|
||||||
|
for _, lr := range lrs {
|
||||||
|
lrByNS[lr.Metadata.Namespace] = append(lrByNS[lr.Metadata.Namespace], lr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]NamespaceDetail, 0, len(nsList.Items))
|
||||||
|
for _, ns := range nsList.Items {
|
||||||
|
name := ns.Metadata.Name
|
||||||
|
nd := NamespaceDetail{
|
||||||
|
Name: name,
|
||||||
|
Status: ns.Status.Phase,
|
||||||
|
Age: formatAge(ns.Metadata.CreationTimestamp),
|
||||||
|
Labels: ns.Metadata.Labels,
|
||||||
|
Annotations: ns.Metadata.Annotations,
|
||||||
|
ResourceCounts: map[string]int{
|
||||||
|
"deployments": depCounts[name],
|
||||||
|
"pods": podCounts[name],
|
||||||
|
"services": svcCounts[name],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if nd.Labels == nil {
|
||||||
|
nd.Labels = map[string]string{}
|
||||||
|
}
|
||||||
|
if qs, ok := quotaByNS[name]; ok && len(qs) > 0 {
|
||||||
|
nd.HasResourceQuota = true
|
||||||
|
nd.ResourceQuotas = qs
|
||||||
|
}
|
||||||
|
if lrs, ok := lrByNS[name]; ok && len(lrs) > 0 {
|
||||||
|
nd.HasLimitRange = true
|
||||||
|
nd.LimitRanges = lrs
|
||||||
|
}
|
||||||
|
result = append(result, nd)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Data: result})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNamespaceDetailHandler returns a single namespace detail with quota info
|
||||||
|
func (h *KubernetesHandler) GetNamespaceDetailHandler(c *gin.Context) {
|
||||||
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
|
if clusterID == 0 {
|
||||||
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
|
if clusterID == 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "cluster not specified"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nsName := c.Param("namespace")
|
||||||
|
if nsName == "" {
|
||||||
|
nsName = c.Param("name")
|
||||||
|
}
|
||||||
|
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ns, err := client.GetNamespace(nsName)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nd := NamespaceDetail{
|
||||||
|
Name: ns.Metadata.Name,
|
||||||
|
Status: ns.Status.Phase,
|
||||||
|
Age: formatAge(ns.Metadata.CreationTimestamp),
|
||||||
|
Labels: ns.Metadata.Labels,
|
||||||
|
Annotations: ns.Metadata.Annotations,
|
||||||
|
ResourceCounts: map[string]int{},
|
||||||
|
}
|
||||||
|
if nd.Labels == nil {
|
||||||
|
nd.Labels = map[string]string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch resource counts (single namespace)
|
||||||
|
if deps, err := client.ListDeployments(); err == nil {
|
||||||
|
count := 0
|
||||||
|
for _, d := range deps.Items {
|
||||||
|
if d.Metadata.Namespace == nsName { count++ }
|
||||||
|
}
|
||||||
|
nd.ResourceCounts["deployments"] = count
|
||||||
|
}
|
||||||
|
if pods, err := client.ListPods(); err == nil {
|
||||||
|
count := 0
|
||||||
|
for _, p := range pods.Items {
|
||||||
|
if p.Metadata.Namespace == nsName { count++ }
|
||||||
|
}
|
||||||
|
nd.ResourceCounts["pods"] = count
|
||||||
|
}
|
||||||
|
if svcs, err := client.ListServices(); err == nil {
|
||||||
|
count := 0
|
||||||
|
for _, s := range svcs.Items {
|
||||||
|
if s.Metadata.Namespace == nsName { count++ }
|
||||||
|
}
|
||||||
|
nd.ResourceCounts["services"] = count
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch quotas + limit ranges for this namespace
|
||||||
|
if quotas, err := client.ListResourceQuotas(nsName); err == nil && len(quotas) > 0 {
|
||||||
|
nd.HasResourceQuota = true
|
||||||
|
nd.ResourceQuotas = quotas
|
||||||
|
}
|
||||||
|
if lrs, err := client.ListLimitRanges(nsName); err == nil && len(lrs) > 0 {
|
||||||
|
nd.HasLimitRange = true
|
||||||
|
nd.LimitRanges = lrs
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Data: nd})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateNamespaceHandler creates a new namespace
|
||||||
|
func (h *KubernetesHandler) CreateNamespaceHandler(c *gin.Context) {
|
||||||
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
|
if clusterID == 0 {
|
||||||
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
|
if clusterID == 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "cluster not specified"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
Name string `json:"name" binding:"required"`
|
||||||
|
Labels map[string]string `json:"labels"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "name is required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safety: block critical system namespaces
|
||||||
|
systemNS := map[string]bool{
|
||||||
|
"kube-system": true, "kube-public": true, "kube-node-lease": true,
|
||||||
|
"default": true, "kubernetes-dashboard": true,
|
||||||
|
}
|
||||||
|
if systemNS[req.Name] {
|
||||||
|
c.JSON(http.StatusForbidden, models.Response{Code: 403, Message: "cannot create system namespace: " + req.Name})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ns, err := client.CreateNamespace(req.Name, req.Labels)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Data: ns, Message: "命名空间创建成功"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteNamespaceHandler deletes a namespace
|
||||||
|
func (h *KubernetesHandler) DeleteNamespaceHandler(c *gin.Context) {
|
||||||
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
|
if clusterID == 0 {
|
||||||
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
|
if clusterID == 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "cluster not specified"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nsName := c.Param("namespace")
|
||||||
|
if nsName == "" {
|
||||||
|
nsName = c.Param("name")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safety: block deletion of critical system namespaces
|
||||||
|
systemNS := map[string]bool{
|
||||||
|
"kube-system": true, "kube-public": true, "kube-node-lease": true,
|
||||||
|
"default": true, "devops": true,
|
||||||
|
}
|
||||||
|
if systemNS[nsName] {
|
||||||
|
c.JSON(http.StatusForbidden, models.Response{Code: 403, Message: "禁止删除系统命名空间: " + nsName})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.DeleteNamespace(nsName); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "命名空间已删除"})
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// ConfigMap & Secret (Kuboard-style namespace resources)
|
// ConfigMap & Secret (Kuboard-style namespace resources)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -389,6 +389,11 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
|||||||
|
|
||||||
// Namespace list
|
// Namespace list
|
||||||
kubernetesRouter.GET("/:clusterCode/namespaces", k8sHandler.ListNamespaces)
|
kubernetesRouter.GET("/:clusterCode/namespaces", k8sHandler.ListNamespaces)
|
||||||
|
// 🆕 M14: Namespace 管理看板 (detail + CRUD)
|
||||||
|
kubernetesRouter.GET("/:clusterCode/namespaces/detail", k8sHandler.GetNamespacesDetail)
|
||||||
|
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/detail", k8sHandler.GetNamespaceDetailHandler)
|
||||||
|
kubernetesRouter.POST("/:clusterCode/namespaces", k8sHandler.CreateNamespaceHandler)
|
||||||
|
kubernetesRouter.DELETE("/:clusterCode/namespaces/:namespace", k8sHandler.DeleteNamespaceHandler)
|
||||||
// All-namespace listing (no ns filter)
|
// All-namespace listing (no ns filter)
|
||||||
kubernetesRouter.GET("/:clusterCode/deployments", k8sHandler.ListDeploymentsAllNS)
|
kubernetesRouter.GET("/:clusterCode/deployments", k8sHandler.ListDeploymentsAllNS)
|
||||||
kubernetesRouter.GET("/:clusterCode/pods", k8sHandler.ListPodsAllNS)
|
kubernetesRouter.GET("/:clusterCode/pods", k8sHandler.ListPodsAllNS)
|
||||||
@@ -411,6 +416,11 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
|||||||
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/pvcs", k8sHandler.ListPVCsNS)
|
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/pvcs", k8sHandler.ListPVCsNS)
|
||||||
kubernetesRouter.DELETE("/:clusterCode/namespace/:namespace/pvcs/:name", k8sHandler.DeletePVCNS)
|
kubernetesRouter.DELETE("/:clusterCode/namespace/:namespace/pvcs/:name", k8sHandler.DeletePVCNS)
|
||||||
|
|
||||||
|
// RBAC — ServiceAccount / Role / RoleBinding
|
||||||
|
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/serviceaccounts", k8sHandler.ListServiceAccountsNS)
|
||||||
|
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/roles", k8sHandler.ListRolesNS)
|
||||||
|
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/rolebindings", k8sHandler.ListRoleBindingsNS)
|
||||||
|
|
||||||
// StatefulSet / DaemonSet
|
// StatefulSet / DaemonSet
|
||||||
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/statefulsets", k8sHandler.ListStatefulSetsNS)
|
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/statefulsets", k8sHandler.ListStatefulSetsNS)
|
||||||
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/daemonsets", k8sHandler.ListDaemonSetsNS)
|
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/daemonsets", k8sHandler.ListDaemonSetsNS)
|
||||||
@@ -676,6 +686,8 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
|||||||
agentGroup.POST("/exec", agentInstallHandler.RemoteExec)
|
agentGroup.POST("/exec", agentInstallHandler.RemoteExec)
|
||||||
// 健康探针:Agent 拉取本机配置(无需 JWT)
|
// 健康探针:Agent 拉取本机配置(无需 JWT)
|
||||||
agentGroup.GET("/health-probes/config", agentHandler.GetHealthProbesForAgent)
|
agentGroup.GET("/health-probes/config", agentHandler.GetHealthProbesForAgent)
|
||||||
|
// 端口策略:Agent 拉取本机端口过滤策略(无需 JWT)
|
||||||
|
agentGroup.GET("/port-policy", agentHandler.GetPortPolicyForAgent)
|
||||||
}
|
}
|
||||||
// Agent 数据查询 + 任务管理(需要 JWT)
|
// Agent 数据查询 + 任务管理(需要 JWT)
|
||||||
agentQuery := authorized.Group("/agent")
|
agentQuery := authorized.Group("/agent")
|
||||||
@@ -710,6 +722,10 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
|||||||
agentQuery.POST("/health-probes", agentHandler.CreateHealthProbe)
|
agentQuery.POST("/health-probes", agentHandler.CreateHealthProbe)
|
||||||
agentQuery.PUT("/health-probes/:id", agentHandler.UpdateHealthProbe)
|
agentQuery.PUT("/health-probes/:id", agentHandler.UpdateHealthProbe)
|
||||||
agentQuery.DELETE("/health-probes/:id", agentHandler.DeleteHealthProbe)
|
agentQuery.DELETE("/health-probes/:id", agentHandler.DeleteHealthProbe)
|
||||||
|
|
||||||
|
// 端口策略(CRUD)
|
||||||
|
agentQuery.GET("/port-policies/:hostname", agentHandler.GetPortPolicy)
|
||||||
|
agentQuery.PUT("/port-policies/:hostname", agentHandler.SavePortPolicy)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Agent 二进制下载(无需鉴权,Agent 升级时使用)
|
// Agent 二进制下载(无需鉴权,Agent 升级时使用)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ type AgentReport struct {
|
|||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
Hostname string `gorm:"size:255;index;not null" json:"hostname"`
|
Hostname string `gorm:"size:255;index;not null" json:"hostname"`
|
||||||
CPUUsage float64 `gorm:"type:decimal(6,2)" json:"cpu_usage"`
|
CPUUsage float64 `gorm:"type:decimal(6,2)" json:"cpu_usage"`
|
||||||
|
CPUIOwait float64 `gorm:"type:decimal(6,2)" json:"cpu_iowait"`
|
||||||
CPUCores int `json:"cpu_cores"`
|
CPUCores int `json:"cpu_cores"`
|
||||||
Load1 float64 `gorm:"type:decimal(8,2)" json:"load1"`
|
Load1 float64 `gorm:"type:decimal(8,2)" json:"load1"`
|
||||||
Load5 float64 `gorm:"type:decimal(8,2)" json:"load5"`
|
Load5 float64 `gorm:"type:decimal(8,2)" json:"load5"`
|
||||||
@@ -16,6 +17,8 @@ type AgentReport struct {
|
|||||||
MemTotal float64 `gorm:"type:decimal(10,2)" json:"mem_total"`
|
MemTotal float64 `gorm:"type:decimal(10,2)" json:"mem_total"`
|
||||||
MemUsed float64 `gorm:"type:decimal(10,2)" json:"mem_used"`
|
MemUsed float64 `gorm:"type:decimal(10,2)" json:"mem_used"`
|
||||||
MemUsage float64 `gorm:"type:decimal(6,2)" json:"mem_usage"`
|
MemUsage float64 `gorm:"type:decimal(6,2)" json:"mem_usage"`
|
||||||
|
MemAvailable float64 `gorm:"type:decimal(10,2)" json:"mem_available"`
|
||||||
|
MemSwapUsed float64 `gorm:"type:decimal(10,2)" json:"mem_swap_used"`
|
||||||
DiskInfo string `gorm:"type:text" json:"disk_info"`
|
DiskInfo string `gorm:"type:text" json:"disk_info"`
|
||||||
PortInfo string `gorm:"type:text" json:"port_info"`
|
PortInfo string `gorm:"type:text" json:"port_info"`
|
||||||
NetInfo string `gorm:"type:text" json:"net_info"`
|
NetInfo string `gorm:"type:text" json:"net_info"`
|
||||||
@@ -141,6 +144,7 @@ type SystemStatModel struct {
|
|||||||
|
|
||||||
type CPUMetric struct {
|
type CPUMetric struct {
|
||||||
UsagePercent float64 `json:"usage_percent"`
|
UsagePercent float64 `json:"usage_percent"`
|
||||||
|
IOWait float64 `json:"iowait"`
|
||||||
CoreCount int `json:"core_count"`
|
CoreCount int `json:"core_count"`
|
||||||
Load1 float64 `json:"load1"`
|
Load1 float64 `json:"load1"`
|
||||||
Load5 float64 `json:"load5"`
|
Load5 float64 `json:"load5"`
|
||||||
@@ -151,15 +155,18 @@ type MemMetric struct {
|
|||||||
TotalGB float64 `json:"total_gb"`
|
TotalGB float64 `json:"total_gb"`
|
||||||
UsedGB float64 `json:"used_gb"`
|
UsedGB float64 `json:"used_gb"`
|
||||||
CachedGB float64 `json:"cached_gb"`
|
CachedGB float64 `json:"cached_gb"`
|
||||||
|
AvailableGB float64 `json:"available_gb"`
|
||||||
|
SwapUsedGB float64 `json:"swap_used_gb"`
|
||||||
UsagePercent float64 `json:"usage_percent"`
|
UsagePercent float64 `json:"usage_percent"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DiskMetric struct {
|
type DiskMetric struct {
|
||||||
Mount string `json:"mount"`
|
Mount string `json:"mount"`
|
||||||
Device string `json:"device"`
|
Device string `json:"device"`
|
||||||
TotalGB float64 `json:"total_gb"`
|
TotalGB float64 `json:"total_gb"`
|
||||||
UsedGB float64 `json:"used_gb"`
|
UsedGB float64 `json:"used_gb"`
|
||||||
UsagePercent float64 `json:"usage_percent"`
|
UsagePercent float64 `json:"usage_percent"`
|
||||||
|
InodeUsagePct float64 `json:"inode_usage_pct"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PortMetric struct {
|
type PortMetric struct {
|
||||||
@@ -177,6 +184,8 @@ type NetworkStatModel struct {
|
|||||||
NicName string `json:"nic_name"`
|
NicName string `json:"nic_name"`
|
||||||
NetRxBPS float64 `json:"net_rx_bps"`
|
NetRxBPS float64 `json:"net_rx_bps"`
|
||||||
NetTxBPS float64 `json:"net_tx_bps"`
|
NetTxBPS float64 `json:"net_tx_bps"`
|
||||||
|
NetRxErrors int64 `json:"net_rx_errors"`
|
||||||
|
NetTxErrors int64 `json:"net_tx_errors"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DiskIOStatModel struct {
|
type DiskIOStatModel struct {
|
||||||
@@ -184,4 +193,25 @@ type DiskIOStatModel struct {
|
|||||||
WriteBPS float64 `json:"write_bps"`
|
WriteBPS float64 `json:"write_bps"`
|
||||||
ReadIOPS float64 `json:"read_iops"`
|
ReadIOPS float64 `json:"read_iops"`
|
||||||
WriteIOPS float64 `json:"write_iops"`
|
WriteIOPS float64 `json:"write_iops"`
|
||||||
|
Util float64 `json:"util"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AgentPortPolicy 主机端口监控策略 // Logan.Li
|
||||||
|
type AgentPortPolicy struct {
|
||||||
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
|
Hostname string `gorm:"size:255;uniqueIndex;not null" json:"hostname"`
|
||||||
|
PortFilterMode string `gorm:"size:20;default:whitelist;not null" json:"port_filter_mode"` // whitelist / blacklist
|
||||||
|
PortWhitelist string `gorm:"type:text" json:"port_whitelist"` // JSON 数组,如 [80,443,3306,6379]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (AgentPortPolicy) TableName() string {
|
||||||
|
return "monitor_agent_port_policy"
|
||||||
|
}
|
||||||
|
|
||||||
|
// AgentPortPolicyDTO 前端交互用的端口策略格式(含已解析的数组)
|
||||||
|
type AgentPortPolicyDTO struct {
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
PortFilterMode string `json:"port_filter_mode"`
|
||||||
|
PortWhitelist []int `json:"port_whitelist"`
|
||||||
|
}
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ type AgentRule struct {
|
|||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
Name string `gorm:"size:255;not null" json:"name"`
|
Name string `gorm:"size:255;not null" json:"name"`
|
||||||
Hostname string `gorm:"size:255" json:"hostname"` // 空表示全部主机
|
Hostname string `gorm:"size:255" json:"hostname"` // 空表示全部主机
|
||||||
Metric string `gorm:"size:32;not null" json:"metric"` // cpu/mem/disk/offline/port_down/tcp_time_wait/tcp_close_wait/service_process
|
Metric string `gorm:"size:32;not null" json:"metric"` // cpu/mem/disk/offline/port_down/tcp_time_wait/tcp_close_wait/service_process/fd
|
||||||
Operator string `gorm:"size:10;default:>" json:"operator"` // > >= < <= ==
|
Operator string `gorm:"size:10;default:>" json:"operator"` // > >= < <= ==
|
||||||
Threshold float64 `gorm:"type:decimal(10,2)" json:"threshold"`
|
Threshold float64 `gorm:"type:decimal(10,2)" json:"threshold"`
|
||||||
Port int `json:"port"` // port_down 专用
|
Port int `json:"port"` // port_down 专用
|
||||||
|
|||||||
@@ -1023,6 +1023,134 @@ func (c *Client) ListNamespaces() ([]string, error) {
|
|||||||
return names, nil
|
return names, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NamespaceItem holds full namespace metadata
|
||||||
|
type NamespaceItem struct {
|
||||||
|
Metadata ObjectMeta `json:"metadata"`
|
||||||
|
Spec NamespaceSpec `json:"spec"`
|
||||||
|
Status NamespaceStatus `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NamespaceSpec holds namespace spec
|
||||||
|
type NamespaceSpec struct {
|
||||||
|
Finalizers []string `json:"finalizers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NamespaceStatus holds namespace phase
|
||||||
|
type NamespaceStatus struct {
|
||||||
|
Phase string `json:"phase"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NamespaceList wraps a list of NamespaceItem
|
||||||
|
type NamespaceList struct {
|
||||||
|
Items []NamespaceItem `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListNamespacesDetailed returns all namespaces with full metadata
|
||||||
|
func (c *Client) ListNamespacesDetailed() (*NamespaceList, error) {
|
||||||
|
var result NamespaceList
|
||||||
|
if err := c.get("/api/v1/namespaces", &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNamespace returns a single namespace by name
|
||||||
|
func (c *Client) GetNamespace(name string) (*NamespaceItem, error) {
|
||||||
|
var result NamespaceItem
|
||||||
|
path := fmt.Sprintf("/api/v1/namespaces/%s", name)
|
||||||
|
if err := c.get(path, &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateNamespace creates a new namespace
|
||||||
|
func (c *Client) CreateNamespace(name string, labels map[string]string) (*NamespaceItem, error) {
|
||||||
|
body := map[string]interface{}{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Namespace",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": name,
|
||||||
|
"labels": labels,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
var result NamespaceItem
|
||||||
|
if err := c.post("/api/v1/namespaces", body, &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteNamespace deletes a namespace by name
|
||||||
|
func (c *Client) DeleteNamespace(name string) error {
|
||||||
|
path := fmt.Sprintf("/api/v1/namespaces/%s", name)
|
||||||
|
return c.delete(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceQuotaItem represents a K8s ResourceQuota
|
||||||
|
type ResourceQuotaItem struct {
|
||||||
|
Metadata ObjectMeta `json:"metadata"`
|
||||||
|
Spec ResourceQuotaSpec `json:"spec"`
|
||||||
|
Status ResourceQuotaStatus `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceQuotaSpec holds hard limits
|
||||||
|
type ResourceQuotaSpec struct {
|
||||||
|
Hard map[string]string `json:"hard"`
|
||||||
|
Scopes []string `json:"scopes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceQuotaStatus holds used/hard counts
|
||||||
|
type ResourceQuotaStatus struct {
|
||||||
|
Hard map[string]string `json:"hard"`
|
||||||
|
Used map[string]string `json:"used"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListResourceQuotas returns ResourceQuotas in a namespace, or all namespaces if ns is empty
|
||||||
|
func (c *Client) ListResourceQuotas(namespace string) ([]ResourceQuotaItem, error) {
|
||||||
|
var result struct {
|
||||||
|
Items []ResourceQuotaItem `json:"items"`
|
||||||
|
}
|
||||||
|
var path string
|
||||||
|
if namespace == "" {
|
||||||
|
path = "/api/v1/resourcequotas"
|
||||||
|
} else {
|
||||||
|
path = fmt.Sprintf("/api/v1/namespaces/%s/resourcequotas", namespace)
|
||||||
|
}
|
||||||
|
if err := c.get(path, &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result.Items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LimitRangeItem represents a K8s LimitRange
|
||||||
|
type LimitRangeItem struct {
|
||||||
|
Metadata ObjectMeta `json:"metadata"`
|
||||||
|
Spec LimitRangeSpec `json:"spec"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LimitRangeSpec holds limit items
|
||||||
|
type LimitRangeSpec struct {
|
||||||
|
Limits []map[string]interface{} `json:"limits"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListLimitRanges returns LimitRanges in a namespace, or all namespaces if ns is empty
|
||||||
|
func (c *Client) ListLimitRanges(namespace string) ([]LimitRangeItem, error) {
|
||||||
|
var result struct {
|
||||||
|
Items []LimitRangeItem `json:"items"`
|
||||||
|
}
|
||||||
|
var path string
|
||||||
|
if namespace == "" {
|
||||||
|
path = "/api/v1/limitranges"
|
||||||
|
} else {
|
||||||
|
path = fmt.Sprintf("/api/v1/namespaces/%s/limitranges", namespace)
|
||||||
|
}
|
||||||
|
if err := c.get(path, &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result.Items, nil
|
||||||
|
}
|
||||||
|
|
||||||
// --- ConfigMap ---
|
// --- ConfigMap ---
|
||||||
|
|
||||||
// ConfigMapList response
|
// ConfigMapList response
|
||||||
@@ -1366,6 +1494,110 @@ func (c *Client) ListEvents(namespace string) (*EventList, error) {
|
|||||||
return &result, nil
|
return &result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// RBAC — ServiceAccount / Role / RoleBinding
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// --- ServiceAccount ---
|
||||||
|
|
||||||
|
type ServiceAccountList struct {
|
||||||
|
Items []ServiceAccountItem `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceAccountItem struct {
|
||||||
|
Metadata ObjectMeta `json:"metadata"`
|
||||||
|
Spec struct {
|
||||||
|
ImagePullSecrets []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"imagePullSecrets"`
|
||||||
|
Secrets []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"secrets"`
|
||||||
|
} `json:"secrets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceAccountRaw struct {
|
||||||
|
Metadata ObjectMeta `json:"metadata"`
|
||||||
|
Secrets []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"secrets"`
|
||||||
|
ImagePullSecrets []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"imagePullSecrets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListServiceAccounts fetches ServiceAccounts in a namespace
|
||||||
|
func (c *Client) ListServiceAccounts(namespace string) ([]ServiceAccountRaw, error) {
|
||||||
|
var result struct {
|
||||||
|
Items []ServiceAccountRaw `json:"items"`
|
||||||
|
}
|
||||||
|
path := fmt.Sprintf("/api/v1/namespaces/%s/serviceaccounts", namespace)
|
||||||
|
if err := c.get(path, &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result.Items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Role ---
|
||||||
|
|
||||||
|
type RoleList struct {
|
||||||
|
Items []RoleItem `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RoleItem struct {
|
||||||
|
Metadata ObjectMeta `json:"metadata"`
|
||||||
|
Rules []PolicyRule `json:"rules"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PolicyRule struct {
|
||||||
|
Verbs []string `json:"verbs"`
|
||||||
|
Resources []string `json:"resources"`
|
||||||
|
APIGroups []string `json:"apiGroups"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListRoles fetches Roles in a namespace
|
||||||
|
func (c *Client) ListRoles(namespace string) (*RoleList, error) {
|
||||||
|
var result RoleList
|
||||||
|
path := fmt.Sprintf("/apis/rbac.authorization.k8s.io/v1/namespaces/%s/roles", namespace)
|
||||||
|
if err := c.get(path, &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- RoleBinding ---
|
||||||
|
|
||||||
|
type RoleBindingList struct {
|
||||||
|
Items []RoleBindingItem `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RoleBindingItem struct {
|
||||||
|
Metadata ObjectMeta `json:"metadata"`
|
||||||
|
RoleRef RoleRef `json:"roleRef"`
|
||||||
|
Subjects []Subject `json:"subjects"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RoleRef struct {
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Subject struct {
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListRoleBindings fetches RoleBindings in a namespace
|
||||||
|
func (c *Client) ListRoleBindings(namespace string) (*RoleBindingList, error) {
|
||||||
|
var result RoleBindingList
|
||||||
|
path := fmt.Sprintf("/apis/rbac.authorization.k8s.io/v1/namespaces/%s/rolebindings", namespace)
|
||||||
|
if err := c.get(path, &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// YAML Apply (Dynamic resource apply)
|
// YAML Apply (Dynamic resource apply)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const KubernetesClusters = lazy(() => import('@/components/kubernetes/Kubernetes
|
|||||||
const ClusterOverviewPage = lazy(() => import('@/components/kubernetes/ClusterOverviewPage').then(m => ({ default: m.default })));
|
const ClusterOverviewPage = lazy(() => import('@/components/kubernetes/ClusterOverviewPage').then(m => ({ default: m.default })));
|
||||||
const NodeDetailPage = lazy(() => import('@/components/kubernetes/NodeDetailPage').then(m => ({ default: m.default })));
|
const NodeDetailPage = lazy(() => import('@/components/kubernetes/NodeDetailPage').then(m => ({ default: m.default })));
|
||||||
const NamespaceOverviewPage = lazy(() => import('@/components/kubernetes/NamespaceOverviewPage').then(m => ({ default: m.default })));
|
const NamespaceOverviewPage = lazy(() => import('@/components/kubernetes/NamespaceOverviewPage').then(m => ({ default: m.default })));
|
||||||
|
const NamespaceManagementPage = lazy(() => import('@/components/kubernetes/NamespaceManagementPage').then(m => ({ default: m.default })));
|
||||||
const NamespaceResourcePage = lazy(() => import('@/components/kubernetes/NamespaceResourcePage').then(m => ({ default: m.default })));
|
const NamespaceResourcePage = lazy(() => import('@/components/kubernetes/NamespaceResourcePage').then(m => ({ default: m.default })));
|
||||||
const DeploymentDetailPage = lazy(() => import('@/components/kubernetes/DeploymentDetailPage').then(m => ({ default: m.default })));
|
const DeploymentDetailPage = lazy(() => import('@/components/kubernetes/DeploymentDetailPage').then(m => ({ default: m.default })));
|
||||||
const WorkloadViewPage = lazy(() => import('@/components/kubernetes/WorkloadViewPage').then(m => ({ default: m.default })));
|
const WorkloadViewPage = lazy(() => import('@/components/kubernetes/WorkloadViewPage').then(m => ({ default: m.default })));
|
||||||
@@ -121,6 +122,7 @@ function App() {
|
|||||||
<Route path="metrics/pods" element={<LazyPage><PodMetricsPage /></LazyPage>} />
|
<Route path="metrics/pods" element={<LazyPage><PodMetricsPage /></LazyPage>} />
|
||||||
<Route path="events" element={<LazyPage><EventsPage /></LazyPage>} />
|
<Route path="events" element={<LazyPage><EventsPage /></LazyPage>} />
|
||||||
<Route path="capacity" element={<LazyPage><CapacityPage /></LazyPage>} />
|
<Route path="capacity" element={<LazyPage><CapacityPage /></LazyPage>} />
|
||||||
|
<Route path="namespaces" element={<LazyPage><NamespaceManagementPage /></LazyPage>} />
|
||||||
<Route path="nodes/:nodeName" element={<LazyPage><NodeDetailPage /></LazyPage>} />
|
<Route path="nodes/:nodeName" element={<LazyPage><NodeDetailPage /></LazyPage>} />
|
||||||
<Route path="topology" element={<LazyPage><TopologyView /></LazyPage>} />
|
<Route path="topology" element={<LazyPage><TopologyView /></LazyPage>} />
|
||||||
<Route path="namespace/:namespace">
|
<Route path="namespace/:namespace">
|
||||||
|
|||||||
@@ -85,8 +85,8 @@ export function Dashboard() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refreshData();
|
refreshData();
|
||||||
// 每 5 分钟自动刷新
|
// 每 5 秒自动刷新
|
||||||
const interval = setInterval(refreshData, 5 * 60 * 1000);
|
const interval = setInterval(refreshData, 5000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [refreshData]);
|
}, [refreshData]);
|
||||||
|
|
||||||
|
|||||||
@@ -202,9 +202,14 @@ export default function ClusterOverviewPage() {
|
|||||||
|
|
||||||
{/* Namespace list */}
|
{/* Namespace list */}
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-slate-100 mb-3 flex items-center gap-2">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<Boxes className="h-4 w-4 text-emerald-400" />命名空间
|
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
|
||||||
</h2>
|
<Boxes className="h-4 w-4 text-emerald-400" />命名空间
|
||||||
|
</h2>
|
||||||
|
<Link to={`/kubernetes/${clusterCode}/namespaces`} className="text-sm text-blue-400 hover:text-blue-300 transition-colors flex items-center gap-1">
|
||||||
|
管理全部 <ChevronRight className="h-3 w-3" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||||
{namespaces.map((ns) => {
|
{namespaces.map((ns) => {
|
||||||
const Icon = NAMESPACE_ICON[ns] || Container;
|
const Icon = NAMESPACE_ICON[ns] || Container;
|
||||||
|
|||||||
@@ -0,0 +1,336 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import {
|
||||||
|
Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
|
||||||
|
} from '@/components/ui/table';
|
||||||
|
import {
|
||||||
|
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogTrigger,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import {
|
||||||
|
RefreshCw, Loader2, ChevronRight, Plus, Trash2, Boxes, Container, Network,
|
||||||
|
Shield, Activity, AlertTriangle, CheckCircle2,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { kubernetesApi } from '@/services/api';
|
||||||
|
|
||||||
|
interface NamespaceDetail {
|
||||||
|
name: string;
|
||||||
|
status: string;
|
||||||
|
age: string;
|
||||||
|
labels: Record<string, string>;
|
||||||
|
resource_counts: Record<string, number>;
|
||||||
|
has_resource_quota: boolean;
|
||||||
|
has_limit_range: boolean;
|
||||||
|
resource_quotas?: Array<{
|
||||||
|
metadata: { name: string };
|
||||||
|
status: { hard: Record<string, string>; used: Record<string, string> };
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SYSTEM_NAMESPACES = new Set([
|
||||||
|
'kube-system', 'kube-public', 'kube-node-lease', 'default', 'devops',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default function NamespaceManagementPage() {
|
||||||
|
const { clusterCode } = useParams<{ clusterCode: string }>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [namespaces, setNamespaces] = useState<NamespaceDetail[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
|
// Create dialog state
|
||||||
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
|
const [newName, setNewName] = useState('');
|
||||||
|
const [creating, setCreating] = useState(false);
|
||||||
|
|
||||||
|
// Delete dialog state
|
||||||
|
const [deleteTarget, setDeleteTarget] = useState<string | null>(null);
|
||||||
|
const [deleting, setDeleting] = useState(false);
|
||||||
|
|
||||||
|
const loadData = useCallback(async () => {
|
||||||
|
if (!clusterCode) return;
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await kubernetesApi.getNamespacesDetail(clusterCode);
|
||||||
|
setNamespaces(Array.isArray(data) ? data : []);
|
||||||
|
} catch {
|
||||||
|
setNamespaces([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [clusterCode]);
|
||||||
|
|
||||||
|
useEffect(() => { loadData(); }, [loadData]);
|
||||||
|
|
||||||
|
const handleCreate = async () => {
|
||||||
|
if (!clusterCode || !newName.trim()) return;
|
||||||
|
setCreating(true);
|
||||||
|
try {
|
||||||
|
await kubernetesApi.createNamespace(clusterCode, newName.trim());
|
||||||
|
setNewName('');
|
||||||
|
setCreateOpen(false);
|
||||||
|
await loadData();
|
||||||
|
} catch (err: any) {
|
||||||
|
alert(err?.message || '创建失败');
|
||||||
|
} finally {
|
||||||
|
setCreating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (!clusterCode || !deleteTarget) return;
|
||||||
|
setDeleting(true);
|
||||||
|
try {
|
||||||
|
await kubernetesApi.deleteNamespace(clusterCode, deleteTarget);
|
||||||
|
setDeleteTarget(null);
|
||||||
|
await loadData();
|
||||||
|
} catch (err: any) {
|
||||||
|
alert(err?.message || '删除失败');
|
||||||
|
} finally {
|
||||||
|
setDeleting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filtered = namespaces.filter(ns =>
|
||||||
|
!searchQuery || ns.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalDeploys = namespaces.reduce((s, n) => s + (n.resource_counts?.deployments || 0), 0);
|
||||||
|
const totalPods = namespaces.reduce((s, n) => s + (n.resource_counts?.pods || 0), 0);
|
||||||
|
const totalSvcs = namespaces.reduce((s, n) => s + (n.resource_counts?.services || 0), 0);
|
||||||
|
const quotaCount = namespaces.filter(n => n.has_resource_quota).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Breadcrumb */}
|
||||||
|
<div className="flex items-center gap-2 text-sm text-slate-500">
|
||||||
|
<Link to="/kubernetes" className="hover:text-slate-300 transition-colors">集群</Link>
|
||||||
|
<ChevronRight className="h-3 w-3" />
|
||||||
|
<Link to={`/kubernetes/${clusterCode}`} className="hover:text-slate-300 transition-colors">{clusterCode}</Link>
|
||||||
|
<ChevronRight className="h-3 w-3" />
|
||||||
|
<span className="text-slate-300">命名空间管理</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-slate-100 flex items-center gap-2">
|
||||||
|
<Boxes className="h-6 w-6 text-emerald-400" />
|
||||||
|
命名空间管理
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-slate-500 mt-1">命名空间资源配额与生命周期管理</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button variant="outline" size="icon" className="h-9 w-9 border-slate-700 text-slate-400"
|
||||||
|
onClick={loadData} disabled={loading}>
|
||||||
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <RefreshCw className="h-4 w-4" />}
|
||||||
|
</Button>
|
||||||
|
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button className="bg-blue-600 hover:bg-blue-700">
|
||||||
|
<Plus className="h-4 w-4 mr-1" />创建命名空间
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>创建命名空间</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>命名空间名称</Label>
|
||||||
|
<Input
|
||||||
|
value={newName}
|
||||||
|
onChange={(e) => setNewName(e.target.value)}
|
||||||
|
placeholder="例如: my-app"
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && handleCreate()}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-slate-500">
|
||||||
|
名称需符合 DNS-1123 标准(小写字母/数字/连字符)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setCreateOpen(false)}>取消</Button>
|
||||||
|
<Button onClick={handleCreate} disabled={creating || !newName.trim()}>
|
||||||
|
{creating ? <Loader2 className="h-4 w-4 animate-spin mr-1" /> : null}
|
||||||
|
创建
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats cards */}
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
|
||||||
|
<Card className="bg-slate-900 border-slate-800">
|
||||||
|
<CardContent className="p-4 flex items-center justify-between">
|
||||||
|
<div><p className="text-xs text-slate-500">命名空间</p><p className="text-xl font-bold text-emerald-400">{namespaces.length}</p></div>
|
||||||
|
<Boxes className="h-6 w-6 opacity-30 text-emerald-400" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="bg-slate-900 border-slate-800">
|
||||||
|
<CardContent className="p-4 flex items-center justify-between">
|
||||||
|
<div><p className="text-xs text-slate-500">Deployments</p><p className="text-xl font-bold text-blue-400">{totalDeploys}</p></div>
|
||||||
|
<Container className="h-6 w-6 opacity-30 text-blue-400" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="bg-slate-900 border-slate-800">
|
||||||
|
<CardContent className="p-4 flex items-center justify-between">
|
||||||
|
<div><p className="text-xs text-slate-500">Pods</p><p className="text-xl font-bold text-purple-400">{totalPods}</p></div>
|
||||||
|
<Activity className="h-6 w-6 opacity-30 text-purple-400" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="bg-slate-900 border-slate-800">
|
||||||
|
<CardContent className="p-4 flex items-center justify-between">
|
||||||
|
<div><p className="text-xs text-slate-500">Services</p><p className="text-xl font-bold text-cyan-400">{totalSvcs}</p></div>
|
||||||
|
<Network className="h-6 w-6 opacity-30 text-cyan-400" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="bg-slate-900 border-slate-800">
|
||||||
|
<CardContent className="p-4 flex items-center justify-between">
|
||||||
|
<div><p className="text-xs text-slate-500">配额限制</p><p className="text-xl font-bold text-amber-400">{quotaCount}</p></div>
|
||||||
|
<Shield className="h-6 w-6 opacity-30 text-amber-400" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search */}
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Input
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
placeholder="搜索命名空间..."
|
||||||
|
className="max-w-xs bg-slate-900 border-slate-700"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Namespace table */}
|
||||||
|
<Card className="bg-slate-900 border-slate-800">
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="border-slate-800 hover:bg-transparent">
|
||||||
|
<TableHead className="text-slate-400">名称</TableHead>
|
||||||
|
<TableHead className="text-slate-400">状态</TableHead>
|
||||||
|
<TableHead className="text-slate-400">年龄</TableHead>
|
||||||
|
<TableHead className="text-slate-400 text-center">Deploy</TableHead>
|
||||||
|
<TableHead className="text-slate-400 text-center">Pods</TableHead>
|
||||||
|
<TableHead className="text-slate-400 text-center">Svc</TableHead>
|
||||||
|
<TableHead className="text-slate-400 text-center">配额</TableHead>
|
||||||
|
<TableHead className="text-slate-400">标签</TableHead>
|
||||||
|
<TableHead className="text-slate-400 text-center">操作</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{loading ? (
|
||||||
|
<TableRow><TableCell colSpan={9} className="text-center text-slate-500 py-12">
|
||||||
|
<Loader2 className="h-5 w-5 animate-spin mx-auto mb-2" />加载中...
|
||||||
|
</TableCell></TableRow>
|
||||||
|
) : filtered.length === 0 ? (
|
||||||
|
<TableRow><TableCell colSpan={9} className="text-center text-slate-500 py-12">
|
||||||
|
<Boxes className="h-8 w-8 mx-auto mb-2 opacity-50" />暂无命名空间
|
||||||
|
</TableCell></TableRow>
|
||||||
|
) : (
|
||||||
|
filtered.map((ns) => {
|
||||||
|
const isSystem = SYSTEM_NAMESPACES.has(ns.name);
|
||||||
|
return (
|
||||||
|
<TableRow key={ns.name} className="border-slate-800 hover:bg-slate-800/30">
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
<button
|
||||||
|
className="text-blue-400 hover:text-blue-300 hover:underline text-left"
|
||||||
|
onClick={() => navigate(`/kubernetes/${clusterCode}/namespace/${ns.name}`)}
|
||||||
|
>
|
||||||
|
{ns.name}
|
||||||
|
</button>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge className={ns.status === 'Active' ? 'bg-emerald-500/20 text-emerald-400' : 'bg-red-500/20 text-red-400'}>
|
||||||
|
{ns.status === 'Active' ? <CheckCircle2 className="h-3 w-3 mr-1" /> : <AlertTriangle className="h-3 w-3 mr-1" />}
|
||||||
|
{ns.status}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-sm text-slate-400">{ns.age}</TableCell>
|
||||||
|
<TableCell className="text-center text-sm text-slate-300">{ns.resource_counts?.deployments ?? 0}</TableCell>
|
||||||
|
<TableCell className="text-center text-sm text-slate-300">{ns.resource_counts?.pods ?? 0}</TableCell>
|
||||||
|
<TableCell className="text-center text-sm text-slate-300">{ns.resource_counts?.services ?? 0}</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
{ns.has_resource_quota ? (
|
||||||
|
<Badge className="bg-amber-500/20 text-amber-400" title={JSON.stringify(ns.resource_quotas?.[0]?.status)}>
|
||||||
|
<Shield className="h-3 w-3 mr-1" />有
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-slate-600">-</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-xs">
|
||||||
|
{ns.labels && Object.keys(ns.labels).length > 0 ? (
|
||||||
|
<div className="flex flex-wrap gap-1 max-w-[200px]">
|
||||||
|
{Object.entries(ns.labels).slice(0, 3).map(([k, v]) => (
|
||||||
|
<span key={k} className="px-1.5 py-0.5 bg-slate-800 rounded text-slate-400">{k}={v}</span>
|
||||||
|
))}
|
||||||
|
{Object.keys(ns.labels).length > 3 && (
|
||||||
|
<span className="text-slate-600">+{Object.keys(ns.labels).length - 3}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-slate-600">-</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
{isSystem ? (
|
||||||
|
<span className="text-xs text-slate-600">系统</span>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="ghost" size="icon"
|
||||||
|
className="h-7 w-7 text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
||||||
|
onClick={() => setDeleteTarget(ns.name)}
|
||||||
|
title="删除命名空间"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Delete confirmation dialog */}
|
||||||
|
<Dialog open={!!deleteTarget} onOpenChange={(open) => !open && setDeleteTarget(null)}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-red-400 flex items-center gap-2">
|
||||||
|
<AlertTriangle className="h-5 w-5" />删除命名空间
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-3 py-4">
|
||||||
|
<div className="bg-red-500/10 border border-red-500/30 rounded-lg p-3">
|
||||||
|
<p className="text-sm text-red-300">
|
||||||
|
⚠️ 即将删除命名空间 <span className="font-bold">{deleteTarget}</span> 及其中所有资源。
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-red-400/70 mt-1">
|
||||||
|
此操作不可逆,所有 Deployment/Pod/Service/ConfigMap 等资源将被永久删除。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setDeleteTarget(null)}>取消</Button>
|
||||||
|
<Button variant="destructive" onClick={handleDelete} disabled={deleting}>
|
||||||
|
{deleting ? <Loader2 className="h-4 w-4 animate-spin mr-1" /> : null}
|
||||||
|
确认删除
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -24,7 +24,8 @@ import { useToast } from '@/hooks/use-toast';
|
|||||||
type ResourceType =
|
type ResourceType =
|
||||||
| 'deployments' | 'pods' | 'services' | 'ingresses'
|
| 'deployments' | 'pods' | 'services' | 'ingresses'
|
||||||
| 'configmaps' | 'secrets' | 'hpas' | 'cronjobs'
|
| 'configmaps' | 'secrets' | 'hpas' | 'cronjobs'
|
||||||
| 'pvcs' | 'statefulsets' | 'daemonsets';
|
| 'pvcs' | 'statefulsets' | 'daemonsets'
|
||||||
|
| 'serviceaccounts' | 'roles' | 'rolebindings';
|
||||||
|
|
||||||
const RESOURCE_CONFIG: Record<ResourceType, {
|
const RESOURCE_CONFIG: Record<ResourceType, {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -228,6 +229,88 @@ const RESOURCE_CONFIG: Record<ResourceType, {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
serviceaccounts: {
|
||||||
|
title: 'ServiceAccounts', subtitle: '服务账号(RBAC 身份标识)', icon: Shield,
|
||||||
|
columns: ['名称', '命名空间', 'Secret数', '镜像拉取凭证', '创建时间', '操作'],
|
||||||
|
renderRow: (sa, opts) => (
|
||||||
|
<TableRow key={sa.name} className="border-slate-800 hover:bg-slate-800/30">
|
||||||
|
<TableCell className="font-medium text-slate-200">{sa.name}</TableCell>
|
||||||
|
<TableCell><Badge variant="outline" className="border-slate-700 text-slate-400">{sa.namespace}</Badge></TableCell>
|
||||||
|
<TableCell className="text-sm text-slate-400">{sa.secret_count ?? 0}</TableCell>
|
||||||
|
<TableCell className="text-xs text-slate-500 max-w-[200px] truncate">
|
||||||
|
{sa.image_pull_secrets && sa.image_pull_secrets.length > 0 ? sa.image_pull_secrets.join(', ') : '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-xs text-slate-500">{sa.age || '-'}</TableCell>
|
||||||
|
<TableCell onClick={(e) => e.stopPropagation()}>
|
||||||
|
<Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400 hover:text-blue-400" title="查看YAML"
|
||||||
|
onClick={() => opts.onYaml?.(sa.name)}><FileCode className="h-3.5 w-3.5" /></Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
title: 'Roles', subtitle: '命名空间级角色权限(RBAC)', icon: Shield,
|
||||||
|
columns: ['名称', '命名空间', '规则数', '权限范围', '创建时间', '操作'],
|
||||||
|
renderRow: (r, opts) => {
|
||||||
|
const verbs = r.verbs || [];
|
||||||
|
const resources = r.resources || [];
|
||||||
|
const verbsStr = verbs.length > 4 ? verbs.slice(0, 4).join(', ') + ` +${verbs.length - 4}` : verbs.join(', ');
|
||||||
|
const resStr = resources.length > 4 ? resources.slice(0, 4).join(', ') + ` +${resources.length - 4}` : resources.join(', ');
|
||||||
|
return (
|
||||||
|
<TableRow key={r.name} className="border-slate-800 hover:bg-slate-800/30">
|
||||||
|
<TableCell className="font-medium text-slate-200">{r.name}</TableCell>
|
||||||
|
<TableCell><Badge variant="outline" className="border-slate-700 text-slate-400">{r.namespace}</Badge></TableCell>
|
||||||
|
<TableCell className="text-sm text-slate-400">{r.rule_count ?? 0}</TableCell>
|
||||||
|
<TableCell className="text-xs text-slate-500 max-w-[280px]">
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<span className="text-cyan-400 truncate" title={verbs.join(', ')}>verbs: {verbsStr || '-'}</span>
|
||||||
|
<span className="text-amber-400 truncate" title={resources.join(', ')}>resources: {resStr || '-'}</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-xs text-slate-500">{r.age || '-'}</TableCell>
|
||||||
|
<TableCell onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div className="flex items-center gap-0.5">
|
||||||
|
<Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400 hover:text-blue-400" title="查看YAML"
|
||||||
|
onClick={() => opts.onYaml?.(r.name)}><FileCode className="h-3.5 w-3.5" /></Button>
|
||||||
|
<Button variant="ghost" size="icon" className="h-7 w-7 text-emerald-400 hover:text-emerald-300" title="编辑"
|
||||||
|
onClick={() => opts.onEdit?.(r.name)}><Pencil className="h-3.5 w-3.5" /></Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rolebindings: {
|
||||||
|
title: 'RoleBindings', subtitle: '角色绑定关系(RBAC 授权)', icon: Shield,
|
||||||
|
columns: ['名称', '命名空间', '角色引用', '绑定主体', '创建时间', '操作'],
|
||||||
|
renderRow: (rb, opts) => {
|
||||||
|
const subjects = rb.subjects || [];
|
||||||
|
const subjStr = subjects.length > 3 ? subjects.slice(0, 3).join(', ') + ` +${subjects.length - 3}` : subjects.join(', ');
|
||||||
|
return (
|
||||||
|
<TableRow key={rb.name} className="border-slate-800 hover:bg-slate-800/30">
|
||||||
|
<TableCell className="font-medium text-slate-200">{rb.name}</TableCell>
|
||||||
|
<TableCell><Badge variant="outline" className="border-slate-700 text-slate-400">{rb.namespace}</Badge></TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge className="bg-purple-500/20 text-purple-400 border-purple-500/30">
|
||||||
|
{rb.role_ref_kind || '-'}/{rb.role_ref_name || '-'}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-xs text-slate-500 max-w-[200px] truncate" title={subjects.join(', ')}>
|
||||||
|
{subjStr || '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-xs text-slate-500">{rb.age || '-'}</TableCell>
|
||||||
|
<TableCell onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div className="flex items-center gap-0.5">
|
||||||
|
<Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400 hover:text-blue-400" title="查看YAML"
|
||||||
|
onClick={() => opts.onYaml?.(rb.name)}><FileCode className="h-3.5 w-3.5" /></Button>
|
||||||
|
<Button variant="ghost" size="icon" className="h-7 w-7 text-emerald-400 hover:text-emerald-300" title="编辑"
|
||||||
|
onClick={() => opts.onEdit?.(rb.name)}><Pencil className="h-3.5 w-3.5" /></Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function NamespaceResourcePage() {
|
export default function NamespaceResourcePage() {
|
||||||
@@ -295,6 +378,9 @@ export default function NamespaceResourcePage() {
|
|||||||
case 'pvcs': res = await kubernetesApi.getPVCs(clusterCode, namespace).catch(() => []); break;
|
case 'pvcs': res = await kubernetesApi.getPVCs(clusterCode, namespace).catch(() => []); break;
|
||||||
case 'statefulsets': res = await kubernetesApi.getStatefulSets(clusterCode, namespace).catch(() => []); break;
|
case 'statefulsets': res = await kubernetesApi.getStatefulSets(clusterCode, namespace).catch(() => []); break;
|
||||||
case 'daemonsets': res = await kubernetesApi.getDaemonSets(clusterCode, namespace).catch(() => []); break;
|
case 'daemonsets': res = await kubernetesApi.getDaemonSets(clusterCode, namespace).catch(() => []); break;
|
||||||
|
case 'serviceaccounts': res = await kubernetesApi.getServiceAccounts(clusterCode, namespace).catch(() => []); break;
|
||||||
|
case 'roles': res = await kubernetesApi.getRoles(clusterCode, namespace).catch(() => []); break;
|
||||||
|
case 'rolebindings': res = await kubernetesApi.getRoleBindings(clusterCode, namespace).catch(() => []); break;
|
||||||
}
|
}
|
||||||
setData(Array.isArray(res) ? res : (res?.list || res?.data || []));
|
setData(Array.isArray(res) ? res : (res?.list || res?.data || []));
|
||||||
} catch { setData([]); } finally { setLoading(false); }
|
} catch { setData([]); } finally { setLoading(false); }
|
||||||
@@ -332,6 +418,9 @@ export default function NamespaceResourcePage() {
|
|||||||
{ type: 'secrets' as ResourceType, label: 'Secrets', icon: Shield },
|
{ type: 'secrets' as ResourceType, label: 'Secrets', icon: Shield },
|
||||||
{ type: 'hpas' as ResourceType, label: 'HPAs', icon: Activity },
|
{ type: 'hpas' as ResourceType, label: 'HPAs', icon: Activity },
|
||||||
{ type: 'pvcs' as ResourceType, label: '存储卷', icon: Boxes },
|
{ type: 'pvcs' as ResourceType, label: '存储卷', icon: Boxes },
|
||||||
|
{ type: 'serviceaccounts' as ResourceType, label: 'ServiceAccounts', icon: Shield },
|
||||||
|
{ type: 'roles' as ResourceType, label: 'Roles', icon: Shield },
|
||||||
|
{ type: 'rolebindings' as ResourceType, label: 'RoleBindings', icon: Shield },
|
||||||
], []);
|
], []);
|
||||||
|
|
||||||
if (!config) return <div className="text-slate-400 p-8 text-center">未知资源类型: {type}</div>;
|
if (!config) return <div className="text-slate-400 p-8 text-center">未知资源类型: {type}</div>;
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export default function HostMonitorDashboard() {
|
|||||||
const [hosts, setHosts] = useState<any[]>([]);
|
const [hosts, setHosts] = useState<any[]>([]);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(15);
|
const [pageSize, setPageSize] = useState(10);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
@@ -441,9 +441,8 @@ export default function HostMonitorDashboard() {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="10">10</SelectItem>
|
<SelectItem value="10">10</SelectItem>
|
||||||
<SelectItem value="15">15</SelectItem>
|
|
||||||
<SelectItem value="20">20</SelectItem>
|
<SelectItem value="20">20</SelectItem>
|
||||||
<SelectItem value="50">50</SelectItem>
|
<SelectItem value="30">30</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<span>条,共 {total} 条</span>
|
<span>条,共 {total} 条</span>
|
||||||
|
|||||||
@@ -102,6 +102,36 @@ export function HostMonitor() {
|
|||||||
}).catch(() => setProbes([]));
|
}).catch(() => setProbes([]));
|
||||||
}, [selectedHost]);
|
}, [selectedHost]);
|
||||||
|
|
||||||
|
// 端口策略 // Logan.Li
|
||||||
|
const [portPolicyMode, setPortPolicyMode] = useState('blacklist');
|
||||||
|
const [portWhitelist, setPortWhitelist] = useState<number[]>([]);
|
||||||
|
const [portPolicyDialogOpen, setPortPolicyDialogOpen] = useState(false);
|
||||||
|
const [portPolicySaving, setPortPolicySaving] = useState(false);
|
||||||
|
|
||||||
|
const fetchPortPolicy = useCallback(() => {
|
||||||
|
if (!selectedHost) return;
|
||||||
|
agentApi.getPortPolicy(selectedHost).then((data: any) => {
|
||||||
|
if (data?.data) {
|
||||||
|
setPortPolicyMode(data.data.port_filter_mode || 'blacklist');
|
||||||
|
setPortWhitelist(data.data.port_whitelist || []);
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
}, [selectedHost]);
|
||||||
|
|
||||||
|
useEffect(() => { fetchPortPolicy(); }, [fetchPortPolicy]);
|
||||||
|
|
||||||
|
const handleSavePortPolicy = async () => {
|
||||||
|
if (!selectedHost) return;
|
||||||
|
setPortPolicySaving(true);
|
||||||
|
try {
|
||||||
|
await agentApi.savePortPolicy(selectedHost, {
|
||||||
|
port_filter_mode: portPolicyMode,
|
||||||
|
port_whitelist: portWhitelist,
|
||||||
|
});
|
||||||
|
setPortPolicyDialogOpen(false);
|
||||||
|
} catch { } finally { setPortPolicySaving(false); }
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchProbes();
|
fetchProbes();
|
||||||
}, [fetchProbes]);
|
}, [fetchProbes]);
|
||||||
@@ -143,25 +173,35 @@ export function HostMonitor() {
|
|||||||
const rawRx: number[] = []; const rawTx: number[] = [];
|
const rawRx: number[] = []; const rawTx: number[] = [];
|
||||||
const rawIoR: number[] = []; const rawIoW: number[] = [];
|
const rawIoR: number[] = []; const rawIoW: number[] = [];
|
||||||
const rawSwap: number[] = []; const rawFD: number[] = [];
|
const rawSwap: number[] = []; const rawFD: number[] = [];
|
||||||
|
// ──── v2.2 新增指标 ────
|
||||||
|
const rawIOWait: number[] = []; const rawMemAvail: number[] = [];
|
||||||
|
const rawIoUtil: number[] = []; const rawConnTotal: number[] = [];
|
||||||
|
const rawRxErr: number[] = []; const rawTxErr: number[] = [];
|
||||||
const timestamps: number[] = [];
|
const timestamps: number[] = [];
|
||||||
|
|
||||||
for (const r of history) {
|
for (const r of history) {
|
||||||
const ts = r.report_time ? new Date(r.report_time).getTime() / 1000 : (r.created_at ? new Date(r.created_at).getTime() / 1000 : 0);
|
const ts = r.report_time ? new Date(r.report_time).getTime() / 1000 : (r.created_at ? new Date(r.created_at).getTime() / 1000 : 0);
|
||||||
timestamps.push(ts);
|
timestamps.push(ts);
|
||||||
rawCpu.push(r.cpu_usage || 0);
|
rawCpu.push(r.cpu_usage || 0);
|
||||||
|
rawIOWait.push(r.cpu_iowait || 0);
|
||||||
rawMem.push(r.mem_usage || 0);
|
rawMem.push(r.mem_usage || 0);
|
||||||
|
rawMemAvail.push(r.mem_available || 0);
|
||||||
rawLoad1.push(r.load1 || 0);
|
rawLoad1.push(r.load1 || 0);
|
||||||
rawDisk.push(r.disk_usage || 0);
|
rawDisk.push(r.disk_usage || 0);
|
||||||
|
|
||||||
const net = parseJSON(r.net_info);
|
const net = parseJSON(r.net_info);
|
||||||
rawTcp.push(net.tcp_established || 0);
|
rawTcp.push(net.tcp_established || 0);
|
||||||
rawTw.push(net.tcp_time_wait || 0);
|
rawTw.push(net.tcp_time_wait || 0);
|
||||||
|
rawConnTotal.push((net.tcp_established || 0) + (net.tcp_time_wait || 0) + (net.tcp_close_wait || 0));
|
||||||
rawRx.push(net.net_rx_bps || 0);
|
rawRx.push(net.net_rx_bps || 0);
|
||||||
rawTx.push(net.net_tx_bps || 0);
|
rawTx.push(net.net_tx_bps || 0);
|
||||||
|
rawRxErr.push(net.net_rx_errors || 0);
|
||||||
|
rawTxErr.push(net.net_tx_errors || 0);
|
||||||
|
|
||||||
const dio = parseJSON(r.disk_io_info);
|
const dio = parseJSON(r.disk_io_info);
|
||||||
rawIoR.push(dio.read_bps || 0);
|
rawIoR.push(dio.read_bps || 0);
|
||||||
rawIoW.push(dio.write_bps || 0);
|
rawIoW.push(dio.write_bps || 0);
|
||||||
|
rawIoUtil.push(dio.util || 0);
|
||||||
|
|
||||||
const swap = parseJSON(r.swap_info);
|
const swap = parseJSON(r.swap_info);
|
||||||
rawSwap.push(swap?.usage_percent || 0);
|
rawSwap.push(swap?.usage_percent || 0);
|
||||||
@@ -183,6 +223,13 @@ export function HostMonitor() {
|
|||||||
ioWrite: bucketWithStats(rawIoW.map(v => v / 1024), timestamps, timeConfig.buckets),
|
ioWrite: bucketWithStats(rawIoW.map(v => v / 1024), timestamps, timeConfig.buckets),
|
||||||
swap: bucketWithStats(rawSwap, timestamps, timeConfig.buckets),
|
swap: bucketWithStats(rawSwap, timestamps, timeConfig.buckets),
|
||||||
fd: bucketWithStats(rawFD, timestamps, timeConfig.buckets),
|
fd: bucketWithStats(rawFD, timestamps, timeConfig.buckets),
|
||||||
|
// ──── v2.2 新增指标 ────
|
||||||
|
iowait: bucketWithStats(rawIOWait, timestamps, timeConfig.buckets),
|
||||||
|
memAvail: bucketWithStats(rawMemAvail, timestamps, timeConfig.buckets),
|
||||||
|
ioUtil: bucketWithStats(rawIoUtil, timestamps, timeConfig.buckets),
|
||||||
|
connTotal: bucketWithStats(rawConnTotal, timestamps, timeConfig.buckets),
|
||||||
|
rxErr: bucketWithStats(rawRxErr, timestamps, timeConfig.buckets),
|
||||||
|
txErr: bucketWithStats(rawTxErr, timestamps, timeConfig.buckets),
|
||||||
};
|
};
|
||||||
}, [history, timeConfig]);
|
}, [history, timeConfig]);
|
||||||
|
|
||||||
@@ -280,8 +327,14 @@ export function HostMonitor() {
|
|||||||
<TabsContent value="basic" className="mt-3 grid grid-cols-1 md:grid-cols-3 gap-3">
|
<TabsContent value="basic" className="mt-3 grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||||
<MetricChartCard title="CPU 使用率(%)" value={currentVal('cpu')} icon={<Cpu className="h-4 w-4 text-blue-400" />}
|
<MetricChartCard title="CPU 使用率(%)" value={currentVal('cpu')} icon={<Cpu className="h-4 w-4 text-blue-400" />}
|
||||||
data={metrics.cpu} color="#3b82f6" unit="%" domain={[0, 100]} aggType={aggType} />
|
data={metrics.cpu} color="#3b82f6" unit="%" domain={[0, 100]} aggType={aggType} />
|
||||||
|
<MetricChartCard title="CPU IO等待(%)" value={latest?.cpu_iowait != null ? `${latest.cpu_iowait.toFixed(1)}%` : '--'} icon={<Clock className="h-4 w-4 text-orange-400" />}
|
||||||
|
data={metrics.iowait} color="#f97316" unit="%" domain={[0, 100]} aggType={aggType} />
|
||||||
<MetricChartCard title="内存使用率(%)" value={currentVal('mem')} icon={<Server className="h-4 w-4 text-emerald-400" />}
|
<MetricChartCard title="内存使用率(%)" value={currentVal('mem')} icon={<Server className="h-4 w-4 text-emerald-400" />}
|
||||||
data={metrics.mem} color="#10b981" unit="%" domain={[0, 100]} aggType={aggType} />
|
data={metrics.mem} color="#10b981" unit="%" domain={[0, 100]} aggType={aggType} />
|
||||||
|
<MetricChartCard title="可用内存(GB)" value={latest?.mem_available != null ? `${latest.mem_available.toFixed(1)}GB` : '--'} icon={<Server className="h-4 w-4 text-emerald-400" />}
|
||||||
|
data={metrics.memAvail} color="#34d399" unit="GB" domain={[0, 'auto']} aggType={aggType} />
|
||||||
|
<MetricChartCard title="Swap 使用率(%)" value={currentVal('swap')} icon={<HardDrive className="h-4 w-4 text-red-400" />}
|
||||||
|
data={metrics.swap} color="#ef4444" unit="%" domain={[0, 100]} aggType={aggType} />
|
||||||
<MetricChartCard title="系统负载(1m)" value={currentVal('load1')} icon={<Activity className="h-4 w-4 text-purple-400" />}
|
<MetricChartCard title="系统负载(1m)" value={currentVal('load1')} icon={<Activity className="h-4 w-4 text-purple-400" />}
|
||||||
data={metrics.load1} color="#8b5cf6" unit="" domain={[0, 'auto']} aggType={aggType} />
|
data={metrics.load1} color="#8b5cf6" unit="" domain={[0, 'auto']} aggType={aggType} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
@@ -295,6 +348,8 @@ export function HostMonitor() {
|
|||||||
data={metrics.ioRead} color="#3b82f6" unit="KB/s" domain={[0, 'auto']} aggType={aggType} />
|
data={metrics.ioRead} color="#3b82f6" unit="KB/s" domain={[0, 'auto']} aggType={aggType} />
|
||||||
<MetricChartCard title="磁盘写入(KB/s)" value={currentVal('ioW')} icon={<Disc className="h-4 w-4 text-emerald-400" />}
|
<MetricChartCard title="磁盘写入(KB/s)" value={currentVal('ioW')} icon={<Disc className="h-4 w-4 text-emerald-400" />}
|
||||||
data={metrics.ioWrite} color="#10b981" unit="KB/s" domain={[0, 'auto']} aggType={aggType} />
|
data={metrics.ioWrite} color="#10b981" unit="KB/s" domain={[0, 'auto']} aggType={aggType} />
|
||||||
|
<MetricChartCard title="磁盘IO繁忙度(%)" value={diskIOInfo?.util != null ? `${diskIOInfo.util.toFixed(1)}%` : '--'} icon={<Disc className="h-4 w-4 text-red-400" />}
|
||||||
|
data={metrics.ioUtil} color="#ef4444" unit="%" domain={[0, 100]} aggType={aggType} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
@@ -305,6 +360,8 @@ export function HostMonitor() {
|
|||||||
data={metrics.tcpEst} color="#8b5cf6" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
data={metrics.tcpEst} color="#8b5cf6" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
||||||
<MetricChartCard title="TCP TIME_WAIT" value={currentVal('tw')} icon={<Clock className="h-4 w-4 text-amber-400" />}
|
<MetricChartCard title="TCP TIME_WAIT" value={currentVal('tw')} icon={<Clock className="h-4 w-4 text-amber-400" />}
|
||||||
data={metrics.tcpTW} color="#f59e0b" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
data={metrics.tcpTW} color="#f59e0b" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
||||||
|
<MetricChartCard title="TCP 总连接数" value={netInfo ? String((netInfo.tcp_established||0)+(netInfo.tcp_time_wait||0)+(netInfo.tcp_close_wait||0)) : '--'} icon={<Network className="h-4 w-4 text-cyan-400" />}
|
||||||
|
data={metrics.connTotal} color="#22d3ee" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
||||||
</div>
|
</div>
|
||||||
{/* 网卡带宽吞吐 — 双线 rx/tx */}
|
{/* 网卡带宽吞吐 — 双线 rx/tx */}
|
||||||
<DualLineChart
|
<DualLineChart
|
||||||
@@ -314,6 +371,12 @@ export function HostMonitor() {
|
|||||||
rxData={metrics.rx} txData={metrics.tx}
|
rxData={metrics.rx} txData={metrics.tx}
|
||||||
aggType={aggType} unit="KB/s"
|
aggType={aggType} unit="KB/s"
|
||||||
/>
|
/>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
|
||||||
|
<MetricChartCard title="网卡接收错误" value={netInfo?.net_rx_errors != null ? String(netInfo.net_rx_errors) : '--'} icon={<XCircle className="h-4 w-4 text-red-400" />}
|
||||||
|
data={metrics.rxErr} color="#ef4444" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
||||||
|
<MetricChartCard title="网卡发送错误" value={netInfo?.net_tx_errors != null ? String(netInfo.net_tx_errors) : '--'} icon={<XCircle className="h-4 w-4 text-orange-400" />}
|
||||||
|
data={metrics.txErr} color="#f97316" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
||||||
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{/* ═══ 端口监控 ═══ */}
|
{/* ═══ 端口监控 ═══ */}
|
||||||
@@ -322,6 +385,22 @@ export function HostMonitor() {
|
|||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<h3 className="text-xs font-medium text-[var(--dg-text-primary)] mb-3 flex items-center gap-2">
|
<h3 className="text-xs font-medium text-[var(--dg-text-primary)] mb-3 flex items-center gap-2">
|
||||||
<Server className="h-4 w-4 text-emerald-400" />监听端口 {portInfo.length}
|
<Server className="h-4 w-4 text-emerald-400" />监听端口 {portInfo.length}
|
||||||
|
<span className="ml-auto flex items-center gap-2">
|
||||||
|
{portPolicyMode === 'blacklist' && portWhitelist.length > 0 && (
|
||||||
|
<Badge variant="outline" className="text-[10px] text-amber-400 border-amber-500/30">
|
||||||
|
黑名单: {portWhitelist.join(',')}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{portPolicyMode === 'whitelist' && (
|
||||||
|
<Badge variant="outline" className="text-[10px] text-blue-400 border-blue-500/30">
|
||||||
|
白名单模式
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
<Button variant="outline" size="sm" className="h-6 text-[10px] px-2"
|
||||||
|
onClick={() => { fetchPortPolicy(); setPortPolicyDialogOpen(true); }}>
|
||||||
|
策略配置
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
</h3>
|
</h3>
|
||||||
{portInfo.length === 0 ? (
|
{portInfo.length === 0 ? (
|
||||||
<div className="text-center py-6 text-xs text-[var(--dg-text-muted)] border border-dashed border-[var(--dg-border)] rounded-lg">
|
<div className="text-center py-6 text-xs text-[var(--dg-text-muted)] border border-dashed border-[var(--dg-border)] rounded-lg">
|
||||||
@@ -492,6 +571,39 @@ export function HostMonitor() {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* FD 文件句柄 + 上下文切换 */}
|
||||||
|
{(fdInfo || csInfo) && (
|
||||||
|
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<HardDrive className="h-4 w-4 text-[var(--dg-text-muted)]" />
|
||||||
|
<h3 className="text-xs font-medium text-[var(--dg-text-primary)]">FD 文件句柄 & 上下文切换</h3>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-x-6 gap-y-3 text-xs">
|
||||||
|
{fdInfo?.allocated != null && [
|
||||||
|
['FD 已分配', <span className="font-mono">{fdInfo.allocated.toLocaleString()}</span>],
|
||||||
|
['FD 上限', <span className="font-mono">{fdInfo.max === -1 ? '无限制' : fdInfo.max?.toLocaleString() || '—'}</span>],
|
||||||
|
['FD 使用率', <span className={`font-mono ${fdInfo.usage_pct > 80 ? 'text-rose-400' : 'text-[var(--dg-text-primary)]'}`}>{fdInfo.usage_pct != null ? `${fdInfo.usage_pct.toFixed(1)}%` : '—'}</span>],
|
||||||
|
].map(([label, val]) => (
|
||||||
|
<div key={label as string}>
|
||||||
|
<span className="text-[var(--dg-text-muted)]">{label}:</span>
|
||||||
|
{val}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{csInfo?.ctx_per_sec != null && [
|
||||||
|
['CTX 累计', <span className="font-mono">{(csInfo.ctx_per_sec / 1e6).toFixed(1)}M</span>],
|
||||||
|
['IRQ 累计', <span className="font-mono">{(csInfo.irq_per_sec / 1e6).toFixed(1)}M</span>],
|
||||||
|
].map(([label, val]) => (
|
||||||
|
<div key={label as string}>
|
||||||
|
<span className="text-[var(--dg-text-muted)]">{label}:</span>
|
||||||
|
{val}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 磁盘分区 + 网络接口 (保持原样) */}
|
{/* 磁盘分区 + 网络接口 (保持原样) */}
|
||||||
{latest?.disk_info && (() => {
|
{latest?.disk_info && (() => {
|
||||||
const disk = parseJSON(latest.disk_info);
|
const disk = parseJSON(latest.disk_info);
|
||||||
@@ -695,6 +807,73 @@ export function HostMonitor() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 端口策略对话框 */}
|
||||||
|
{portPolicyDialogOpen && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={() => setPortPolicyDialogOpen(false)}>
|
||||||
|
<div className="bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg p-5 w-[440px] max-h-[90vh] overflow-y-auto shadow-xl" onClick={e => e.stopPropagation()}>
|
||||||
|
<h3 className="text-sm font-semibold text-[var(--dg-text-primary)] mb-4">端口监控策略</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 模式选择 */}
|
||||||
|
<div>
|
||||||
|
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1.5 block">过滤模式</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
className={`flex-1 h-9 rounded text-xs font-medium border transition-colors ${portPolicyMode === 'whitelist' ? 'bg-blue-500/20 border-blue-500 text-blue-400' : 'bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-muted)]'}`}
|
||||||
|
onClick={() => setPortPolicyMode('whitelist')}
|
||||||
|
>
|
||||||
|
白名单 ─ 全量上报
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`flex-1 h-9 rounded text-xs font-medium border transition-colors ${portPolicyMode === 'blacklist' ? 'bg-amber-500/20 border-amber-500 text-amber-400' : 'bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-muted)]'}`}
|
||||||
|
onClick={() => setPortPolicyMode('blacklist')}
|
||||||
|
>
|
||||||
|
黑名单 ─ 仅上报选中
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-[var(--dg-text-muted)] mt-1.5 leading-relaxed">
|
||||||
|
{portPolicyMode === 'whitelist'
|
||||||
|
? '所有发现的端口都上报,任意端口变化都会触发告警 + 恢复通知。'
|
||||||
|
: '仅上报选中端口的变化,其余端口变化静默忽略(不告警)。系统进程端口(sshd/rpcbind/systemd等)永远忽略。'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 端口白名单 */}
|
||||||
|
<div>
|
||||||
|
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1.5 block">
|
||||||
|
端口列表 <span className="text-[var(--dg-text-muted)]">(逗号分隔,如: 80,443,3306,6379)</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
className="w-full h-20 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 py-1.5 text-xs font-mono text-[var(--dg-text-primary)] resize-none"
|
||||||
|
value={portWhitelist.join(',')}
|
||||||
|
onChange={e => {
|
||||||
|
const val = e.target.value;
|
||||||
|
const nums = val.split(',').map(s => parseInt(s.trim())).filter(n => !isNaN(n) && n > 0 && n <= 65535);
|
||||||
|
setPortWhitelist(nums);
|
||||||
|
}}
|
||||||
|
placeholder="80,443,3306,6379"
|
||||||
|
/>
|
||||||
|
<p className="text-[10px] text-[var(--dg-text-muted)] mt-1">
|
||||||
|
已输入 {portWhitelist.length} 个端口
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 系统排除说明 */}
|
||||||
|
<div className="bg-amber-500/5 border border-amber-500/20 rounded p-2.5">
|
||||||
|
<p className="text-[10px] text-amber-300/80 leading-relaxed">
|
||||||
|
⚠️ 无论选择哪种模式,以下系统进程端口<b>永远不上报</b>:sshd、rpcbind、master、systemd、dbus-daemon、rsyslogd 等。Agent 采集时自动跳过。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-2 mt-5">
|
||||||
|
<Button variant="outline" size="sm" className="h-7 text-[11px]" onClick={() => setPortPolicyDialogOpen(false)}>取消</Button>
|
||||||
|
<Button size="sm" className="h-7 text-[11px] bg-blue-600 hover:bg-blue-500" onClick={handleSavePortPolicy} disabled={portPolicySaving}>
|
||||||
|
{portPolicySaving ? '保存中…' : '保存策略'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -444,6 +444,17 @@ export const kubernetesApi = {
|
|||||||
getDaemonSets: async (clusterCode: string, namespace: string) => {
|
getDaemonSets: async (clusterCode: string, namespace: string) => {
|
||||||
return http.get(`/kubernetes/${clusterCode}/namespace/${namespace}/daemonsets`);
|
return http.get(`/kubernetes/${clusterCode}/namespace/${namespace}/daemonsets`);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// === RBAC — ServiceAccount / Role / RoleBinding ===
|
||||||
|
getServiceAccounts: async (clusterCode: string, namespace: string) => {
|
||||||
|
return http.get(`/kubernetes/${clusterCode}/namespace/${namespace}/serviceaccounts`);
|
||||||
|
},
|
||||||
|
getRoles: async (clusterCode: string, namespace: string) => {
|
||||||
|
return http.get(`/kubernetes/${clusterCode}/namespace/${namespace}/roles`);
|
||||||
|
},
|
||||||
|
getRoleBindings: async (clusterCode: string, namespace: string) => {
|
||||||
|
return http.get(`/kubernetes/${clusterCode}/namespace/${namespace}/rolebindings`);
|
||||||
|
},
|
||||||
getStatefulSetDetail: async (clusterCode: string, namespace: string, name: string) => { return http.get(`/kubernetes/${clusterCode}/namespace/${namespace}/statefulsets/${name}`); },
|
getStatefulSetDetail: async (clusterCode: string, namespace: string, name: string) => { return http.get(`/kubernetes/${clusterCode}/namespace/${namespace}/statefulsets/${name}`); },
|
||||||
getDaemonSetDetail: async (clusterCode: string, namespace: string, name: string) => { return http.get(`/kubernetes/${clusterCode}/namespace/${namespace}/daemonsets/${name}`); },
|
getDaemonSetDetail: async (clusterCode: string, namespace: string, name: string) => { return http.get(`/kubernetes/${clusterCode}/namespace/${namespace}/daemonsets/${name}`); },
|
||||||
scaleStatefulSet: async (clusterCode: string, namespace: string, name: string, replicas: number) => { return http.post(`/kubernetes/${clusterCode}/namespace/${namespace}/statefulsets/${name}/scale`, { replicas }); },
|
scaleStatefulSet: async (clusterCode: string, namespace: string, name: string, replicas: number) => { return http.post(`/kubernetes/${clusterCode}/namespace/${namespace}/statefulsets/${name}/scale`, { replicas }); },
|
||||||
@@ -532,6 +543,19 @@ export const kubernetesApi = {
|
|||||||
return list.find((d: any) => d.name === name) || null;
|
return list.find((d: any) => d.name === name) || null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// === 🆕 M14: Namespace 管理看板 ===
|
||||||
|
getNamespacesDetail: async (clusterCode: string) => {
|
||||||
|
return http.get(`/kubernetes/${clusterCode}/namespaces/detail`);
|
||||||
|
},
|
||||||
|
getNamespaceDetail: async (clusterCode: string, namespace: string) => {
|
||||||
|
return http.get(`/kubernetes/${clusterCode}/namespace/${namespace}/detail`);
|
||||||
|
},
|
||||||
|
createNamespace: async (clusterCode: string, name: string, labels?: Record<string, string>) => {
|
||||||
|
return http.post(`/kubernetes/${clusterCode}/namespaces`, { name, labels: labels || {} });
|
||||||
|
},
|
||||||
|
deleteNamespace: async (clusterCode: string, namespace: string) => {
|
||||||
|
return http.delete(`/kubernetes/${clusterCode}/namespaces/${namespace}`);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==================== 告警 API ====================
|
// ==================== 告警 API ====================
|
||||||
@@ -890,6 +914,9 @@ export const agentApi = {
|
|||||||
createHealthProbe: (data: any) => http.post('/agent/health-probes', data),
|
createHealthProbe: (data: any) => http.post('/agent/health-probes', data),
|
||||||
updateHealthProbe: (id: number, data: any) => http.put(`/agent/health-probes/${id}`, data),
|
updateHealthProbe: (id: number, data: any) => http.put(`/agent/health-probes/${id}`, data),
|
||||||
deleteHealthProbe: (id: number) => http.delete(`/agent/health-probes/${id}`),
|
deleteHealthProbe: (id: number) => http.delete(`/agent/health-probes/${id}`),
|
||||||
|
// 端口策略
|
||||||
|
getPortPolicy: (hostname: string) => http.get(`/agent/port-policies/${hostname}`),
|
||||||
|
savePortPolicy: (hostname: string, data: { port_filter_mode: string; port_whitelist: number[] }) => http.put(`/agent/port-policies/${hostname}`, data),
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==================== 区域管理 API ====================
|
// ==================== 区域管理 API ====================
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
agent.hostname [s|opsx-server]
|
||||||
|
agent.hostmetadata [s|Linux]
|
||||||
|
agent.ping [u|1]
|
||||||
|
agent.variant [u|1]
|
||||||
|
agent.version [s|7.2.15]
|
||||||
|
modbus.get[tcp://127.0.0.1] [m|ZBX_NOTSUPPORTED] [Cannot read modbus data: modbus_connect() failed: Connection refused.]
|
||||||
|
system.localtime[utc] [u|1782313657]
|
||||||
|
system.run[echo test] [t|test]
|
||||||
|
vfs.file.size[/etc/passwd] [u|1998]
|
||||||
|
vfs.file.time[/etc/passwd,modify] [u|1782313505]
|
||||||
|
vfs.file.exists[/etc/passwd] [u|1]
|
||||||
|
vfs.file.contents[/etc/passwd] [t|root:x:0:0:root:/root:/bin/bash
|
||||||
|
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
|
||||||
|
bin:x:2:2:bin:/bin:/usr/sbin/nologin
|
||||||
|
sys:x:3:3:sys:/dev:/usr/sbin/nologin
|
||||||
|
sync:x:4:65534:sync:/bin:/bin/sync
|
||||||
|
games:x:5:60:games:/usr/games:/usr/sbin/nologin
|
||||||
|
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
|
||||||
|
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
|
||||||
|
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
|
||||||
|
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
|
||||||
|
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
|
||||||
|
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
|
||||||
|
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
|
||||||
|
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
|
||||||
|
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
|
||||||
|
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
|
||||||
|
_apt:x:42:65534::/nonexistent:/usr/sbin/nologin
|
||||||
|
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
|
||||||
|
systemd-network:x:998:998:systemd Network Management:/:/usr/sbin/nologin
|
||||||
|
systemd-timesync:x:997:997:systemd Time Synchronization:/:/usr/sbin/nologin
|
||||||
|
dhcpcd:x:100:65534:DHCP Client Daemon,,,:/usr/lib/dhcpcd:/bin/false
|
||||||
|
messagebus:x:101:102::/nonexistent:/usr/sbin/nologin
|
||||||
|
systemd-resolve:x:992:992:systemd Resolver:/:/usr/sbin/nologin
|
||||||
|
pollinate:x:102:1::/var/cache/pollinate:/bin/false
|
||||||
|
polkitd:x:991:991:User for polkitd:/:/usr/sbin/nologin
|
||||||
|
syslog:x:103:104::/nonexistent:/usr/sbin/nologin
|
||||||
|
uuidd:x:104:105::/run/uuidd:/usr/sbin/nologin
|
||||||
|
tcpdump:x:105:107::/nonexistent:/usr/sbin/nologin
|
||||||
|
tss:x:106:108:TPM software stack,,,:/var/lib/tpm:/bin/false
|
||||||
|
landscape:x:107:109::/var/lib/landscape:/usr/sbin/nologin
|
||||||
|
fwupd-refresh:x:989:989:Firmware update daemon:/var/lib/fwupd:/usr/sbin/nologin
|
||||||
|
usbmux:x:108:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
|
||||||
|
sshd:x:109:65534::/run/sshd:/usr/sbin/nologin
|
||||||
|
ubuntu:x:1000:1000:ubuntu:/home/ubuntu:/bin/bash
|
||||||
|
_rpc:x:110:65534::/run/rpcbind:/usr/sbin/nologin
|
||||||
|
statd:x:111:65534::/var/lib/nfs:/usr/sbin/nologin
|
||||||
|
tinyproxy:x:112:110:Tinyproxy daemon:/run/tinyproxy:/bin/false
|
||||||
|
zabbix:x:113:111::/var/lib/zabbix/:/usr/sbin/nologin]
|
||||||
|
vfs.file.regexp[/etc/passwd,root] [s|root:x:0:0:root:/root:/bin/bash]
|
||||||
|
vfs.file.regmatch[/etc/passwd,root] [u|1]
|
||||||
|
vfs.file.md5sum[/etc/passwd] [s|bca9025abf4bf816ff4fe022318e3285]
|
||||||
|
vfs.file.cksum[/etc/passwd] [u|3501695293]
|
||||||
|
vfs.file.owner[/etc/passwd,user,name] [s|root]
|
||||||
|
vfs.file.permissions[/etc/passwd] [s|0644]
|
||||||
|
vfs.file.get[/etc/passwd] [s|{"basename":"passwd","pathname":"/etc/passwd","dirname":"/etc","type":"file","user":"root","group":"root","permissions":"0644","uid":0,"gid":0,"size":1998,"time":{"access":"2026-06-24T15:05:05+0000","modify":"2026-06-24T15:05:05+0000","change":"2026-06-24T15:05:05+0000"},"timestamp":{"access":1782313505,"modify":1782313505,"change":1782313505}}]
|
||||||
|
vfs.dir.size[/var/log] [u|188143320]
|
||||||
|
vfs.dir.count[/var/log] [u|92]
|
||||||
|
vfs.dir.get[/var/log] [s|[{"basename":"apt","pathname":"/var/log/apt","dirname":"/var/log","type":"dir","user":"root","group":"root","permissions":"0755","uid":0,"gid":0,"size":4096,"time":{"access":"2026-06-24T15:06:44+0000","modify":"2026-06-24T15:05:04+0000","change":"2026-06-24T15:05:04+0000"},"timestamp":{"access":1782313604,"modify":1782313504,"change":1782313504}},{"basename":"lastlog","pathname":"/var/log/lastlog","dirname":"/var/log","type":"file","user":"root","group":"utmp","permissions":"0664","uid":0,"gid":43,"size":292292,"time":{"access":"2026-06-24T13:53:38+0000","modify":"2026-06-24T15:05:05+0000","change":"2026-06-24T15:05:05+0000"},"timestamp":{"access":1782309218,"modify":1782313505,"change":1782313505}},{"basename":"tinyproxy","pathname":"/var/log/tinyproxy","dirname":"/var/log","type":"dir","user":"tinyproxy","group":"adm","permissions":"0750","uid":112,"gid":4,"size":4096,"time":{"access":"2026-06-24T15:06:44+0000","modify":"2026-06-24T00:00:00+0000","change":"2026-06-24T00:00:00+0000"},"timestamp":{"access":1782313604,"modify":1782259200,"change":1782259200}},{"basename":"kern.log.1","pathname":"/var/log/kern.log.1","dirname":"/var/log","type":"file","user":"syslog","group":"adm","permissions":"0640","uid":103,"gid":4,"size":1615671,"time":{"access":"2026-06-17T13:50:51+0000","modify":"2026-06-20T19:21:12+0000","change":"2026-06-21T00:00:00+0000"},"timestamp":{"access":1781704251,"modify":1781983272,"change":1782000000}},{"basename":"README","pathname":"/var/log/README","dirname":"/var/log","type":"sym","user":"root","group":"root","permissions":"0777","uid":0,"gid":0,"size":39,"time":{"access":"2026-06-24T14:44:40+0000","modify":"2024-04-23T09:40:20+0000","change":"2026-02-02T14:03:34+0000"},"timestamp":{"access":1782312280,"modify":1713865220,"change":1770041014}},{"basename":"alternatives.log","pathname":"/var/log/alternatives.log","dirname":"/var/log","type":"file","user":"root","group":"root","permissions":"0644","uid":0,"gid":0,"size":4292,"time":{"access":"2026-06-17T15:32:01+0000","modify":"2026-06-19T14:16:45+0000","change":"2026-06-19T14:16:45+0000"},"timestamp":{"access":1781710321,"modify":1781878605,"change":1781878605}},{"basename":"zabbix","pathname":"/var/log/zabbix","dirname":"/var/log","type":"dir","user":"zabbix","group":"zabbix","permissions":"0755","uid":113,"gid":111,"size":4096,"time":{"access":"2026-06-24T15:06:44+0000","modify":"2026-06-24T15:05:05+0000","change":"2026-06-24T15:05:05+0000"},"timestamp":{"access":1782313604,"modify":1782313505,"change":1782313505}},{"basename":"auth.log.1","pathname":"/var/log/auth.log.1","dirname":"/var/log","type":"file","user":"syslog","group":"adm","permissions":"0640","uid":103,"gid":4,"size":244353,"time":{"access":"2026-06-17T13:50:51+0000","modify":"2026-06-20T23:59:01+0000","change":"2026-06-21T00:00:00+0000"},"timestamp":{"access":1781704251,"modify":1781999941,"change":1782000000}},{"basename":"wtmp","pathname":"/var/log/wtmp","dirname":"/var/log","type":"file","user":"root","group":"utmp","permissions":"0664","uid":0,"gid":43,"size":71040,"time":{"access":"2026-02-02T14:03:34+0000","modify":"2026-06-24T13:53:38+0000","change":"2026-06-24T13:53:38+0000"},"timestamp":{"access":1770041014,"modify":1782309218,"change":1782309218}},{"basename":"landscape","pathname":"/var/log/landscape","dirname":"/var/log","type":"dir","user":"landscape","group":"landscape","permissions":"0755","uid":107,"gid":109,"size":4096,"time":{"access":"2026-06-24T15:06:44+0000","modify":"2026-02-02T14:33:17+0000","change":"2026-02-02T14:33:17+0000"},"timestamp":{"access":1782313604,"modify":1770042797,"change":1770042797}},{"basename":"unattended-upgrades","pathname":"/var/log/unattended-upgrades","dirname":"/var/log","type":"dir","user":"root","group":"adm","permissions":"0750","uid":0,"gid":4,"size":4096,"time":{"access":"2026-06-24T15:06:44+0000","modify":"2026-02-02T14:32:54+0000","change":"2026-02-02T14:32:54+0000"},"timestamp":{"access":1782313604,"modify":1770042774,"change":1770042774}},{"basename":"dmesg.1.gz","pathname":"/var/log/dmesg.1.gz","dirname":"/var/log","type":"file","user":"root","group":"adm","permissions":"0640","uid":0,"gid":4,"size":15631,"time":{"access":"2026-06-17T13:50:56+0000","modify":"2026-06-17T13:50:56+0000","change":"2026-06-17T14:28:33+0000"},"timestamp":{"access":1781704256,"modify":1781704256,"change":1781706513}},{"basename":"faillog","pathname":"/var/log/faillog","dirname":"/var/log","type":"file","user":"root","group":"root","permissions":"0644","uid":0,"gid":0,"size":0,"time":{"access":"2026-02-02T14:03:34+0000","modify":"2024-04-23T09:37:12+0000","change":"2026-02-02T14:03:34+0000"},"timestamp":{"access":1770041014,"modify":1713865032,"change":1770041014}},{"basename":"kern.log","pathname":"/var/log/kern.log","dirname":"/var/log","type":"file","user":"syslog","group":"adm","permissions":"0640","uid":103,"gid":4,"size":1291471,"time":{"access":"2026-06-21T00:00:00+0000","modify":Line truncated
|
||||||
|
net.dns [m|ZBX_NOTSUPPORTED] [Second parameter cannot be empty.]
|
||||||
|
net.dns.record [m|ZBX_NOTSUPPORTED] [Second parameter cannot be empty.]
|
||||||
|
net.dns.perf [m|ZBX_NOTSUPPORTED] [Second parameter cannot be empty.]
|
||||||
|
net.tcp.dns[,zabbix.com] [u|1]
|
||||||
|
net.tcp.dns.query[,zabbix.com] [t|zabbix.com SOA ines.ns.cloudflare.com dns.cloudflare.com 2407352100 10000 2400 604800 1800]
|
||||||
|
net.tcp.port[,80] [u|1]
|
||||||
|
system.users.num [u|1]
|
||||||
|
log[logfile] [m|ZBX_NOTSUPPORTED] [Accessible only as active check.]
|
||||||
|
log.count[logfile] [m|ZBX_NOTSUPPORTED] [Accessible only as active check.]
|
||||||
|
logrt[logfile] [m|ZBX_NOTSUPPORTED] [Accessible only as active check.]
|
||||||
|
logrt.count[logfile] [m|ZBX_NOTSUPPORTED] [Accessible only as active check.]
|
||||||
|
eventlog[system] [m|ZBX_NOTSUPPORTED] [Accessible only as active check.]
|
||||||
|
eventlog.count[system] [m|ZBX_NOTSUPPORTED] [Accessible only as active check.]
|
||||||
|
zabbix.stats[127.0.0.1,10051] [m|ZBX_NOTSUPPORTED] [Cannot obtain internal statistics: cannot connect to [[127.0.0.1]:10051]: connection error (POLLERR,POLLHUP)]
|
||||||
|
web.page.get[localhost,,80] [t|HTTP/1.1 200 OK
|
||||||
|
Server: nginx/1.31.2
|
||||||
|
Date: Wed, 24 Jun 2026 15:07:37 GMT
|
||||||
|
Content-Type: text/html
|
||||||
|
Content-Length: 590
|
||||||
|
Last-Modified: Wed, 24 Jun 2026 14:50:51 GMT
|
||||||
|
Connection: close
|
||||||
|
ETag: "6a3beecb-24e"
|
||||||
|
Accept-Ranges: bytes
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
|
<meta name="theme-color" content="#1e6feb" />
|
||||||
|
<title>AIOps智能运维平台</title>
|
||||||
|
<script type="module" crossorigin src="./assets/index-C2daw8dB.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="./assets/index-hY2FdsKP.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>]
|
||||||
|
web.page.perf[localhost,,80] [d|0.00052094459533691406]
|
||||||
|
web.page.regexp[localhost,,80,OK] [s|OK]
|
||||||
|
kernel.maxfiles [u|9223372036854775807]
|
||||||
|
kernel.maxproc [u|4194304]
|
||||||
|
kernel.openfiles [u|3136]
|
||||||
|
vfs.fs.size[/,free] [u|213612339200]
|
||||||
|
vfs.fs.inode[/,free] [u|15893125]
|
||||||
|
vfs.fs.discovery [s|[{"{#FSNAME}":"/sys","{#FSTYPE}":"sysfs","{#FSOPTIONS}":"rw,nosuid,nodev,noexec,relatime"},{"{#FSNAME}":"/proc","{#FSTYPE}":"proc","{#FSOPTIONS}":"rw,nosuid,nodev,noexec,relatime"},{"{#FSNAME}":"/dev","{#FSTYPE}":"devtmpfs","{#FSOPTIONS}":"rw,nosuid,relatime,size=16393268k,nr_inodes=4098317,mode=755,inode64"},{"{#FSNAME}":"/dev/pts","{#FSTYPE}":"devpts","{#FSOPTIONS}":"rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000"},{"{#FSNAME}":"/run","{#FSTYPE}":"tmpfs","{#FSOPTIONS}":"rw,nosuid,nodev,noexec,relatime,size=3286612k,mode=755,inode64"},{"{#FSNAME}":"/","{#FSTYPE}":"ext4","{#FSOPTIONS}":"rw,relatime"},{"{#FSNAME}":"/sys/kernel/security","{#FSTYPE}":"securityfs","{#FSOPTIONS}":"rw,nosuid,nodev,noexec,relatime"},{"{#FSNAME}":"/dev/shm","{#FSTYPE}":"tmpfs","{#FSOPTIONS}":"rw,nosuid,nodev,inode64"},{"{#FSNAME}":"/run/lock","{#FSTYPE}":"tmpfs","{#FSOPTIONS}":"rw,nosuid,nodev,noexec,relatime,size=5120k,inode64"},{"{#FSNAME}":"/sys/fs/cgroup","{#FSTYPE}":"cgroup2","{#FSOPTIONS}":"rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot"},{"{#FSNAME}":"/sys/fs/pstore","{#FSTYPE}":"pstore","{#FSOPTIONS}":"rw,nosuid,nodev,noexec,relatime"},{"{#FSNAME}":"/sys/fs/bpf","{#FSTYPE}":"bpf","{#FSOPTIONS}":"rw,nosuid,nodev,noexec,relatime,mode=700"},{"{#FSNAME}":"/proc/sys/fs/binfmt_misc","{#FSTYPE}":"autofs","{#FSOPTIONS}":"rw,relatime,fd=32,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=6315"},{"{#FSNAME}":"/dev/hugepages","{#FSTYPE}":"hugetlbfs","{#FSOPTIONS}":"rw,nosuid,nodev,relatime,pagesize=2M"},{"{#FSNAME}":"/dev/mqueue","{#FSTYPE}":"mqueue","{#FSOPTIONS}":"rw,nosuid,nodev,noexec,relatime"},{"{#FSNAME}":"/sys/kernel/tracing","{#FSTYPE}":"tracefs","{#FSOPTIONS}":"rw,nosuid,nodev,noexec,relatime"},{"{#FSNAME}":"/sys/kernel/debug","{#FSTYPE}":"debugfs","{#FSOPTIONS}":"rw,nosuid,nodev,noexec,relatime"},{"{#FSNAME}":"/sys/fs/fuse/connections","{#FSTYPE}":"fusectl","{#FSOPTIONS}":"rw,nosuid,nodev,noexec,relatime"},{"{#FSNAME}":"/sys/kernel/config","{#FSTYPE}":"configfs","{#FSOPTIONS}":"rw,nosuid,nodev,noexec,relatime"},{"{#FSNAME}":"/boot","{#FSTYPE}":"ext4","{#FSOPTIONS}":"rw,relatime"},{"{#FSNAME}":"/proc/sys/fs/binfmt_misc","{#FSTYPE}":"binfmt_misc","{#FSOPTIONS}":"rw,nosuid,nodev,noexec,relatime"},{"{#FSNAME}":"/run/user/0","{#FSTYPE}":"tmpfs","{#FSOPTIONS}":"rw,nosuid,nodev,relatime,size=3286612k,nr_inodes=821653,mode=700,inode64"},{"{#FSNAME}":"/sys/kernel/debug/tracing","{#FSTYPE}":"tracefs","{#FSOPTIONS}":"rw,nosuid,nodev,noexec,relatime"},{"{#FSNAME}":"/run/rpc_pipefs","{#FSTYPE}":"rpc_pipefs","{#FSOPTIONS}":"rw,relatime"},{"{#FSNAME}":"/proc/fs/nfsd","{#FSTYPE}":"nfsd","{#FSOPTIONS}":"rw,relatime"},{"{#FSNAME}":"/var/lib/docker/rootfs/overlayfs/11c0fa077c833d8a9752076d2a0429470748c91ea6a7d983da8a86151252beb3","{#FSTYPE}":"overlay","{#FSOPTIONS}":"rw,relatime,lowerdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/4707/fs:/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/99/fs:/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/98/fs:/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/97/fs:/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/96/fs:/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/95/fs:/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/94/fs:/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/93/fs,upperdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/4709/fs,workdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/4709/work,nouserxattr"},{"{#FSNAME}":"/var/lib/docker/rootfs/overlayfs/f564f454b57dfa9c881e3957defbba55886c66934e0771c6b108b36de396034a","{#FSTYPE}":"overlay","{#FSOPTIONS}":"rw,relatime,lowerdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/2455/fs:/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/2454/fs:/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/2453/fs:/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/2452/fs:/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/2451/fs:/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/2450/fs,upperdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/2456/fs,workdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/2456/work,nouserxattr"},{"{#FSNAME}":"/var/lib/docker/rootfs/overlayfs/eda32ec9e25b2e02ade3dfbeace0ec8a4bdf665b5971277245af5eb1047754cd","{#FSTYPE}":"overlay","{#FSOPTIONS}":"rw,relatime,lowerdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/4706/fs:/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/92/fs:/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/71/fs:/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/70/fs:/var/lib/containerd/io.containerdLine truncated
|
||||||
|
vfs.fs.get [s|[{"fsname":"/sys","fstype":"sysfs","bytes":{"total":0,"free":0,"used":0,"pfree":0.000000,"pused":0.000000},"inodes":{"total":0,"free":0,"used":0,"pfree":100.000000,"pused":0.000000},"options":"rw,nosuid,nodev,noexec,relatime"},{"fsname":"/proc","fstype":"proc","bytes":{"total":0,"free":0,"used":0,"pfree":0.000000,"pused":0.000000},"inodes":{"total":0,"free":0,"used":0,"pfree":100.000000,"pused":0.000000},"options":"rw,nosuid,nodev,noexec,relatime"},{"fsname":"/dev","fstype":"devtmpfs","bytes":{"total":16786706432,"free":16786706432,"used":0,"pfree":100.000000,"pused":0.000000},"inodes":{"total":4098317,"free":4097820,"used":497,"pfree":99.987873,"pused":0.012127},"options":"rw,nosuid,relatime,size=16393268k,nr_inodes=4098317,mode=755,inode64"},{"fsname":"/dev/pts","fstype":"devpts","bytes":{"total":0,"free":0,"used":0,"pfree":0.000000,"pused":0.000000},"inodes":{"total":0,"free":0,"used":0,"pfree":100.000000,"pused":0.000000},"options":"rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000"},{"fsname":"/run","fstype":"tmpfs","bytes":{"total":3365490688,"free":3363639296,"used":1851392,"pfree":99.944989,"pused":0.055011},"inodes":{"total":4108265,"free":4107254,"used":1011,"pfree":99.975391,"pused":0.024609},"options":"rw,nosuid,nodev,noexec,relatime,size=3286612k,mode=755,inode64"},{"fsname":"/","fstype":"ext4","bytes":{"total":293473615872,"free":213612326912,"used":67486547968,"pfree":75.991883,"pused":24.008117},"inodes":{"total":18219008,"free":15893125,"used":2325883,"pfree":87.233756,"pused":12.766244},"options":"rw,relatime"},{"fsname":"/sys/kernel/security","fstype":"securityfs","bytes":{"total":0,"free":0,"used":0,"pfree":0.000000,"pused":0.000000},"inodes":{"total":0,"free":0,"used":0,"pfree":100.000000,"pused":0.000000},"options":"rw,nosuid,nodev,noexec,relatime"},{"fsname":"/dev/shm","fstype":"tmpfs","bytes":{"total":16827453440,"free":16827453440,"used":0,"pfree":100.000000,"pused":0.000000},"inodes":{"total":4108265,"free":4108264,"used":1,"pfree":99.999976,"pused":0.000024},"options":"rw,nosuid,nodev,inode64"},{"fsname":"/run/lock","fstype":"tmpfs","bytes":{"total":5242880,"free":5242880,"used":0,"pfree":100.000000,"pused":0.000000},"inodes":{"total":4108265,"free":4108262,"used":3,"pfree":99.999927,"pused":0.000073},"options":"rw,nosuid,nodev,noexec,relatime,size=5120k,inode64"},{"fsname":"/sys/fs/cgroup","fstype":"cgroup2","bytes":{"total":0,"free":0,"used":0,"pfree":0.000000,"pused":0.000000},"inodes":{"total":0,"free":0,"used":0,"pfree":100.000000,"pused":0.000000},"options":"rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot"},{"fsname":"/sys/fs/pstore","fstype":"pstore","bytes":{"total":0,"free":0,"used":0,"pfree":0.000000,"pused":0.000000},"inodes":{"total":0,"free":0,"used":0,"pfree":100.000000,"pused":0.000000},"options":"rw,nosuid,nodev,noexec,relatime"},{"fsname":"/sys/fs/bpf","fstype":"bpf","bytes":{"total":0,"free":0,"used":0,"pfree":0.000000,"pused":0.000000},"inodes":{"total":0,"free":0,"used":0,"pfree":100.000000,"pused":0.000000},"options":"rw,nosuid,nodev,noexec,relatime,mode=700"},{"fsname":"/proc/sys/fs/binfmt_misc","fstype":"autofs","bytes":{"total":0,"free":0,"used":0,"pfree":0.000000,"pused":0.000000},"inodes":{"total":0,"free":0,"used":0,"pfree":100.000000,"pused":0.000000},"options":"rw,relatime,fd=32,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=6315"},{"fsname":"/dev/hugepages","fstype":"hugetlbfs","bytes":{"total":0,"free":0,"used":0,"pfree":0.000000,"pused":0.000000},"inodes":{"total":0,"free":0,"used":0,"pfree":100.000000,"pused":0.000000},"options":"rw,nosuid,nodev,relatime,pagesize=2M"},{"fsname":"/dev/mqueue","fstype":"mqueue","bytes":{"total":0,"free":0,"used":0,"pfree":0.000000,"pused":0.000000},"inodes":{"total":0,"free":0,"used":0,"pfree":100.000000,"pused":0.000000},"options":"rw,nosuid,nodev,noexec,relatime"},{"fsname":"/sys/kernel/tracing","fstype":"tracefs","bytes":{"total":0,"free":0,"used":0,"pfree":0.000000,"pused":0.000000},"inodes":{"total":0,"free":0,"used":0,"pfree":100.000000,"pused":0.000000},"options":"rw,nosuid,nodev,noexec,relatime"},{"fsname":"/sys/kernel/debug","fstype":"debugfs","bytes":{"total":0,"free":0,"used":0,"pfree":0.000000,"pused":0.000000},"inodes":{"total":0,"free":0,"used":0,"pfree":100.000000,"pused":0.000000},"options":"rw,nosuid,nodev,noexec,relatime"},{"fsname":"/sys/fs/fuse/connections","fstype":"fusectl","bytes":{"total":0,"free":0,"used":0,"pfree":0.000000,"pused":0.000000},"inodes":{"total":0,"free":0,"used":0,"pfree":100.000000,"pused":0.000000},"options":"rw,nosuid,nodev,noexec,relatime"},{"fsname":"/sys/kernel/config","fstype":"configfs","bytes":{"total":0,"free":0,"used":0,"pfree":0.000000,"pused":0.000000},"inodes":{"total":0,"free":0,"used":0,"pfree":100.000000,"pused":0.000000},"options":"rw,nosuid,nodev,noexec,relatime"},{"fsname":"/boot","fstype":"ext4","bytes":{"total":2040373248,"free":1811996672,"used":104226816,"pfree":94.560821,"puseLine truncated
|
||||||
|
vfs.dev.read[sda,operations] [u|471032]
|
||||||
|
vfs.dev.write[sda,operations] [u|26053370]
|
||||||
|
vfs.dev.discovery [s|[{"{#DEVNAME}":"sdb","{#DEVTYPE}":"disk"},{"{#DEVNAME}":"dm-0","{#DEVTYPE}":"disk"},{"{#DEVNAME}":"sr0","{#DEVTYPE}":"rom"},{"{#DEVNAME}":"sda3","{#DEVTYPE}":"partition"},{"{#DEVNAME}":"sda2","{#DEVTYPE}":"partition"},{"{#DEVNAME}":"sda1","{#DEVTYPE}":"partition"},{"{#DEVNAME}":"sda","{#DEVTYPE}":"disk"},{"{#DEVNAME}":"loop7","{#DEVTYPE}":"disk"},{"{#DEVNAME}":"loop6","{#DEVTYPE}":"disk"},{"{#DEVNAME}":"loop5","{#DEVTYPE}":"disk"},{"{#DEVNAME}":"loop4","{#DEVTYPE}":"disk"},{"{#DEVNAME}":"loop3","{#DEVTYPE}":"disk"},{"{#DEVNAME}":"loop2","{#DEVTYPE}":"disk"},{"{#DEVNAME}":"loop1","{#DEVTYPE}":"disk"},{"{#DEVNAME}":"loop0","{#DEVTYPE}":"disk"}]]
|
||||||
|
net.tcp.listen[80] [u|1]
|
||||||
|
net.udp.listen[68] [u|1]
|
||||||
|
net.tcp.socket.count[,80] [u|6]
|
||||||
|
net.udp.socket.count[,68] [u|1]
|
||||||
|
net.if.in[lo,bytes] [u|714335780]
|
||||||
|
net.if.out[lo,bytes] [u|714335780]
|
||||||
|
net.if.total[lo,bytes] [u|1428671560]
|
||||||
|
net.if.collisions[lo] [u|0]
|
||||||
|
net.if.discovery [s|[{"{#IFNAME}":"lo"},{"{#IFNAME}":"br-e706b66ec89b"},{"{#IFNAME}":"ens18"},{"{#IFNAME}":"docker0"},{"{#IFNAME}":"veth6d69a97"},{"{#IFNAME}":"veth4c9b699"},{"{#IFNAME}":"veth8d7d728"},{"{#IFNAME}":"veth08ecc11"},{"{#IFNAME}":"vethe2def72"},{"{#IFNAME}":"vetha8739c6"},{"{#IFNAME}":"br-dbeb0f52ba9e"},{"{#IFNAME}":"veth4ee1648"}]]
|
||||||
|
vm.memory.size[total] [u|33654906880]
|
||||||
|
proc.cpu.util[inetd] [m|ZBX_NOTSUPPORTED] [Collector is not started.]
|
||||||
|
proc.get[inetd] [s|[]]
|
||||||
|
proc.num[inetd] [u|0]
|
||||||
|
proc.mem[inetd] [u|0]
|
||||||
|
system.cpu.switches [u|4163834909]
|
||||||
|
system.cpu.intr [u|2479286837]
|
||||||
|
system.cpu.util[all,user,avg1] [m|ZBX_NOTSUPPORTED] [Collector is not started.]
|
||||||
|
system.cpu.load[all,avg1] [d|0.25146484375]
|
||||||
|
system.cpu.num[online] [u|8]
|
||||||
|
system.cpu.discovery [m|ZBX_NOTSUPPORTED] [Collector is not started.]
|
||||||
|
system.uname [s|Linux opsx-server 6.8.0-90-generic #91-Ubuntu SMP PREEMPT_DYNAMIC Tue Nov 18 14:14:30 UTC 2025 x86_64]
|
||||||
|
system.hw.chassis [s|QEMU Standard PC (i440FX + PIIX, 1996) Other]
|
||||||
|
system.hw.cpu [t|processor 0: AuthenticAMD AMD EPYC 7H12 64-Core Processor working at 2595MHz
|
||||||
|
processor 1: AuthenticAMD AMD EPYC 7H12 64-Core Processor working at 2595MHz
|
||||||
|
processor 2: AuthenticAMD AMD EPYC 7H12 64-Core Processor working at 2595MHz
|
||||||
|
processor 3: AuthenticAMD AMD EPYC 7H12 64-Core Processor working at 2595MHz
|
||||||
|
processor 4: AuthenticAMD AMD EPYC 7H12 64-Core Processor working at 2595MHz
|
||||||
|
processor 5: AuthenticAMD AMD EPYC 7H12 64-Core Processor working at 2595MHz
|
||||||
|
processor 6: AuthenticAMD AMD EPYC 7H12 64-Core Processor working at 2595MHz
|
||||||
|
processor 7: AuthenticAMD AMD EPYC 7H12 64-Core Processor working at 2595MHz]
|
||||||
|
system.hw.devices [t|00:00.0 Host bridge: Intel Corporation 440FX - 82441FX PMC [Natoma] (rev 02)
|
||||||
|
00:01.0 ISA bridge: Intel Corporation 82371SB PIIX3 ISA [Natoma/Triton II]
|
||||||
|
00:01.1 IDE interface: Intel Corporation 82371SB PIIX3 IDE [Natoma/Triton II]
|
||||||
|
00:01.2 USB controller: Intel Corporation 82371SB PIIX3 USB [Natoma/Triton II] (rev 01)
|
||||||
|
00:01.3 Bridge: Intel Corporation 82371AB/EB/MB PIIX4 ACPI (rev 03)
|
||||||
|
00:02.0 VGA compatible controller: Device 1234:1111 (rev 02)
|
||||||
|
00:03.0 Unclassified device [00ff]: Red Hat, Inc. Virtio memory balloon
|
||||||
|
00:05.0 PCI bridge: Red Hat, Inc. QEMU PCI-PCI bridge
|
||||||
|
00:12.0 Ethernet controller: Red Hat, Inc. Virtio network device
|
||||||
|
00:1e.0 PCI bridge: Red Hat, Inc. QEMU PCI-PCI bridge
|
||||||
|
00:1f.0 PCI bridge: Red Hat, Inc. QEMU PCI-PCI bridge
|
||||||
|
01:01.0 SCSI storage controller: Red Hat, Inc. Virtio SCSI
|
||||||
|
01:02.0 SCSI storage controller: Red Hat, Inc. Virtio SCSI]
|
||||||
|
system.hw.macaddr [s|[br-dbeb0f52ba9e] f6:ff:ae:c6:f9:ec, [br-e706b66ec89b] fa:51:1e:3c:09:a2, [docker0] 72:b5:05:65:00:4a, [ens18] 1e:a2:11:1a:0e:34, [ens18] 1e:a2:11:1a:0e:34]
|
||||||
|
system.sw.arch [s|x86_64]
|
||||||
|
system.sw.os [s|Linux version 6.8.0-90-generic (buildd@lcy02-amd64-092) (x86_64-linux-gnu-gcc-13 (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0, GNU ld (GNU Binutils for Ubuntu) 2.42) #91-Ubuntu SMP PREEMPT_DYNAMIC Tue Nov 18 14:14:30 UTC 2025]
|
||||||
|
system.sw.os.get [s|{"os_type":"linux","product_name":"Ubuntu 24.04 LTS","architecture":"x86_64","kernel_major":"6","kernel_minor":"8","kernel_patch":"0","kernel":"6.8.0-90-generic","version_pretty":"Ubuntu 24.04 LTS x86_64 6.8.0-90-generic","version_full":"Linux version 6.8.0-90-generic (buildd@lcy02-amd64-092) (x86_64-linux-gnu-gcc-13 (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0, GNU ld (GNU Binutils for Ubuntu) 2.42) #91-Ubuntu SMP PREEMPT_DYNAMIC Tue Nov 18 14:14:30 UTC 2025"}]
|
||||||
|
system.sw.packages [t|[dpkg] adduser, adwaita-icon-theme, alsa-topology-conf, alsa-ucm-conf, amd64-microcode, ansible, ansible-core, apparmor, apport, apport-core-dump-handler, apport-symptoms, appstream, apt, apt-utils, at-spi2-common, at-spi2-core, base-files, base-passwd, bash, bash-completion, bc, bcache-tools, bind9-dnsutils, bind9-host, bind9-libs:amd64, binutils, binutils-common:amd64, binutils-x86-64-linux-gnu, bolt, bpfcc-tools, bpftrace, bsdextrautils, bsdutils, btrfs-progs, build-essential, busybox-initramfs, busybox-static, byobu, bzip2, ca-certificates, cloud-guest-utils, cloud-init, cloud-initramfs-copymods, cloud-initramfs-dyn-netconf, command-not-found, console-setup, console-setup-linux, containerd.io, coreutils, cpio, cpp, cpp-13, cpp-13-x86-64-linux-gnu, cpp-x86-64-linux-gnu, cron, cron-daemon-common, cryptsetup, cryptsetup-bin, cryptsetup-initramfs, curl, dash, dbus, dbus-bin, dbus-daemon, dbus-session-bus-common, dbus-system-bus-common, dbus-user-session, dconf-gsettings-backend:amd64, dconf-service, debconf, debconf-i18n, debianutils, dhcpcd-base, diffutils, dirmngr, distro-info, distro-info-data, dmeventd, dmidecode, dmsetup, docker-buildx-plugin, docker-ce, docker-ce-cli, docker-ce-rootless-extras, docker-compose-plugin, dosfstools, dpkg, dpkg-dev, dracut-install, e2fsprogs, e2fsprogs-l10n, eatmydata, ed, eject, ethtool, fakeroot, fdisk, ffmpeg, file, finalrd, findutils, firmware-sof-signed, fontconfig, fontconfig-config, fonts-dejavu-core, fonts-dejavu-mono, fonts-ubuntu-console, friendly-recovery, ftp, fuse3, fwupd, fwupd-signed, g++, g++-13, g++-13-x86-64-linux-gnu, g++-x86-64-linux-gnu, gawk, gcc, gcc-13, gcc-13-base:amd64, gcc-13-x86-64-linux-gnu, gcc-14-base:amd64, gcc-x86-64-linux-gnu, gdisk, gettext-base, gir1.2-girepository-2.0:amd64, gir1.2-glib-2.0:amd64, gir1.2-packagekitglib-1.0, git, git-man, gnupg, gnupg-l10n, gnupg-utils, gpg, gpg-agent, gpg-wks-client, gpgconf, gpgsm, gpgv, grep, groff-base, grub-common, grub-gfxpayload-lists, grub-pc, grub-pc-bin, grub2-common, gsettings-desktop-schemas, gtk-update-icon-cache, gzip, hdparm, hicolor-icon-theme, hostname, htop, humanity-icon-theme, hwdata, i965-va-driver:amd64, ibverbs-providers:amd64, ieee-data, inetutils-telnet, info, init, init-system-helpers, initramfs-tools, initramfs-tools-bin, initramfs-tools-core, install-info, intel-media-va-driver:amd64, intel-microcode, iproute2, iptables, iptables-persistent, iputils-ping, iputils-tracepath, iso-codes, iucode-tool, javascript-common, jq, kbd, keyboard-configuration, keyboxd, keyutils, klibc-utils, kmod, kpartx, krb5-locales, landscape-common, less, libaacs0:amd64, libacl1:amd64, libaio1t64:amd64, libalgorithm-diff-perl, libalgorithm-diff-xs-perl:amd64, libalgorithm-merge-perl, libaom3:amd64, libapparmor1:amd64, libappstream5:amd64, libapt-pkg6.0t64:amd64, libarchive13t64:amd64, libargon2-1:amd64, libasan8:amd64, libasound2-data, libasound2t64:amd64, libass9:amd64, libassuan0:amd64, libasyncns0:amd64, libatasmart4:amd64, libatk-bridge2.0-0t64:amd64, libatk1.0-0t64:amd64, libatm1t64:amd64, libatomic1:amd64, libatspi2.0-0t64:amd64, libattr1:amd64, libaudit-common, libaudit1:amd64, libavahi-client3:amd64, libavahi-common-data:amd64, libavahi-common3:amd64, libavc1394-0:amd64, libavcodec60:amd64, libavdevice60:amd64, libavfilter9:amd64, libavformat60:amd64, libavutil58:amd64, libbdplus0:amd64, libbinutils:amd64, libblas3:amd64, libblkid1:amd64, libblockdev-crypto3:amd64, libblockdev-fs3:amd64, libblockdev-loop3:amd64, libblockdev-mdraid3:amd64, libblockdev-nvme3:amd64, libblockdev-part3:amd64, libblockdev-swap3:amd64, libblockdev-utils3:amd64, libblockdev3:amd64, libbluray2:amd64, libbpf1:amd64, libbpfcc:amd64, libbrotli1:amd64, libbs2b0:amd64, libbsd0:amd64, libbytesize-common, libbytesize1:amd64, libbz2-1.0:amd64, libc-bin, libc-dev-bin, libc-devtools, libc6-dev:amd64, libc6:amd64, libcaca0:amd64, libcairo-gobject2:amd64, libcairo2:amd64, libcap-ng0:amd64, libcap2-bin, libcap2:amd64, libcbor0.10:amd64, libcc1-0:amd64, libcdio-cdda2t64:amd64, libcdio-paranoia2t64:amd64, libcdio19t64:amd64, libchromaprint1:amd64, libcjson1:amd64, libclang-cpp18, libclang1-18, libcodec2-1.2:amd64, libcolord2:amd64, libcom-err2:amd64, libcrypt-dev:amd64, libcrypt1:amd64, libcryptsetup12:amd64, libctf-nobfd0:amd64, libctf0:amd64, libcups2t64:amd64, libcurl3t64-gnutls:amd64, libcurl4t64:amd64, libdatrie1:amd64, libdav1d7:amd64, libdb5.3t64:amd64, libdbus-1-3:amd64, libdbus-glib-1-2:amd64, libdc1394-25:amd64, libdconf1:amd64, libde265-0:amd64, libdebconfclient0:amd64, libdecor-0-0:amd64, libdecor-0-plugin-1-gtk:amd64, libdeflate0:amd64, libdevmapper-event1.02.1:amd64, libdevmapper1.02.1:amd64, libdpkg-perl, libdrm-amdgpu1:amd64, libdrm-common, libdrm-intel1:amd64, libdrm2:amd64, libduktape207:amd64, libdw1t64:amd64, libeatmydata1:amd64, libedit2:amd64, libefiboot1t64:amd64, libefivar1t64:amd64, libelf1t64:amd64, libepoxy0:amd64, liberror-perl, libestr0:amd64, libevdev2:amLine truncated
|
||||||
|
system.sw.packages.get [t|[{"name":"adduser","manager":"dpkg","version":"3.137ubuntu1","size":462848,"arch":"all","buildtime":{"timestamp":0,"value":""},"installtime":{"timestamp":0,"value":""}},{"name":"adwaita-icon-theme","manager":"dpkg","version":"46.0-1","size":12340224,"arch":"all","buildtime":{"timestamp":0,"value":""},"installtime":{"timestamp":0,"value":""}},{"name":"alsa-topology-conf","manager":"dpkg","version":"1.2.5.1-2","size":430080,"arch":"all","buildtime":{"timestamp":0,"value":""},"installtime":{"timestamp":0,"value":""}},{"name":"alsa-ucm-conf","manager":"dpkg","version":"1.2.10-1ubuntu5.11","size":964608,"arch":"all","buildtime":{"timestamp":0,"value":""},"installtime":{"timestamp":0,"value":""}},{"name":"amd64-microcode","manager":"dpkg","version":"3.20250311.1ubuntu0.24.04.1","size":694272,"arch":"amd64","buildtime":{"timestamp":0,"value":""},"installtime":{"timestamp":0,"value":""}},{"name":"ansible","manager":"dpkg","version":"9.2.0+dfsg-0ubuntu5","size":294230016,"arch":"all","buildtime":{"timestamp":0,"value":""},"installtime":{"timestamp":0,"value":""}},{"name":"ansible-core","manager":"dpkg","version":"2.16.3-0ubuntu2","size":8101888,"arch":"all","buildtime":{"timestamp":0,"value":""},"installtime":{"timestamp":0,"value":""}},{"name":"apparmor","manager":"dpkg","version":"4.0.0-beta3-0ubuntu3","size":3174400,"arch":"amd64","buildtime":{"timestamp":0,"value":""},"installtime":{"timestamp":0,"value":""}},{"name":"apport","manager":"dpkg","version":"2.28.1-0ubuntu2","size":679936,"arch":"all","buildtime":{"timestamp":0,"value":""},"installtime":{"timestamp":0,"value":""}},{"name":"apport-core-dump-handler","manager":"dpkg","version":"2.28.1-0ubuntu2","size":35840,"arch":"all","buildtime":{"timestamp":0,"value":""},"installtime":{"timestamp":0,"value":""}},{"name":"apport-symptoms","manager":"dpkg","version":"0.25","size":61440,"arch":"all","buildtime":{"timestamp":0,"value":""},"installtime":{"timestamp":0,"value":""}},{"name":"appstream","manager":"dpkg","version":"1.0.2-1build6","size":339968,"arch":"amd64","buildtime":{"timestamp":0,"value":""},"installtime":{"timestamp":0,"value":""}},{"name":"apt","manager":"dpkg","version":"2.7.14build2","size":4201472,"arch":"amd64","buildtime":{"timestamp":0,"value":""},"installtime":{"timestamp":0,"value":""}},{"name":"apt-utils","manager":"dpkg","version":"2.7.14build2","size":675840,"arch":"amd64","buildtime":{"timestamp":0,"value":""},"installtime":{"timestamp":0,"value":""}},{"name":"at-spi2-common","manager":"dpkg","version":"2.52.0-1build1","size":53248,"arch":"all","buildtime":{"timestamp":0,"value":""},"installtime":{"timestamp":0,"value":""}},{"name":"at-spi2-core","manager":"dpkg","version":"2.52.0-1build1","size":181248,"arch":"amd64","buildtime":{"timestamp":0,"value":""},"installtime":{"timestamp":0,"value":""}},{"name":"base-files","manager":"dpkg","version":"13ubuntu10","size":436224,"arch":"amd64","buildtime":{"timestamp":0,"value":""},"installtime":{"timestamp":0,"value":""}},{"name":"base-passwd","manager":"dpkg","version":"3.6.3build1","size":260096,"arch":"amd64","buildtime":{"timestamp":0,"value":""},"installtime":{"timestamp":0,"value":""}},{"name":"bash","manager":"dpkg","version":"5.2.21-2ubuntu4","size":1945600,"arch":"amd64","buildtime":{"timestamp":0,"value":""},"installtime":{"timestamp":0,"value":""}},{"name":"bash-completion","manager":"dpkg","version":"1:2.11-8","size":1488896,"arch":"all","buildtime":{"timestamp":0,"value":""},"installtime":{"timestamp":0,"value":""}},{"name":"bc","manager":"dpkg","version":"1.07.1-3ubuntu4","size":218112,"arch":"amd64","buildtime":{"timestamp":0,"value":""},"installtime":{"timestamp":0,"value":""}},{"name":"bcache-tools","manager":"dpkg","version":"1.0.8-5build1","size":108544,"arch":"amd64","buildtime":{"timestamp":0,"value":""},"installtime":{"timestamp":0,"value":""}},{"name":"bind9-dnsutils","manager":"dpkg","version":"1:9.18.24-0ubuntu5","size":518144,"arch":"amd64","buildtime":{"timestamp":0,"value":""},"installtime":{"timestamp":0,"value":""}},{"name":"bind9-host","manager":"dpkg","version":"1:9.18.24-0ubuntu5","size":157696,"arch":"amd64","buildtime":{"timestamp":0,"value":""},"installtime":{"timestamp":0,"value":""}},{"name":"bind9-libs","manager":"dpkg","version":"1:9.18.24-0ubuntu5","size":3465216,"arch":"amd64","buildtime":{"timestamp":0,"value":""},"installtime":{"timestamp":0,"value":""}},{"name":"binutils","manager":"dpkg","version":"2.42-4ubuntu2.10","size":164864,"arch":"amd64","buildtime":{"timestamp":0,"value":""},"installtime":{"timestamp":0,"value":""}},{"name":"binutils-common","manager":"dpkg","version":"2.42-4ubuntu2.10","size":577536,"arch":"amd64","buildtime":{"timestamp":0,"value":""},"installtime":{"timestamp":0,"value":""}},{"name":"binutils-x86-64-linux-gnu","manager":"dpkg","version":"2.42-4ubuntu2.10","size":11826176,"arch":"amd64","buildtime":{"timestamp":0,"value":""},"installtime":{"timestamp":0,"value":""}},{"name":Line truncated
|
||||||
|
system.swap.size[all,free] [u|4105420800]
|
||||||
|
system.swap.in[all] [u|0]
|
||||||
|
system.swap.out[all] [u|17]
|
||||||
|
system.uptime [u|607149]
|
||||||
|
system.boottime [u|1781706509]
|
||||||
|
sensor[w83781d-i2c-0-2d,temp1] [m|ZBX_NOTSUPPORTED] [Cannot obtain sensor information.]
|
||||||
|
net.tcp.service[ssh,127.0.0.1,22] [u|1]
|
||||||
|
net.tcp.service.perf[ssh,127.0.0.1,22] [d|0.0083231925964355469]
|
||||||
|
net.udp.service[ntp,127.0.0.1,123] [u|0]
|
||||||
|
net.udp.service.perf[ntp,127.0.0.1,123] [d|0]
|
||||||
|
system.hostname [s|opsx-server]
|
||||||
|
tcp [t|]
|
||||||
|
ports.discovery [t|/etc/zabbix/zabbix_agentd.d/ports.discovery.py:29: SyntaxWarning: invalid escape sequence '\/'
|
||||||
|
cmd = """netstat -pntl | awk '/[0-9]\//{print $4,$7}' | sed 's/:$//' | sed 's_.*\:__' | sed 's#[ ][0-9].*/#:#g' | sort | uniq | egrep -vw '%s'""" % drop_str
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"{#PNAME}": "systemd",
|
||||||
|
"{#PPORT}": "22"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"{#PNAME}": "tinyproxy",
|
||||||
|
"{#PPORT}": "3128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"{#PNAME}": "docker-prox",
|
||||||
|
"{#PPORT}": "3306"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"{#PNAME}": "registry",
|
||||||
|
"{#PPORT}": "5000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"{#PNAME}": "node",
|
||||||
|
"{#PPORT}": "5173"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"{#PNAME}": "systemd-res",
|
||||||
|
"{#PPORT}": "53"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"{#PNAME}": "docker-prox",
|
||||||
|
"{#PPORT}": "6379"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"{#PNAME}": "docker-proxy",
|
||||||
|
"{#PPORT}": "8080"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"{#PNAME}": "docker-proxy",
|
||||||
|
"{#PPORT}": "80"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"{#PNAME}": "docker-prox",
|
||||||
|
"{#PPORT}": "9999"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}]
|
||||||
Binary file not shown.
Reference in new issue
Block a user