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 验证 |
|
||||
| **StatefulSet/DaemonSet 详情页** | ✅ GetStatefulSetDetail/GetDaemonSetDetail + YAML路由修复 (M11) | ✅ WorkloadViewPage 重写 (M11) | ✅ curl 验证 |
|
||||
| **多集群资源对比看板** | ✅ GetMultiClusterComparison (M13) | ✅ ClusterComparePage (M13) | ✅ urllib 验证 |
|
||||
| **Namespace 管理看板** | ✅ GetNamespacesDetail + CRUD (M14) | ✅ NamespaceManagementPage (M14) | ✅ urllib 验证 |
|
||||
|
||||
### ❌ 缺失 / 进行中
|
||||
|
||||
|
||||
+45
@@ -329,3 +329,48 @@ pending → in_progress → done / blocked
|
||||
1. 更新本文件状态
|
||||
2. 更新 PROJECT_EVALUATION.md 对应行
|
||||
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] 详细对比表格可点击跳转集群概览
|
||||
- [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,用于两次采样计算差值
|
||||
type cpuSample struct {
|
||||
total uint64 // 自系统启动以来 CPU 所有状态的累计 jiffies
|
||||
idle uint64 // idle + iowait 的累计 jiffies
|
||||
total uint64 // 自系统启动以来 CPU 所有状态的累计 jiffies
|
||||
idle uint64 // idle + iowait 的累计 jiffies
|
||||
iowait uint64 // iowait 单独累计 jiffies(用于计算 iowait 占比)
|
||||
}
|
||||
|
||||
// CPUCollector CPU 指标采集器。
|
||||
@@ -50,12 +51,18 @@ func (c *CPUCollector) Collect() (model.CPUStat, error) {
|
||||
if c.prev != nil {
|
||||
totalDelta := cur.total - c.prev.total
|
||||
idleDelta := cur.idle - c.prev.idle
|
||||
iowaitDelta := cur.iowait - c.prev.iowait
|
||||
if totalDelta > 0 {
|
||||
// 使用率 = (总增量 - 空闲增量) / 总增量 * 100
|
||||
stat.UsagePercent = float64(totalDelta-idleDelta) / float64(totalDelta) * 100.0
|
||||
if 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
|
||||
@@ -100,6 +107,10 @@ func readCPUSample() (cpuSample, error) {
|
||||
if i == 3 || i == 4 {
|
||||
sample.idle += x
|
||||
}
|
||||
// iowait(索引4) 单独记录
|
||||
if i == 4 {
|
||||
sample.iowait += x
|
||||
}
|
||||
}
|
||||
return sample, nil
|
||||
}
|
||||
|
||||
+15
-5
@@ -76,12 +76,22 @@ func (d *DiskCollector) Collect() ([]model.DiskStat, error) {
|
||||
}
|
||||
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{
|
||||
Mount: m.mountpoint,
|
||||
Device: m.device,
|
||||
TotalGB: bytesToGB(total),
|
||||
UsedGB: bytesToGB(used),
|
||||
UsagePercent: float64(used) / float64(total) * 100.0,
|
||||
Mount: m.mountpoint,
|
||||
Device: m.device,
|
||||
TotalGB: bytesToGB(total),
|
||||
UsedGB: bytesToGB(used),
|
||||
UsagePercent: float64(used) / float64(total) * 100.0,
|
||||
InodeUsagePct: inodePct,
|
||||
})
|
||||
}
|
||||
return disks, nil
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Package collector — 磁盘 IO 指标采集器
|
||||
// 通过读取 /proc/diskstats 获取磁盘读写 BPS 和 IOPS。
|
||||
// 通过读取 /proc/diskstats 获取磁盘读写 BPS、IOPS 和 util(磁盘繁忙程度)。
|
||||
//
|
||||
// 署名 Hermes
|
||||
package collector
|
||||
@@ -23,6 +23,7 @@ type diskIOSample struct {
|
||||
writeSects uint64
|
||||
readOps uint64
|
||||
writeOps uint64
|
||||
ioTicks uint64 // io_ticks (field 12) — 磁盘忙碌的毫秒累计
|
||||
}
|
||||
|
||||
// DiskIOCollector 磁盘 IO 采集器
|
||||
@@ -40,7 +41,7 @@ func NewDiskIOCollector() *DiskIOCollector {
|
||||
func (c *DiskIOCollector) Collect() (model.DiskIOStat, error) {
|
||||
stat := model.DiskIOStat{}
|
||||
|
||||
rSects, wSects, rOps, wOps, err := readDiskStats()
|
||||
rSects, wSects, rOps, wOps, ioTicks, err := readDiskStats()
|
||||
if err != nil {
|
||||
return stat, fmt.Errorf("读取磁盘 IO 失败: %w", err)
|
||||
}
|
||||
@@ -55,6 +56,7 @@ func (c *DiskIOCollector) Collect() (model.DiskIOStat, error) {
|
||||
writeSects: wSects,
|
||||
readOps: rOps,
|
||||
writeOps: wOps,
|
||||
ioTicks: ioTicks,
|
||||
}
|
||||
|
||||
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.ReadIOPS = float64(cur.readOps-c.prev.readOps) / 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
|
||||
@@ -73,11 +84,11 @@ func (c *DiskIOCollector) Collect() (model.DiskIOStat, error) {
|
||||
}
|
||||
|
||||
// readDiskStats 读取 /proc/diskstats,汇总所有物理磁盘的读写数据
|
||||
// fields[2]=设备名, fields[3]=读完成次数, fields[5]=读扇区数, fields[7]=写完成次数, fields[9]=写扇区数
|
||||
func readDiskStats() (rSects, wSects, rOps, wOps uint64, err error) {
|
||||
// fields[2]=设备名, fields[3]=读完成次数, fields[5]=读扇区数, fields[7]=写完成次数, fields[9]=写扇区数, fields[12]=io_ticks
|
||||
func readDiskStats() (rSects, wSects, rOps, wOps, ioTicks uint64, err error) {
|
||||
f, err := os.Open("/proc/diskstats")
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, err
|
||||
return 0, 0, 0, 0, 0, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
@@ -103,6 +114,7 @@ func readDiskStats() (rSects, wSects, rOps, wOps uint64, err error) {
|
||||
// field 5 = 读扇区数 (rd_sectors)
|
||||
// field 7 = 写完成次数 (wr_ios)
|
||||
// field 9 = 写扇区数 (wr_sectors)
|
||||
// field 12 = io_ticks (milliseconds spent doing I/O)
|
||||
if v, e := strconv.ParseUint(fields[3], 10, 64); e == nil {
|
||||
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 {
|
||||
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"]
|
||||
availableKB := values["MemAvailable"]
|
||||
freeKB := values["MemFree"]
|
||||
swapTotalKB := values["SwapTotal"]
|
||||
swapFreeKB := values["SwapFree"]
|
||||
|
||||
// 若内核未提供 MemAvailable(较老版本),则按传统方式估算可用内存
|
||||
if availableKB == 0 {
|
||||
@@ -48,10 +50,18 @@ func (m *MemCollector) Collect() (model.MemoryStat, error) {
|
||||
usedKB = totalKB - availableKB
|
||||
}
|
||||
|
||||
// Swap used
|
||||
swapUsedKB := uint64(0)
|
||||
if swapTotalKB > swapFreeKB {
|
||||
swapUsedKB = swapTotalKB - swapFreeKB
|
||||
}
|
||||
|
||||
stat := model.MemoryStat{
|
||||
TotalGB: float64(totalKB) * kbToGB,
|
||||
UsedGB: float64(usedKB) * kbToGB,
|
||||
CachedGB: float64(buffersKB+cachedKB) * kbToGB,
|
||||
TotalGB: float64(totalKB) * kbToGB,
|
||||
UsedGB: float64(usedKB) * kbToGB,
|
||||
CachedGB: float64(buffersKB+cachedKB) * kbToGB,
|
||||
AvailableGB: float64(availableKB) * kbToGB,
|
||||
SwapUsedGB: float64(swapUsedKB) * kbToGB,
|
||||
}
|
||||
if totalKB > 0 {
|
||||
stat.UsagePercent = float64(usedKB) / float64(totalKB) * 100.0
|
||||
|
||||
+25
-14
@@ -18,9 +18,11 @@ import (
|
||||
|
||||
// netSample 保存上一次的网络采样数据,用于计算速率
|
||||
type netSample struct {
|
||||
time time.Time
|
||||
rxBytes uint64
|
||||
txBytes uint64
|
||||
time time.Time
|
||||
rxBytes uint64
|
||||
txBytes uint64
|
||||
rxErrors uint64
|
||||
txErrors uint64
|
||||
}
|
||||
|
||||
// NetworkCollector 网络指标采集器
|
||||
@@ -34,7 +36,7 @@ func NewNetworkCollector() *NetworkCollector {
|
||||
return &NetworkCollector{}
|
||||
}
|
||||
|
||||
// Collect 采集网络指标(并发连接数、TIME_WAIT、网卡 rx/tx B/s)
|
||||
// Collect 采集网络指标(并发连接数、TIME_WAIT、网卡 rx/tx B/s、错误包)
|
||||
func (c *NetworkCollector) Collect() (model.NetworkStat, error) {
|
||||
stat := model.NetworkStat{}
|
||||
|
||||
@@ -46,8 +48,8 @@ func (c *NetworkCollector) Collect() (model.NetworkStat, error) {
|
||||
}
|
||||
stat.TCPCloseWait = countCloseWait()
|
||||
|
||||
// 2. 采集网卡流量(取所有非 lo 网卡合计)
|
||||
curRx, curTx, nicName, err := readNetDevTotal()
|
||||
// 2. 采集网卡流量 + 错误包(取所有非 lo 网卡合计)
|
||||
curRx, curTx, curRxErr, curTxErr, nicName, err := readNetDevTotal()
|
||||
if err != nil {
|
||||
return stat, fmt.Errorf("读取网卡流量失败: %w", err)
|
||||
}
|
||||
@@ -57,7 +59,7 @@ func (c *NetworkCollector) Collect() (model.NetworkStat, error) {
|
||||
defer c.mu.Unlock()
|
||||
|
||||
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) {
|
||||
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.NetRxErrors = int64(cur.rxErrors)
|
||||
stat.NetTxErrors = int64(cur.txErrors)
|
||||
|
||||
c.prev = cur
|
||||
|
||||
return stat, nil
|
||||
@@ -120,11 +127,11 @@ func readSockStat() (tcpSockStat, error) {
|
||||
return result, scanner.Err()
|
||||
}
|
||||
|
||||
// readNetDevTotal 读取 /proc/net/dev,取第一个非 lo 且活跃的物理网卡总流量
|
||||
func readNetDevTotal() (rxBytes, txBytes uint64, nicName string, err error) {
|
||||
// readNetDevTotal 读取 /proc/net/dev,取第一个非 lo 且活跃的物理网卡总流量 + 错误包
|
||||
func readNetDevTotal() (rxBytes, txBytes, rxErrors, txErrors uint64, nicName string, err error) {
|
||||
f, err := os.Open("/proc/net/dev")
|
||||
if err != nil {
|
||||
return 0, 0, "", err
|
||||
return 0, 0, 0, 0, "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
@@ -149,26 +156,30 @@ func readNetDevTotal() (rxBytes, txBytes uint64, nicName string, err error) {
|
||||
}
|
||||
|
||||
fields := strings.Fields(parts[1])
|
||||
if len(fields) < 10 {
|
||||
if len(fields) < 11 {
|
||||
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)
|
||||
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 {
|
||||
nicName = name
|
||||
rxBytes += rb
|
||||
txBytes += tb
|
||||
rxErrors += re
|
||||
txErrors += te
|
||||
}
|
||||
}
|
||||
|
||||
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 连接数
|
||||
|
||||
+71
-2
@@ -2,6 +2,8 @@
|
||||
// 本文件实现监听 TCP 端口的自动发现:
|
||||
// - 扫描 /proc/net/tcp 与 /proc/net/tcp6 中状态为 LISTEN(0A) 的套接字;
|
||||
// - 通过遍历 /proc/*/fd 反查套接字 inode 归属的进程。
|
||||
// - 内置系统进程排除列表(sshd、rpcbind、master 等永远不上报)。
|
||||
// - 支持远程端口策略:whitelist(全量上报,默认)/ blacklist(只上报白名单端口)。
|
||||
//
|
||||
// 署名 Logan.Li
|
||||
package collector
|
||||
@@ -15,14 +17,47 @@ import (
|
||||
"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 监听端口自动发现采集器
|
||||
type PortCollector struct{}
|
||||
type PortCollector struct {
|
||||
Policy *model.PortPolicy // 可选:后端下发的端口过滤策略
|
||||
}
|
||||
|
||||
// NewPortCollector 创建端口采集器实例
|
||||
func NewPortCollector() *PortCollector {
|
||||
return &PortCollector{}
|
||||
}
|
||||
|
||||
// SetPolicy 设置端口过滤策略(daemon 模式下从后端轮询获得)
|
||||
func (p *PortCollector) SetPolicy(policy *model.PortPolicy) {
|
||||
p.Policy = policy
|
||||
}
|
||||
|
||||
// listenSocket 描述一个处于 LISTEN 状态的套接字
|
||||
type listenSocket struct {
|
||||
port int
|
||||
@@ -47,7 +82,7 @@ func (p *PortCollector) Collect() ([]model.PortStat, error) {
|
||||
// 2. 构建 inode -> 进程信息 映射(仅 Linux 上可用)
|
||||
inodeMap := p.buildInodeMap()
|
||||
|
||||
// 3. 组装结果并按 (端口, 协议, PID) 去重
|
||||
// 3. 组装结果并按 (端口, 协议, PID) 去重,应用过滤策略
|
||||
seen := make(map[string]bool)
|
||||
ports := make([]model.PortStat, 0, len(sockets))
|
||||
for _, s := range sockets {
|
||||
@@ -55,6 +90,22 @@ func (p *PortCollector) Collect() ([]model.PortStat, error) {
|
||||
if info, ok := inodeMap[s.inode]; ok {
|
||||
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)
|
||||
if seen[key] {
|
||||
continue
|
||||
@@ -200,3 +251,21 @@ func readProcessName(pid string) string {
|
||||
}
|
||||
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 {
|
||||
case "version", "v", "-v", "--version":
|
||||
fmt.Printf("AIOps Agent %s (built %s) go=%s\n", version, buildTime, runtime.Version())
|
||||
return
|
||||
case "output":
|
||||
runOutput(cfg)
|
||||
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
|
||||
go func() {
|
||||
pollAndExecute()
|
||||
pullProbes()
|
||||
pullPortPolicy()
|
||||
ticker := time.NewTicker(pollInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
@@ -290,6 +308,7 @@ func main() {
|
||||
case <-ticker.C:
|
||||
pollAndExecute()
|
||||
pullProbes()
|
||||
pullPortPolicy()
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
@@ -404,6 +423,7 @@ func runOutput(cfg Config) {
|
||||
|
||||
// CPU
|
||||
pd("cpu.usage_percent", report.CPU.UsagePercent)
|
||||
pd("cpu.iowait", report.CPU.IOWait)
|
||||
pu("cpu.core_count", report.CPU.CoreCount)
|
||||
pd("cpu.load1", report.CPU.Load1)
|
||||
pd("cpu.load5", report.CPU.Load5)
|
||||
@@ -413,6 +433,8 @@ func runOutput(cfg Config) {
|
||||
pd("memory.total_gb", report.Memory.TotalGB)
|
||||
pd("memory.used_gb", report.Memory.UsedGB)
|
||||
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)
|
||||
|
||||
// Swap
|
||||
@@ -428,6 +450,7 @@ func runOutput(cfg Config) {
|
||||
pd(prefix+".total_gb", d.TotalGB)
|
||||
pd(prefix+".used_gb", d.UsedGB)
|
||||
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.close_wait", report.Network.TCPCloseWait)
|
||||
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.tx_bps", report.Network.NetTxBPS)
|
||||
pu64("net.rx_errors", report.Network.NetRxErrors)
|
||||
pu64("net.tx_errors", report.Network.NetTxErrors)
|
||||
|
||||
// 磁盘 IO
|
||||
pd("disk_io.read_bps", report.DiskIO.ReadBPS)
|
||||
pd("disk_io.write_bps", report.DiskIO.WriteBPS)
|
||||
pd("disk_io.read_iops", report.DiskIO.ReadIOPS)
|
||||
pd("disk_io.write_iops", report.DiskIO.WriteIOPS)
|
||||
pd("disk_io.util", report.DiskIO.Util)
|
||||
|
||||
// 进程 TOP
|
||||
for i, pr := range report.ProcessTop {
|
||||
|
||||
@@ -28,6 +28,7 @@ type Report struct {
|
||||
// CPUStat CPU 相关指标
|
||||
type CPUStat struct {
|
||||
UsagePercent float64 `json:"usage_percent"` // CPU 总体使用率(百分比)
|
||||
IOWait float64 `json:"iowait"` // CPU 等待 IO 的时间占比(百分比)
|
||||
CoreCount int `json:"core_count"` // CPU 逻辑核心数
|
||||
Load1 float64 `json:"load1"` // 1 分钟平均负载
|
||||
Load5 float64 `json:"load5"` // 5 分钟平均负载
|
||||
@@ -39,16 +40,19 @@ type MemoryStat struct {
|
||||
TotalGB float64 `json:"total_gb"` // 总内存(GB)
|
||||
UsedGB float64 `json:"used_gb"` // 已用内存(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"` // 内存使用率(百分比)
|
||||
}
|
||||
|
||||
// DiskStat 单个挂载点的磁盘使用情况
|
||||
type DiskStat struct {
|
||||
Mount string `json:"mount"` // 挂载点
|
||||
Device string `json:"device"` // 设备名
|
||||
TotalGB float64 `json:"total_gb"` // 总容量(GB)
|
||||
UsedGB float64 `json:"used_gb"` // 已用容量(GB)
|
||||
UsagePercent float64 `json:"usage_percent"` // 使用率(百分比)
|
||||
Mount string `json:"mount"` // 挂载点
|
||||
Device string `json:"device"` // 设备名
|
||||
TotalGB float64 `json:"total_gb"` // 总容量(GB)
|
||||
UsedGB float64 `json:"used_gb"` // 已用容量(GB)
|
||||
UsagePercent float64 `json:"usage_percent"` // 使用率(百分比)
|
||||
InodeUsagePct float64 `json:"inode_usage_pct"` // inode 使用率(百分比)— 防止文件数爆炸
|
||||
}
|
||||
|
||||
// PortStat 监听端口及其归属进程
|
||||
|
||||
+41
-16
@@ -9,6 +9,8 @@ type NetworkStat struct {
|
||||
NicName string `json:"nic_name"` // 主网卡名
|
||||
NetRxBPS float64 `json:"net_rx_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 相关指标
|
||||
@@ -17,6 +19,7 @@ type DiskIOStat struct {
|
||||
WriteBPS float64 `json:"write_bps"` // 写入速率 (bytes/s)
|
||||
ReadIOPS float64 `json:"read_iops"` // 读取 IOPS (count/s)
|
||||
WriteIOPS float64 `json:"write_iops"` // 写入 IOPS (count/s)
|
||||
Util float64 `json:"util"` // 磁盘繁忙程度(百分比)— 判断磁盘是否瓶颈的核心指标
|
||||
}
|
||||
|
||||
// SwapStat 交换分区指标(OOM 前兆判断)
|
||||
@@ -28,34 +31,34 @@ type SwapStat struct {
|
||||
|
||||
// SocketFDStat 系统级 FD 使用情况
|
||||
type SocketFDStat struct {
|
||||
Allocated int `json:"allocated"` // 已分配的 file handles (fs.file-nr 第一列)
|
||||
Max int64 `json:"max"` // 系统上限 (fs.file-max)
|
||||
Allocated int `json:"allocated"` // 已分配的 file handles (fs.file-nr 第一列)
|
||||
Max int64 `json:"max"` // 系统上限 (fs.file-max)
|
||||
UsagePct float64 `json:"usage_pct"`
|
||||
}
|
||||
|
||||
// ContextSwitchStat 上下文切换(决定是否开中断合并/CPU 隔离)
|
||||
type ContextSwitchStat struct {
|
||||
CTXPerSec float64 `json:"ctx_per_sec"` // 每秒上下文切换次数
|
||||
IRQPerSec float64 `json:"irq_per_sec"` // 每秒中断次数
|
||||
CTXPerSec float64 `json:"ctx_per_sec"` // 每秒上下文切换次数
|
||||
IRQPerSec float64 `json:"irq_per_sec"` // 每秒中断次数
|
||||
}
|
||||
|
||||
// AgentHeartbeat Agent 自检心跳(随每次上报附带,极小 payload)
|
||||
type AgentHeartbeat struct {
|
||||
Version string `json:"version"`
|
||||
UptimeSec int64 `json:"uptime_sec"`
|
||||
LastErr string `json:"last_err,omitempty"`
|
||||
SelfCPU float64 `json:"self_cpu_pct"`
|
||||
SelfRSSMB float64 `json:"self_rss_mb"`
|
||||
ReportSeq uint64 `json:"report_seq"` // 单调递增序列号,用于丢包检测
|
||||
Version string `json:"version"`
|
||||
UptimeSec int64 `json:"uptime_sec"`
|
||||
LastErr string `json:"last_err,omitempty"`
|
||||
SelfCPU float64 `json:"self_cpu_pct"`
|
||||
SelfRSSMB float64 `json:"self_rss_mb"`
|
||||
ReportSeq uint64 `json:"report_seq"` // 单调递增序列号,用于丢包检测
|
||||
}
|
||||
|
||||
// SystemStat 系统信息(启动时采集一次)
|
||||
type SystemStat struct {
|
||||
OSVersion string `json:"os_version"` // OS 版本 (如 "Ubuntu 22.04")
|
||||
Kernel string `json:"kernel"` // 内核版本
|
||||
Uptime int64 `json:"uptime"` // 运行秒数
|
||||
UlimitNofile int64 `json:"ulimit_nofile"` // ulimit -n (最大文件描述符)
|
||||
UlimitNproc int64 `json:"ulimit_nproc"` // ulimit -u (最大进程数)
|
||||
OSVersion string `json:"os_version"` // OS 版本 (如 "Ubuntu 22.04")
|
||||
Kernel string `json:"kernel"` // 内核版本
|
||||
Uptime int64 `json:"uptime"` // 运行秒数
|
||||
UlimitNofile int64 `json:"ulimit_nofile"` // ulimit -n (最大文件描述符)
|
||||
UlimitNproc int64 `json:"ulimit_nproc"` // ulimit -u (最大进程数)
|
||||
}
|
||||
|
||||
// ──── v2.1 健康探针 ────
|
||||
@@ -80,8 +83,30 @@ type ProbeResult struct {
|
||||
ProbeID uint `json:"probe_id"`
|
||||
Name string `json:"name"`
|
||||
ProbeType string `json:"probe_type"`
|
||||
Status string `json:"status"` // up / down
|
||||
Status string `json:"status"` // up / down
|
||||
LatencyMs int `json:"latency_ms"`
|
||||
StatusCode int `json:"status_code,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"
|
||||
resultPathFmt = "/api/v1/agent/tasks/%s/result"
|
||||
probePath = "/api/v1/agent/health-probes/config"
|
||||
portPolicyPath = "/api/v1/agent/port-policy"
|
||||
)
|
||||
|
||||
// Client 负责与后端任务/探针接口通信。
|
||||
@@ -74,3 +75,18 @@ func (c *Client) PollProbes(ctx context.Context) ([]model.ProbeConfig, error) {
|
||||
}
|
||||
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{
|
||||
Hostname: req.Hostname,
|
||||
CPUUsage: req.CPU.UsagePercent,
|
||||
CPUIOwait: req.CPU.IOWait,
|
||||
CPUCores: req.CPU.CoreCount,
|
||||
Load1: req.CPU.Load1,
|
||||
Load5: req.CPU.Load5,
|
||||
@@ -69,6 +70,8 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
|
||||
MemTotal: req.Memory.TotalGB,
|
||||
MemUsed: req.Memory.UsedGB,
|
||||
MemUsage: req.Memory.UsagePercent,
|
||||
MemAvailable: req.Memory.AvailableGB,
|
||||
MemSwapUsed: req.Memory.SwapUsedGB,
|
||||
DiskInfo: string(diskJSON),
|
||||
PortInfo: string(portJSON),
|
||||
NetInfo: string(netJSON),
|
||||
@@ -110,7 +113,7 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
|
||||
// 直接 SQL 更新 asset_servers 监控字段
|
||||
// 若 AgentVersion 非空则同步写入,否则保留数据库中已有版本号(防止旧版Agent空字段覆盖已设置的版本)
|
||||
updateSQL := `UPDATE asset_servers SET
|
||||
cpu_usage = ?, memory_usage = ?, disk_usage = ?,
|
||||
cpu_usage = ?, cpu_iowait = ?, memory_usage = ?, disk_usage = ?,
|
||||
cpu = ?, memory = ?, disk = ?,
|
||||
status = ?, agent_status = ?` + func() string {
|
||||
if req.AgentVersion != "" {
|
||||
@@ -121,7 +124,7 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
|
||||
hostname = CASE WHEN hostname != ? AND ip = ? THEN ? ELSE hostname END,
|
||||
updated_at = NOW()`
|
||||
args := []interface{}{
|
||||
req.CPU.UsagePercent, req.Memory.UsagePercent, diskAvg,
|
||||
req.CPU.UsagePercent, req.CPU.IOWait, req.Memory.UsagePercent, diskAvg,
|
||||
req.CPU.CoreCount, memMB, diskGB,
|
||||
"running", "online",
|
||||
}
|
||||
@@ -528,3 +531,94 @@ func avgDiskUsage(disks []models.DiskMetric) float64 {
|
||||
}
|
||||
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}
|
||||
// @Router /dashboard/metrics [get]
|
||||
func (h *DashboardHandler) GetMetrics(c *gin.Context) {
|
||||
hours := 24
|
||||
hours := 6 // 默认6小时,减少扫描行数
|
||||
if hv := c.Query("hours"); hv != "" {
|
||||
if parsed, err := strconv.Atoi(hv); err == nil && parsed > 0 && parsed <= 168 {
|
||||
hours = parsed
|
||||
|
||||
@@ -1740,6 +1740,272 @@ func (h *KubernetesHandler) ListNamespaces(c *gin.Context) {
|
||||
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)
|
||||
// ============================================================================
|
||||
|
||||
@@ -389,6 +389,11 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
||||
|
||||
// Namespace list
|
||||
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)
|
||||
kubernetesRouter.GET("/:clusterCode/deployments", k8sHandler.ListDeploymentsAllNS)
|
||||
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.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
|
||||
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/statefulsets", k8sHandler.ListStatefulSetsNS)
|
||||
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)
|
||||
// 健康探针:Agent 拉取本机配置(无需 JWT)
|
||||
agentGroup.GET("/health-probes/config", agentHandler.GetHealthProbesForAgent)
|
||||
// 端口策略:Agent 拉取本机端口过滤策略(无需 JWT)
|
||||
agentGroup.GET("/port-policy", agentHandler.GetPortPolicyForAgent)
|
||||
}
|
||||
// Agent 数据查询 + 任务管理(需要 JWT)
|
||||
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.PUT("/health-probes/:id", agentHandler.UpdateHealthProbe)
|
||||
agentQuery.DELETE("/health-probes/:id", agentHandler.DeleteHealthProbe)
|
||||
|
||||
// 端口策略(CRUD)
|
||||
agentQuery.GET("/port-policies/:hostname", agentHandler.GetPortPolicy)
|
||||
agentQuery.PUT("/port-policies/:hostname", agentHandler.SavePortPolicy)
|
||||
}
|
||||
|
||||
// Agent 二进制下载(无需鉴权,Agent 升级时使用)
|
||||
|
||||
@@ -9,6 +9,7 @@ type AgentReport struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
Hostname string `gorm:"size:255;index;not null" json:"hostname"`
|
||||
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"`
|
||||
Load1 float64 `gorm:"type:decimal(8,2)" json:"load1"`
|
||||
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"`
|
||||
MemUsed float64 `gorm:"type:decimal(10,2)" json:"mem_used"`
|
||||
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"`
|
||||
PortInfo string `gorm:"type:text" json:"port_info"`
|
||||
NetInfo string `gorm:"type:text" json:"net_info"`
|
||||
@@ -141,6 +144,7 @@ type SystemStatModel struct {
|
||||
|
||||
type CPUMetric struct {
|
||||
UsagePercent float64 `json:"usage_percent"`
|
||||
IOWait float64 `json:"iowait"`
|
||||
CoreCount int `json:"core_count"`
|
||||
Load1 float64 `json:"load1"`
|
||||
Load5 float64 `json:"load5"`
|
||||
@@ -151,15 +155,18 @@ type MemMetric struct {
|
||||
TotalGB float64 `json:"total_gb"`
|
||||
UsedGB float64 `json:"used_gb"`
|
||||
CachedGB float64 `json:"cached_gb"`
|
||||
AvailableGB float64 `json:"available_gb"`
|
||||
SwapUsedGB float64 `json:"swap_used_gb"`
|
||||
UsagePercent float64 `json:"usage_percent"`
|
||||
}
|
||||
|
||||
type DiskMetric struct {
|
||||
Mount string `json:"mount"`
|
||||
Device string `json:"device"`
|
||||
TotalGB float64 `json:"total_gb"`
|
||||
UsedGB float64 `json:"used_gb"`
|
||||
UsagePercent float64 `json:"usage_percent"`
|
||||
Mount string `json:"mount"`
|
||||
Device string `json:"device"`
|
||||
TotalGB float64 `json:"total_gb"`
|
||||
UsedGB float64 `json:"used_gb"`
|
||||
UsagePercent float64 `json:"usage_percent"`
|
||||
InodeUsagePct float64 `json:"inode_usage_pct"`
|
||||
}
|
||||
|
||||
type PortMetric struct {
|
||||
@@ -177,6 +184,8 @@ type NetworkStatModel struct {
|
||||
NicName string `json:"nic_name"`
|
||||
NetRxBPS float64 `json:"net_rx_bps"`
|
||||
NetTxBPS float64 `json:"net_tx_bps"`
|
||||
NetRxErrors int64 `json:"net_rx_errors"`
|
||||
NetTxErrors int64 `json:"net_tx_errors"`
|
||||
}
|
||||
|
||||
type DiskIOStatModel struct {
|
||||
@@ -184,4 +193,25 @@ type DiskIOStatModel struct {
|
||||
WriteBPS float64 `json:"write_bps"`
|
||||
ReadIOPS float64 `json:"read_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"`
|
||||
Name string `gorm:"size:255;not null" json:"name"`
|
||||
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"` // > >= < <= ==
|
||||
Threshold float64 `gorm:"type:decimal(10,2)" json:"threshold"`
|
||||
Port int `json:"port"` // port_down 专用
|
||||
|
||||
@@ -1023,6 +1023,134 @@ func (c *Client) ListNamespaces() ([]string, error) {
|
||||
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 ---
|
||||
|
||||
// ConfigMapList response
|
||||
@@ -1366,6 +1494,110 @@ func (c *Client) ListEvents(namespace string) (*EventList, error) {
|
||||
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)
|
||||
// ============================================================================
|
||||
|
||||
@@ -21,6 +21,7 @@ const KubernetesClusters = lazy(() => import('@/components/kubernetes/Kubernetes
|
||||
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 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 DeploymentDetailPage = lazy(() => import('@/components/kubernetes/DeploymentDetailPage').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="events" element={<LazyPage><EventsPage /></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="topology" element={<LazyPage><TopologyView /></LazyPage>} />
|
||||
<Route path="namespace/:namespace">
|
||||
|
||||
@@ -85,8 +85,8 @@ export function Dashboard() {
|
||||
|
||||
useEffect(() => {
|
||||
refreshData();
|
||||
// 每 5 分钟自动刷新
|
||||
const interval = setInterval(refreshData, 5 * 60 * 1000);
|
||||
// 每 5 秒自动刷新
|
||||
const interval = setInterval(refreshData, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, [refreshData]);
|
||||
|
||||
|
||||
@@ -202,9 +202,14 @@ export default function ClusterOverviewPage() {
|
||||
|
||||
{/* Namespace list */}
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-slate-100 mb-3 flex items-center gap-2">
|
||||
<Boxes className="h-4 w-4 text-emerald-400" />命名空间
|
||||
</h2>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
|
||||
<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">
|
||||
{namespaces.map((ns) => {
|
||||
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 =
|
||||
| 'deployments' | 'pods' | 'services' | 'ingresses'
|
||||
| 'configmaps' | 'secrets' | 'hpas' | 'cronjobs'
|
||||
| 'pvcs' | 'statefulsets' | 'daemonsets';
|
||||
| 'pvcs' | 'statefulsets' | 'daemonsets'
|
||||
| 'serviceaccounts' | 'roles' | 'rolebindings';
|
||||
|
||||
const RESOURCE_CONFIG: Record<ResourceType, {
|
||||
title: string;
|
||||
@@ -228,6 +229,88 @@ const RESOURCE_CONFIG: Record<ResourceType, {
|
||||
</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() {
|
||||
@@ -295,6 +378,9 @@ export default function NamespaceResourcePage() {
|
||||
case 'pvcs': res = await kubernetesApi.getPVCs(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 '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 || []));
|
||||
} catch { setData([]); } finally { setLoading(false); }
|
||||
@@ -332,6 +418,9 @@ export default function NamespaceResourcePage() {
|
||||
{ type: 'secrets' as ResourceType, label: 'Secrets', icon: Shield },
|
||||
{ type: 'hpas' as ResourceType, label: 'HPAs', icon: Activity },
|
||||
{ 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>;
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function HostMonitorDashboard() {
|
||||
const [hosts, setHosts] = useState<any[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(15);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [search, setSearch] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
@@ -441,9 +441,8 @@ export default function HostMonitorDashboard() {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="10">10</SelectItem>
|
||||
<SelectItem value="15">15</SelectItem>
|
||||
<SelectItem value="20">20</SelectItem>
|
||||
<SelectItem value="50">50</SelectItem>
|
||||
<SelectItem value="30">30</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span>条,共 {total} 条</span>
|
||||
|
||||
@@ -102,6 +102,36 @@ export function HostMonitor() {
|
||||
}).catch(() => setProbes([]));
|
||||
}, [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(() => {
|
||||
fetchProbes();
|
||||
}, [fetchProbes]);
|
||||
@@ -143,25 +173,35 @@ export function HostMonitor() {
|
||||
const rawRx: number[] = []; const rawTx: number[] = [];
|
||||
const rawIoR: number[] = []; const rawIoW: 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[] = [];
|
||||
|
||||
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);
|
||||
timestamps.push(ts);
|
||||
rawCpu.push(r.cpu_usage || 0);
|
||||
rawIOWait.push(r.cpu_iowait || 0);
|
||||
rawMem.push(r.mem_usage || 0);
|
||||
rawMemAvail.push(r.mem_available || 0);
|
||||
rawLoad1.push(r.load1 || 0);
|
||||
rawDisk.push(r.disk_usage || 0);
|
||||
|
||||
const net = parseJSON(r.net_info);
|
||||
rawTcp.push(net.tcp_established || 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);
|
||||
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);
|
||||
rawIoR.push(dio.read_bps || 0);
|
||||
rawIoW.push(dio.write_bps || 0);
|
||||
rawIoUtil.push(dio.util || 0);
|
||||
|
||||
const swap = parseJSON(r.swap_info);
|
||||
rawSwap.push(swap?.usage_percent || 0);
|
||||
@@ -183,6 +223,13 @@ export function HostMonitor() {
|
||||
ioWrite: bucketWithStats(rawIoW.map(v => v / 1024), timestamps, timeConfig.buckets),
|
||||
swap: bucketWithStats(rawSwap, 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]);
|
||||
|
||||
@@ -280,8 +327,14 @@ export function HostMonitor() {
|
||||
<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" />}
|
||||
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" />}
|
||||
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" />}
|
||||
data={metrics.load1} color="#8b5cf6" unit="" domain={[0, 'auto']} aggType={aggType} />
|
||||
</TabsContent>
|
||||
@@ -295,6 +348,8 @@ export function HostMonitor() {
|
||||
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" />}
|
||||
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>
|
||||
</TabsContent>
|
||||
|
||||
@@ -305,6 +360,8 @@ export function HostMonitor() {
|
||||
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" />}
|
||||
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>
|
||||
{/* 网卡带宽吞吐 — 双线 rx/tx */}
|
||||
<DualLineChart
|
||||
@@ -314,6 +371,12 @@ export function HostMonitor() {
|
||||
rxData={metrics.rx} txData={metrics.tx}
|
||||
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>
|
||||
|
||||
{/* ═══ 端口监控 ═══ */}
|
||||
@@ -322,6 +385,22 @@ export function HostMonitor() {
|
||||
<CardContent className="p-4">
|
||||
<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}
|
||||
<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>
|
||||
{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">
|
||||
@@ -492,6 +571,39 @@ export function HostMonitor() {
|
||||
</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 && (() => {
|
||||
const disk = parseJSON(latest.disk_info);
|
||||
@@ -695,6 +807,73 @@ export function HostMonitor() {
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -444,6 +444,17 @@ export const kubernetesApi = {
|
||||
getDaemonSets: async (clusterCode: string, namespace: string) => {
|
||||
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}`); },
|
||||
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 }); },
|
||||
@@ -532,6 +543,19 @@ export const kubernetesApi = {
|
||||
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 ====================
|
||||
@@ -890,6 +914,9 @@ export const agentApi = {
|
||||
createHealthProbe: (data: any) => http.post('/agent/health-probes', data),
|
||||
updateHealthProbe: (id: number, data: any) => http.put(`/agent/health-probes/${id}`, data),
|
||||
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 ====================
|
||||
|
||||
@@ -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