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:
attacker committed 2026-06-24 23:46:52 +08:00
1 parent c7b61968bc
commit 8240a88a84
33 files changed
+2029 -71

No files matched your search

+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+21 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+27
View File
@@ -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 {
+9 -5
View File
@@ -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
View File
@@ -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
}
+16
View File
@@ -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.
+96 -2
View File
@@ -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: "端口策略已保存"})
}
+1 -1
View File
@@ -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
+266
View File
@@ -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)
// ============================================================================
+16
View File
@@ -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 升级时使用)
+35 -5
View File
@@ -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"`
}
+1 -1
View File
@@ -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 专用
+232
View File
@@ -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)
// ============================================================================
+2
View File
@@ -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>
+179
View File
@@ -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 文件句柄 &amp; 上下文切换</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>
);
}
+27
View File
@@ -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 ====================
+218
View File
@@ -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.