From 402e183d2fa9773e3152a3986d8297e2b0d5eb70 Mon Sep 17 00:00:00 2001 From: lijiahangmax Date: Mon, 14 Jul 2025 19:06:36 +0800 Subject: [PATCH 01/17] =?UTF-8?q?:pencil2:=20=E4=BF=AE=E6=94=B9=E6=96=87?= =?UTF-8?q?=E6=A1=A3.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- NOTICE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NOTICE b/NOTICE index 64df0a06..5896e2ac 100644 --- a/NOTICE +++ b/NOTICE @@ -5,5 +5,5 @@ 1. 禁止修改或删除 LICENSE 文件。 2. 禁止修改或删除源码头部的版权声明。 3. 本项目可免费商业使用,商业使用请保留项目源码、出处、描述文件和作者声明等。 - 4. 分发源码时候,请注明软件出处 https://visor.dromara.org/ + 4. 分发源码时候,请注明软件出处 https://visor.orionsec.cn/ 5. 不可二次开发或参与同类竞品的开发。 From a3476596dde422cee56f8369ab871f4318869af9 Mon Sep 17 00:00:00 2001 From: lijiahangmax Date: Sun, 10 Aug 2025 19:26:33 +0800 Subject: [PATCH 02/17] =?UTF-8?q?:hammer:=20=E4=BF=AE=E6=94=B9=E9=A2=9D?= =?UTF-8?q?=E5=A4=96=E9=85=8D=E7=BD=AE=E5=AD=97=E6=AE=B5=E5=90=8D=E7=A7=B0?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- orion-visor-ui/src/api/asset/host-extra.ts | 9 ++- .../host-list/components/host-card-list.vue | 2 +- .../host-list/components/host-form-spec.vue | 79 ++++++++++--------- .../asset/host-list/components/host-table.vue | 2 +- 4 files changed, 48 insertions(+), 44 deletions(-) diff --git a/orion-visor-ui/src/api/asset/host-extra.ts b/orion-visor-ui/src/api/asset/host-extra.ts index 62419d8d..ca235c83 100644 --- a/orion-visor-ui/src/api/asset/host-extra.ts +++ b/orion-visor-ui/src/api/asset/host-extra.ts @@ -50,20 +50,23 @@ export interface HostLabelExtraSettingModel { export interface HostSpecExtraModel { sn: string; osName: string; - cpuCore: number; + cpuCount: number; + cpuPhysicalCore: number; + cpuLogicalCore: number; cpuFrequency: number; cpuModel: string; memorySize: number; diskSize: number; inBandwidth: number; outBandwidth: number; - publicIpAddress: Array; - privateIpAddress: Array; + publicIpAddresses: Array; + privateIpAddresses: Array; chargePerson: string; createdTime: number; expiredTime: number; items: Array<{ label: string; + key?: string; value: string; }>; } diff --git a/orion-visor-ui/src/views/asset/host-list/components/host-card-list.vue b/orion-visor-ui/src/views/asset/host-list/components/host-card-list.vue index d0740b5d..90b7eee1 100644 --- a/orion-visor-ui/src/views/asset/host-list/components/host-card-list.vue +++ b/orion-visor-ui/src/views/asset/host-list/components/host-card-list.vue @@ -163,7 +163,7 @@ {{ [ - addSuffix(record.spec.cpuCore, 'C'), + addSuffix(record.spec.cpuPhysicalCore, 'C'), addSuffix(record.spec.memorySize, 'G'), addSuffix(record.spec.diskSize, 'G') ].filter(Boolean).join('/') || '-' diff --git a/orion-visor-ui/src/views/asset/host-list/components/host-form-spec.vue b/orion-visor-ui/src/views/asset/host-list/components/host-form-spec.vue index b1e5e085..e4240511 100644 --- a/orion-visor-ui/src/views/asset/host-list/components/host-form-spec.vue +++ b/orion-visor-ui/src/views/asset/host-list/components/host-form-spec.vue @@ -32,12 +32,22 @@ - {{ formModel.cpuCore }} + {{ formModel.cpuPhysicalCore }} + + + + + {{ formModel.cpuLogicalCore }} @@ -111,43 +121,19 @@ - - - - {{ addr }} - - - + - - - - {{ addr }} - - - + @@ -221,16 +207,24 @@ + @click="toggleEditing"> 编辑 - 保存 + + + 取消 + ({} as HostSpecExtraModel); // 加载配置 const fetchHostSpec = async () => { setLoading(true); + editing.value = false; try { const { data } = await getHostExtraItem({ hostId: props.hostId, item: 'SPEC' }); formModel.value = data; @@ -298,6 +292,13 @@ const saveSpec = async () => { setLoading(true); try { + // 设置额外配置的 key + if (formModel.value.items?.length) { + formModel.value.items.forEach(s => { + s.key = s.label; + }); + } + // 更新 await updateHostSpec({ hostId: props.hostId, extra: JSON.stringify(formModel.value) diff --git a/orion-visor-ui/src/views/asset/host-list/components/host-table.vue b/orion-visor-ui/src/views/asset/host-list/components/host-table.vue index 2952a165..8712ac95 100644 --- a/orion-visor-ui/src/views/asset/host-list/components/host-table.vue +++ b/orion-visor-ui/src/views/asset/host-list/components/host-table.vue @@ -191,7 +191,7 @@ {{ [ - addSuffix(record.spec.cpuCore, 'C'), + addSuffix(record.spec.cpuPhysicalCore, 'C'), addSuffix(record.spec.memorySize, 'G'), addSuffix(record.spec.diskSize, 'G') ].filter(Boolean).join(' / ') || '-' From c661d34a79898fc26aba98c4158cf6db877c7e46 Mon Sep 17 00:00:00 2001 From: lijiahangmax Date: Sun, 10 Aug 2025 19:29:57 +0800 Subject: [PATCH 03/17] =?UTF-8?q?:hammer:=20=E4=BF=AE=E6=94=B9=E4=B8=BB?= =?UTF-8?q?=E6=9C=BA=E9=85=8D=E7=BD=AE=E5=AD=97=E6=AE=B5.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../host/extra/model/HostSpecExtraModel.java | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/host/extra/model/HostSpecExtraModel.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/host/extra/model/HostSpecExtraModel.java index e0eafb63..5c85129d 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/host/extra/model/HostSpecExtraModel.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/host/extra/model/HostSpecExtraModel.java @@ -51,10 +51,20 @@ public class HostSpecExtraModel implements GenericsDataModel { */ private String osName; + /** + * cpu 数量 + */ + private String cpuCount; + /** * cpu 核心数 */ - private Integer cpuCore; + private Integer cpuPhysicalCore; + + /** + * cpu 线程数 + */ + private Integer cpuLogicalCore; /** * cpu 频率 @@ -72,7 +82,7 @@ public class HostSpecExtraModel implements GenericsDataModel { private Double memorySize; /** - * 硬盘大小 + * 磁盘大小 */ private Double diskSize; @@ -89,12 +99,12 @@ public class HostSpecExtraModel implements GenericsDataModel { /** * 公网 ip 列表 */ - private List publicIpAddress; + private List publicIpAddresses; /** * 内网 ip 列表 */ - private List privateIpAddress; + private List privateIpAddresses; /** * 负责人 @@ -131,6 +141,11 @@ public class HostSpecExtraModel implements GenericsDataModel { */ private String label; + /** + * 键 + */ + private String key; + /** * 值 */ From c53042a4b540b28861f1431215e48a58d681f721 Mon Sep 17 00:00:00 2001 From: lijiahangmax Date: Tue, 12 Aug 2025 23:28:30 +0800 Subject: [PATCH 04/17] =?UTF-8?q?:hammer:=20=E4=BF=AE=E6=94=B9=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E7=94=9F=E6=88=90=E5=99=A8=E6=A8=A1=E6=9D=BF.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mybatis/core/generator/core/CodeGenerator.java | 6 ++++++ .../templates/orion-server-module-entity-do.java.vm | 2 ++ .../resources/templates/orion-vue-views-types-const.ts.vm | 2 +- .../templates/orion-vue-views-types-form.rules.ts.vm | 1 + 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/java/org/dromara/visor/framework/mybatis/core/generator/core/CodeGenerator.java b/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/java/org/dromara/visor/framework/mybatis/core/generator/core/CodeGenerator.java index d2d0ec94..462ada82 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/java/org/dromara/visor/framework/mybatis/core/generator/core/CodeGenerator.java +++ b/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/java/org/dromara/visor/framework/mybatis/core/generator/core/CodeGenerator.java @@ -23,10 +23,12 @@ package org.dromara.visor.framework.mybatis.core.generator.core; import cn.orionsec.kit.lang.able.Executable; +import cn.orionsec.kit.lang.constant.Const; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.generator.AutoGenerator; import com.baomidou.mybatisplus.generator.config.*; import com.baomidou.mybatisplus.generator.config.builder.CustomFile; +import com.baomidou.mybatisplus.generator.config.builder.Entity; import com.baomidou.mybatisplus.generator.config.querys.MySqlQuery; import com.baomidou.mybatisplus.generator.config.rules.DateType; import com.baomidou.mybatisplus.generator.config.rules.DbColumnType; @@ -135,6 +137,10 @@ public class CodeGenerator implements Executable { // 整合注入配置 .injection(injectionConfig); + // 提前解析父类 并且排除父类的 id 字段 + Entity entity = strategyConfig.entity(); + entity.getSuperEntityColumns().remove(Const.ID); + // 执行 ag.execute(engine); } diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-server-module-entity-do.java.vm b/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-server-module-entity-do.java.vm index 25166243..39ec31ca 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-server-module-entity-do.java.vm +++ b/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-server-module-entity-do.java.vm @@ -60,6 +60,7 @@ public class ${entity} { #end ## ---------- BEGIN 字段循环遍历 ---------- #foreach($field in ${table.fields}) +#if("$!field.propertyName" != "id") #if(${field.keyFlag}) #set($keyPropertyName=${field.propertyName}) @@ -88,5 +89,6 @@ public class ${entity} { #end private ${field.propertyType} ${field.propertyName}; #end +#end } diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-types-const.ts.vm b/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-types-const.ts.vm index bf109351..c961baa6 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-types-const.ts.vm +++ b/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-types-const.ts.vm @@ -1,4 +1,4 @@ -export const TABLE_NAME = '$table.name'; +export const TableName = '$table.name'; #if($dictMap.entrySet().size() > 0) #foreach($enumEntity in $dictMap.entrySet()) diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-types-form.rules.ts.vm b/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-types-form.rules.ts.vm index ec72e63f..c59dbe55 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-types-form.rules.ts.vm +++ b/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-types-form.rules.ts.vm @@ -1,4 +1,5 @@ import type { FieldRule } from '@arco-design/web-vue'; + #foreach($field in ${table.fields}) #if("$!field.propertyName" != "id") #if(${field.propertyType} == 'String' && "$field.metaInfo.jdbcType" != "LONGVARCHAR") From 8501e900c7f6930ce05105769b97d016d20f789a Mon Sep 17 00:00:00 2001 From: lijiahangmax Date: Wed, 13 Aug 2025 00:00:45 +0800 Subject: [PATCH 05/17] =?UTF-8?q?:hammer:=20=E4=BF=AE=E6=94=B9=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E7=94=9F=E6=88=90=E5=99=A8=E6=A8=A1=E6=9D=BF.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../templates/orion-vue-views-types-card.fields.ts.vm | 2 ++ .../templates/orion-vue-views-types-table.columns.ts.vm | 3 +++ 2 files changed, 5 insertions(+) diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-types-card.fields.ts.vm b/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-types-card.fields.ts.vm index 38ac51c9..f8d9584f 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-types-card.fields.ts.vm +++ b/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-types-card.fields.ts.vm @@ -30,6 +30,7 @@ const fieldConfig = { render: ({ record }) => { return dateFormat(new Date(record.createTime)); }, + default: true, }, { label: '修改时间', dataIndex: 'updateTime', @@ -41,6 +42,7 @@ const fieldConfig = { label: '创建人', dataIndex: 'creator', slotName: 'creator', + default: true, }, { label: '修改人', dataIndex: 'updater', diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-types-table.columns.ts.vm b/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-types-table.columns.ts.vm index d9af843a..a33cfb03 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-types-table.columns.ts.vm +++ b/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-types-table.columns.ts.vm @@ -35,6 +35,7 @@ const columns = [ render: ({ record }) => { return dateFormat(new Date(record.createTime)); }, + default: true, }, { title: '修改时间', dataIndex: 'updateTime', @@ -48,6 +49,7 @@ const columns = [ title: '创建人', dataIndex: 'creator', slotName: 'creator', + default: true, }, { title: '修改人', dataIndex: 'updater', @@ -58,6 +60,7 @@ const columns = [ width: 130, align: 'center', fixed: 'right', + default: true, }, ] as TableColumnData[]; From 393286d3098aaf898d2f143d87ec78947d7609ee Mon Sep 17 00:00:00 2001 From: lijiahangmax Date: Sat, 23 Aug 2025 14:11:15 +0800 Subject: [PATCH 06/17] =?UTF-8?q?:hammer:=20=E4=BF=AE=E6=94=B9=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E5=8A=A0=E5=AF=86=E6=A8=A1=E5=9D=97.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- orion-visor-dependencies/pom.xml | 2 +- .../biz/operator/log/core/aspect/OperatorLogAspect.java | 1 - .../pom.xml | 2 +- .../configuration/OrionEncryptAutoConfiguration.java | 0 .../encrypt/configuration/config/AesEncryptConfig.java | 0 .../visor/framework/encrypt/core/BaseAesEncryptor.java | 0 .../visor/framework/encrypt/core/impl/AesEncryptorImpl.java | 0 .../visor/framework/encrypt/core/impl/RsaDecryptorImpl.java | 0 .../META-INF/additional-spring-configuration-metadata.json | 0 ...ngframework.boot.autoconfigure.AutoConfiguration.imports | 0 .../templates/orion-vue-views-types-card.fields.ts.vm | 6 ++++++ orion-visor-framework/pom.xml | 2 +- orion-visor-launch/pom.xml | 2 +- .../orion-visor-module-asset-service/pom.xml | 2 +- .../visor/module/infra/controller/DictKeyController.java | 3 ++- 15 files changed, 13 insertions(+), 7 deletions(-) rename orion-visor-framework/{orion-visor-spring-boot-starter-encrypt => orion-visor-spring-boot-starter-cipher}/pom.xml (93%) rename orion-visor-framework/{orion-visor-spring-boot-starter-encrypt => orion-visor-spring-boot-starter-cipher}/src/main/java/org/dromara/visor/framework/encrypt/configuration/OrionEncryptAutoConfiguration.java (100%) rename orion-visor-framework/{orion-visor-spring-boot-starter-encrypt => orion-visor-spring-boot-starter-cipher}/src/main/java/org/dromara/visor/framework/encrypt/configuration/config/AesEncryptConfig.java (100%) rename orion-visor-framework/{orion-visor-spring-boot-starter-encrypt => orion-visor-spring-boot-starter-cipher}/src/main/java/org/dromara/visor/framework/encrypt/core/BaseAesEncryptor.java (100%) rename orion-visor-framework/{orion-visor-spring-boot-starter-encrypt => orion-visor-spring-boot-starter-cipher}/src/main/java/org/dromara/visor/framework/encrypt/core/impl/AesEncryptorImpl.java (100%) rename orion-visor-framework/{orion-visor-spring-boot-starter-encrypt => orion-visor-spring-boot-starter-cipher}/src/main/java/org/dromara/visor/framework/encrypt/core/impl/RsaDecryptorImpl.java (100%) rename orion-visor-framework/{orion-visor-spring-boot-starter-encrypt => orion-visor-spring-boot-starter-cipher}/src/main/resources/META-INF/additional-spring-configuration-metadata.json (100%) rename orion-visor-framework/{orion-visor-spring-boot-starter-encrypt => orion-visor-spring-boot-starter-cipher}/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (100%) diff --git a/orion-visor-dependencies/pom.xml b/orion-visor-dependencies/pom.xml index 08190e0d..cd7ee7c8 100644 --- a/orion-visor-dependencies/pom.xml +++ b/orion-visor-dependencies/pom.xml @@ -118,7 +118,7 @@ org.dromara.visor - orion-visor-spring-boot-starter-encrypt + orion-visor-spring-boot-starter-cipher ${revision} diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-biz-operator-log/src/main/java/org/dromara/visor/framework/biz/operator/log/core/aspect/OperatorLogAspect.java b/orion-visor-framework/orion-visor-spring-boot-starter-biz-operator-log/src/main/java/org/dromara/visor/framework/biz/operator/log/core/aspect/OperatorLogAspect.java index 283e55dd..7b9f32c3 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-biz-operator-log/src/main/java/org/dromara/visor/framework/biz/operator/log/core/aspect/OperatorLogAspect.java +++ b/orion-visor-framework/orion-visor-spring-boot-starter-biz-operator-log/src/main/java/org/dromara/visor/framework/biz/operator/log/core/aspect/OperatorLogAspect.java @@ -73,7 +73,6 @@ public class OperatorLogAspect { .maxPoolSize(1) .useLinkedBlockingQueue() .allowCoreThreadTimeout() - .useLinkedBlockingQueue() .build(); private final OperatorLogFrameworkService operatorLogFrameworkService; diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-encrypt/pom.xml b/orion-visor-framework/orion-visor-spring-boot-starter-cipher/pom.xml similarity index 93% rename from orion-visor-framework/orion-visor-spring-boot-starter-encrypt/pom.xml rename to orion-visor-framework/orion-visor-spring-boot-starter-cipher/pom.xml index b1364d07..db7e036d 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-encrypt/pom.xml +++ b/orion-visor-framework/orion-visor-spring-boot-starter-cipher/pom.xml @@ -9,7 +9,7 @@ 4.0.0 - orion-visor-spring-boot-starter-encrypt + orion-visor-spring-boot-starter-cipher ${project.artifactId} jar diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-encrypt/src/main/java/org/dromara/visor/framework/encrypt/configuration/OrionEncryptAutoConfiguration.java b/orion-visor-framework/orion-visor-spring-boot-starter-cipher/src/main/java/org/dromara/visor/framework/encrypt/configuration/OrionEncryptAutoConfiguration.java similarity index 100% rename from orion-visor-framework/orion-visor-spring-boot-starter-encrypt/src/main/java/org/dromara/visor/framework/encrypt/configuration/OrionEncryptAutoConfiguration.java rename to orion-visor-framework/orion-visor-spring-boot-starter-cipher/src/main/java/org/dromara/visor/framework/encrypt/configuration/OrionEncryptAutoConfiguration.java diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-encrypt/src/main/java/org/dromara/visor/framework/encrypt/configuration/config/AesEncryptConfig.java b/orion-visor-framework/orion-visor-spring-boot-starter-cipher/src/main/java/org/dromara/visor/framework/encrypt/configuration/config/AesEncryptConfig.java similarity index 100% rename from orion-visor-framework/orion-visor-spring-boot-starter-encrypt/src/main/java/org/dromara/visor/framework/encrypt/configuration/config/AesEncryptConfig.java rename to orion-visor-framework/orion-visor-spring-boot-starter-cipher/src/main/java/org/dromara/visor/framework/encrypt/configuration/config/AesEncryptConfig.java diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-encrypt/src/main/java/org/dromara/visor/framework/encrypt/core/BaseAesEncryptor.java b/orion-visor-framework/orion-visor-spring-boot-starter-cipher/src/main/java/org/dromara/visor/framework/encrypt/core/BaseAesEncryptor.java similarity index 100% rename from orion-visor-framework/orion-visor-spring-boot-starter-encrypt/src/main/java/org/dromara/visor/framework/encrypt/core/BaseAesEncryptor.java rename to orion-visor-framework/orion-visor-spring-boot-starter-cipher/src/main/java/org/dromara/visor/framework/encrypt/core/BaseAesEncryptor.java diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-encrypt/src/main/java/org/dromara/visor/framework/encrypt/core/impl/AesEncryptorImpl.java b/orion-visor-framework/orion-visor-spring-boot-starter-cipher/src/main/java/org/dromara/visor/framework/encrypt/core/impl/AesEncryptorImpl.java similarity index 100% rename from orion-visor-framework/orion-visor-spring-boot-starter-encrypt/src/main/java/org/dromara/visor/framework/encrypt/core/impl/AesEncryptorImpl.java rename to orion-visor-framework/orion-visor-spring-boot-starter-cipher/src/main/java/org/dromara/visor/framework/encrypt/core/impl/AesEncryptorImpl.java diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-encrypt/src/main/java/org/dromara/visor/framework/encrypt/core/impl/RsaDecryptorImpl.java b/orion-visor-framework/orion-visor-spring-boot-starter-cipher/src/main/java/org/dromara/visor/framework/encrypt/core/impl/RsaDecryptorImpl.java similarity index 100% rename from orion-visor-framework/orion-visor-spring-boot-starter-encrypt/src/main/java/org/dromara/visor/framework/encrypt/core/impl/RsaDecryptorImpl.java rename to orion-visor-framework/orion-visor-spring-boot-starter-cipher/src/main/java/org/dromara/visor/framework/encrypt/core/impl/RsaDecryptorImpl.java diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-encrypt/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/orion-visor-framework/orion-visor-spring-boot-starter-cipher/src/main/resources/META-INF/additional-spring-configuration-metadata.json similarity index 100% rename from orion-visor-framework/orion-visor-spring-boot-starter-encrypt/src/main/resources/META-INF/additional-spring-configuration-metadata.json rename to orion-visor-framework/orion-visor-spring-boot-starter-cipher/src/main/resources/META-INF/additional-spring-configuration-metadata.json diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-encrypt/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/orion-visor-framework/orion-visor-spring-boot-starter-cipher/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports similarity index 100% rename from orion-visor-framework/orion-visor-spring-boot-starter-encrypt/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports rename to orion-visor-framework/orion-visor-spring-boot-starter-cipher/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-types-card.fields.ts.vm b/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-types-card.fields.ts.vm index f8d9584f..60313e3b 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-types-card.fields.ts.vm +++ b/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-types-card.fields.ts.vm @@ -42,11 +42,17 @@ const fieldConfig = { label: '创建人', dataIndex: 'creator', slotName: 'creator', + width: 148, + ellipsis: true, + tooltip: true, default: true, }, { label: '修改人', dataIndex: 'updater', slotName: 'updater', + width: 148, + ellipsis: true, + tooltip: true, } ] as CardField[] } as CardFieldConfig; diff --git a/orion-visor-framework/pom.xml b/orion-visor-framework/pom.xml index 209522eb..aab6e488 100644 --- a/orion-visor-framework/pom.xml +++ b/orion-visor-framework/pom.xml @@ -26,7 +26,7 @@ orion-visor-spring-boot-starter-websocket orion-visor-spring-boot-starter-redis orion-visor-spring-boot-starter-desensitize - orion-visor-spring-boot-starter-encrypt + orion-visor-spring-boot-starter-cipher orion-visor-spring-boot-starter-log orion-visor-spring-boot-starter-storage orion-visor-spring-boot-starter-security diff --git a/orion-visor-launch/pom.xml b/orion-visor-launch/pom.xml index fa4e9ee6..a27591db 100644 --- a/orion-visor-launch/pom.xml +++ b/orion-visor-launch/pom.xml @@ -91,7 +91,7 @@ org.dromara.visor - orion-visor-spring-boot-starter-encrypt + orion-visor-spring-boot-starter-cipher org.dromara.visor diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/pom.xml b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/pom.xml index 8f09d18d..65bda5cb 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/pom.xml +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/pom.xml @@ -61,7 +61,7 @@ org.dromara.visor - orion-visor-spring-boot-starter-encrypt + orion-visor-spring-boot-starter-cipher org.dromara.visor diff --git a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/controller/DictKeyController.java b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/controller/DictKeyController.java index de2dff02..f991c4fa 100644 --- a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/controller/DictKeyController.java +++ b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/controller/DictKeyController.java @@ -27,6 +27,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; +import org.dromara.visor.common.validator.group.Page; import org.dromara.visor.framework.biz.operator.log.core.annotation.OperatorLog; import org.dromara.visor.framework.log.core.annotation.IgnoreLog; import org.dromara.visor.framework.log.core.enums.IgnoreLogMode; @@ -92,7 +93,7 @@ public class DictKeyController { @PostMapping("/query") @Operation(summary = "分页查询全部字典配置项") @PreAuthorize("@ss.hasPermission('infra:dict-key:query')") - public DataGrid getDictKeyPage(@Validated @RequestBody DictKeyQueryRequest request) { + public DataGrid getDictKeyPage(@Validated(Page.class) @RequestBody DictKeyQueryRequest request) { return dictKeyService.getDictKeyPage(request); } From 3c75aedcecb298d2311faf3246867f1963daf4bf Mon Sep 17 00:00:00 2001 From: lijiahangmax Date: Sat, 23 Aug 2025 15:05:03 +0800 Subject: [PATCH 07/17] =?UTF-8?q?:hammer:=20=E4=BC=98=E5=8C=96=E9=94=81?= =?UTF-8?q?=E9=80=BB=E8=BE=91.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{interfaces => cipher}/AesEncryptor.java | 2 +- .../{interfaces => cipher}/RsaDecryptor.java | 2 +- .../{interfaces => file}/FileClient.java | 2 +- .../visor/common/interfaces/Locker.java | 55 ------- .../visor/common/lock/EmptyLocker.java | 80 ++++++++++ .../org/dromara/visor/common/lock/Locker.java | 116 ++++++++++++++ .../visor/common/utils/AesEncryptUtils.java | 2 +- .../visor/common/utils/LockerUtils.java | 96 ++++++++++-- .../common/utils/RsaParamDecryptUtils.java | 2 +- .../OrionEncryptAutoConfiguration.java | 4 +- .../encrypt/core/BaseAesEncryptor.java | 2 +- .../encrypt/core/impl/RsaDecryptorImpl.java | 2 +- .../OrionMockRedisAutoConfiguration.java | 17 +- .../OrionRedisAutoConfiguration.java | 2 +- .../redis/core/lock/RedisLocker.java | 145 ++++++++++++++++-- .../OrionStorageAutoConfiguration.java | 2 +- .../core/client/AbstractFileClient.java | 2 +- .../OrionMockRedisTestConfiguration.java | 17 +- .../handler/BaseExecCommandHandler.java | 2 +- .../exec/log/tracker/ExecLogTracker.java | 2 +- .../handler/upload/uploader/FileUploader.java | 2 +- .../exec/service/impl/ExecLogServiceImpl.java | 2 +- .../service/impl/UploadTaskServiceImpl.java | 2 +- .../exec/task/ExecLogFileAutoClearTask.java | 2 +- .../upload/FileUploadMessageDispatcher.java | 2 +- .../upload/handler/FileUploadHandler.java | 2 +- .../module/infra/task/TagAutoClearTask.java | 2 +- .../CommandSnippetGroupAutoClearTask.java | 2 +- .../task/PathBookmarkGroupAutoClearTask.java | 2 +- .../task/TerminalConnectLogAutoClearTask.java | 2 +- 30 files changed, 443 insertions(+), 131 deletions(-) rename orion-visor-common/src/main/java/org/dromara/visor/common/{interfaces => cipher}/AesEncryptor.java (97%) rename orion-visor-common/src/main/java/org/dromara/visor/common/{interfaces => cipher}/RsaDecryptor.java (95%) rename orion-visor-common/src/main/java/org/dromara/visor/common/{interfaces => file}/FileClient.java (98%) delete mode 100644 orion-visor-common/src/main/java/org/dromara/visor/common/interfaces/Locker.java create mode 100644 orion-visor-common/src/main/java/org/dromara/visor/common/lock/EmptyLocker.java create mode 100644 orion-visor-common/src/main/java/org/dromara/visor/common/lock/Locker.java diff --git a/orion-visor-common/src/main/java/org/dromara/visor/common/interfaces/AesEncryptor.java b/orion-visor-common/src/main/java/org/dromara/visor/common/cipher/AesEncryptor.java similarity index 97% rename from orion-visor-common/src/main/java/org/dromara/visor/common/interfaces/AesEncryptor.java rename to orion-visor-common/src/main/java/org/dromara/visor/common/cipher/AesEncryptor.java index bf3f1360..326908f1 100644 --- a/orion-visor-common/src/main/java/org/dromara/visor/common/interfaces/AesEncryptor.java +++ b/orion-visor-common/src/main/java/org/dromara/visor/common/cipher/AesEncryptor.java @@ -20,7 +20,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.dromara.visor.common.interfaces; +package org.dromara.visor.common.cipher; import cn.orionsec.kit.lang.utils.codec.Base62s; import cn.orionsec.kit.lang.utils.crypto.symmetric.SymmetricCrypto; diff --git a/orion-visor-common/src/main/java/org/dromara/visor/common/interfaces/RsaDecryptor.java b/orion-visor-common/src/main/java/org/dromara/visor/common/cipher/RsaDecryptor.java similarity index 95% rename from orion-visor-common/src/main/java/org/dromara/visor/common/interfaces/RsaDecryptor.java rename to orion-visor-common/src/main/java/org/dromara/visor/common/cipher/RsaDecryptor.java index a0bec1ae..d64e4e0c 100644 --- a/orion-visor-common/src/main/java/org/dromara/visor/common/interfaces/RsaDecryptor.java +++ b/orion-visor-common/src/main/java/org/dromara/visor/common/cipher/RsaDecryptor.java @@ -20,7 +20,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.dromara.visor.common.interfaces; +package org.dromara.visor.common.cipher; /** * rsa 解密器 diff --git a/orion-visor-common/src/main/java/org/dromara/visor/common/interfaces/FileClient.java b/orion-visor-common/src/main/java/org/dromara/visor/common/file/FileClient.java similarity index 98% rename from orion-visor-common/src/main/java/org/dromara/visor/common/interfaces/FileClient.java rename to orion-visor-common/src/main/java/org/dromara/visor/common/file/FileClient.java index d6f62d4a..e9ea011b 100644 --- a/orion-visor-common/src/main/java/org/dromara/visor/common/interfaces/FileClient.java +++ b/orion-visor-common/src/main/java/org/dromara/visor/common/file/FileClient.java @@ -20,7 +20,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.dromara.visor.common.interfaces; +package org.dromara.visor.common.file; import java.io.InputStream; import java.io.OutputStream; diff --git a/orion-visor-common/src/main/java/org/dromara/visor/common/interfaces/Locker.java b/orion-visor-common/src/main/java/org/dromara/visor/common/interfaces/Locker.java deleted file mode 100644 index d08705ea..00000000 --- a/orion-visor-common/src/main/java/org/dromara/visor/common/interfaces/Locker.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2023 - present Dromara, All rights reserved. - * - * https://visor.dromara.org - * https://visor.dromara.org.cn - * https://visor.orionsec.cn - * - * Members: - * Jiahang Li - ljh1553488six@139.com - author - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.dromara.visor.common.interfaces; - -import java.util.function.Supplier; - -/** - * 分布式锁 - * - * @author Jiahang Li - * @version 1.0.0 - * @since 2024/5/16 12:24 - */ -public interface Locker { - - /** - * 尝试获取锁 - * - * @param key key - * @param run run - * @return 是否获取到锁 - */ - boolean tryLock(String key, Runnable run); - - /** - * 尝试获取锁 - * - * @param key key - * @param call call - * @param T - * @return 执行结果 - */ - T tryLock(String key, Supplier call); - -} diff --git a/orion-visor-common/src/main/java/org/dromara/visor/common/lock/EmptyLocker.java b/orion-visor-common/src/main/java/org/dromara/visor/common/lock/EmptyLocker.java new file mode 100644 index 00000000..b2e16b7c --- /dev/null +++ b/orion-visor-common/src/main/java/org/dromara/visor/common/lock/EmptyLocker.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.common.lock; + +import cn.orionsec.kit.lang.able.Executable; + +import java.util.function.Supplier; + +/** + * 空实现的锁 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/23 13:59 + */ +public class EmptyLocker implements Locker { + + @Override + public boolean tryLockExecute(String key, Executable executable) { + executable.exec(); + return true; + } + + @Override + public boolean tryLockExecute(String key, long timeout, Executable executable) { + executable.exec(); + return true; + } + + @Override + public T tryLockExecute(String key, Supplier callable) { + return callable.get(); + } + + @Override + public T tryLockExecute(String key, long timeout, Supplier callable) { + return callable.get(); + } + + @Override + public void lockExecute(String key, Executable executable) { + executable.exec(); + } + + @Override + public void lockExecute(String key, long timeout, Executable executable) { + executable.exec(); + } + + @Override + public T lockExecute(String key, Supplier callable) { + return callable.get(); + } + + @Override + public T lockExecute(String key, long timeout, Supplier callable) { + return callable.get(); + } + +} diff --git a/orion-visor-common/src/main/java/org/dromara/visor/common/lock/Locker.java b/orion-visor-common/src/main/java/org/dromara/visor/common/lock/Locker.java new file mode 100644 index 00000000..12e87471 --- /dev/null +++ b/orion-visor-common/src/main/java/org/dromara/visor/common/lock/Locker.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.common.lock; + +import cn.orionsec.kit.lang.able.Executable; + +import java.util.function.Supplier; + +/** + * 分布式锁 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/5/16 12:24 + */ +public interface Locker { + + /** + * 尝试获取锁并执行 + * + * @param key key + * @param executable exec + * @return 是否获取到锁 + */ + boolean tryLockExecute(String key, Executable executable); + + /** + * 尝试获取锁并执行 + * + * @param key key + * @param timeout timeout + * @param executable exec + * @return 是否获取到锁 + */ + boolean tryLockExecute(String key, long timeout, Executable executable); + + /** + * 尝试获取锁并执行 未获取到锁则抛出异常 + * + * @param key key + * @param callable callable + * @param T + * @return 执行结果 + */ + T tryLockExecute(String key, Supplier callable); + + /** + * 尝试获取锁并执行 未获取到锁则抛出异常 + * + * @param key key + * @param timeout timeout + * @param callable callable + * @param T + * @return 执行结果 + */ + T tryLockExecute(String key, long timeout, Supplier callable); + + /** + * 阻塞获取锁并执行 + * + * @param key key + * @param executable exec + */ + void lockExecute(String key, Executable executable); + + /** + * 阻塞获取锁并执行 + * + * @param key key + * @param timeout timeout + * @param executable exec + */ + void lockExecute(String key, long timeout, Executable executable); + + /** + * 阻塞获取锁并执行 + * + * @param key key + * @param callable callable + * @param T + * @return 执行结果 + */ + T lockExecute(String key, Supplier callable); + + /** + * 阻塞获取锁并执行 + * + * @param key key + * @param timeout timeout + * @param callable callable + * @param T + * @return 执行结果 + */ + T lockExecute(String key, long timeout, Supplier callable); + +} diff --git a/orion-visor-common/src/main/java/org/dromara/visor/common/utils/AesEncryptUtils.java b/orion-visor-common/src/main/java/org/dromara/visor/common/utils/AesEncryptUtils.java index cf8cfaf1..56446750 100644 --- a/orion-visor-common/src/main/java/org/dromara/visor/common/utils/AesEncryptUtils.java +++ b/orion-visor-common/src/main/java/org/dromara/visor/common/utils/AesEncryptUtils.java @@ -23,7 +23,7 @@ package org.dromara.visor.common.utils; import cn.orionsec.kit.lang.utils.Exceptions; -import org.dromara.visor.common.interfaces.AesEncryptor; +import org.dromara.visor.common.cipher.AesEncryptor; /** * aes 数据加密工具类 diff --git a/orion-visor-common/src/main/java/org/dromara/visor/common/utils/LockerUtils.java b/orion-visor-common/src/main/java/org/dromara/visor/common/utils/LockerUtils.java index 01ce1219..e72ba3f2 100644 --- a/orion-visor-common/src/main/java/org/dromara/visor/common/utils/LockerUtils.java +++ b/orion-visor-common/src/main/java/org/dromara/visor/common/utils/LockerUtils.java @@ -22,9 +22,10 @@ */ package org.dromara.visor.common.utils; +import cn.orionsec.kit.lang.able.Executable; import cn.orionsec.kit.lang.utils.Exceptions; import lombok.extern.slf4j.Slf4j; -import org.dromara.visor.common.interfaces.Locker; +import org.dromara.visor.common.lock.Locker; import java.util.function.Supplier; @@ -44,26 +45,97 @@ public class LockerUtils { } /** - * 尝试获取锁 + * 尝试获取锁并执行 * - * @param key key - * @param run run + * @param key key + * @param executable exec * @return 是否获取到锁 */ - public static boolean tryLock(String key, Runnable run) { - return delegate.tryLock(key, run); + public static boolean tryLockExecute(String key, Executable executable) { + return delegate.tryLockExecute(key, executable); } /** - * 尝试获取锁 + * 尝试获取锁并执行 * - * @param key key - * @param call call - * @param T + * @param key key + * @param timeout timeout + * @param executable exec + * @return 是否获取到锁 + */ + public static boolean tryLockExecute(String key, long timeout, Executable executable) { + return delegate.tryLockExecute(key, timeout, executable); + } + + /** + * 尝试获取锁并执行 未获取到锁则抛出异常 + * + * @param key key + * @param callable callable + * @param T * @return 执行结果 */ - public static T tryLock(String key, Supplier call) { - return delegate.tryLock(key, call); + public static T tryLockExecute(String key, Supplier callable) { + return delegate.tryLockExecute(key, callable); + } + + /** + * 尝试获取锁并执行 未获取到锁则抛出异常 + * + * @param key key + * @param timeout timeout + * @param callable callable + * @param T + * @return 执行结果 + */ + public static T tryLockExecute(String key, long timeout, Supplier callable) { + return delegate.tryLockExecute(key, timeout, callable); + } + + /** + * 阻塞获取锁并执行 + * + * @param key key + * @param executable exec + */ + public static void lockExecute(String key, Executable executable) { + delegate.lockExecute(key, executable); + } + + /** + * 阻塞获取锁并执行 + * + * @param key key + * @param timeout timeout + * @param executable exec + */ + public static void lockExecute(String key, long timeout, Executable executable) { + delegate.lockExecute(key, timeout, executable); + } + + /** + * 阻塞获取锁并执行 + * + * @param key key + * @param callable callable + * @param T + * @return 执行结果 + */ + public static T lockExecute(String key, Supplier callable) { + return delegate.lockExecute(key, callable); + } + + /** + * 阻塞获取锁并执行 + * + * @param key key + * @param timeout timeout + * @param callable callable + * @param T + * @return 执行结果 + */ + public static T lockExecute(String key, long timeout, Supplier callable) { + return delegate.lockExecute(key, timeout, callable); } public static void setDelegate(Locker delegate) { diff --git a/orion-visor-common/src/main/java/org/dromara/visor/common/utils/RsaParamDecryptUtils.java b/orion-visor-common/src/main/java/org/dromara/visor/common/utils/RsaParamDecryptUtils.java index 56b24887..f7d14ff6 100644 --- a/orion-visor-common/src/main/java/org/dromara/visor/common/utils/RsaParamDecryptUtils.java +++ b/orion-visor-common/src/main/java/org/dromara/visor/common/utils/RsaParamDecryptUtils.java @@ -23,7 +23,7 @@ package org.dromara.visor.common.utils; import cn.orionsec.kit.lang.utils.Exceptions; -import org.dromara.visor.common.interfaces.RsaDecryptor; +import org.dromara.visor.common.cipher.RsaDecryptor; /** * rsa 参数解密工具类 diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-cipher/src/main/java/org/dromara/visor/framework/encrypt/configuration/OrionEncryptAutoConfiguration.java b/orion-visor-framework/orion-visor-spring-boot-starter-cipher/src/main/java/org/dromara/visor/framework/encrypt/configuration/OrionEncryptAutoConfiguration.java index f6e942be..36110dfa 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-cipher/src/main/java/org/dromara/visor/framework/encrypt/configuration/OrionEncryptAutoConfiguration.java +++ b/orion-visor-framework/orion-visor-spring-boot-starter-cipher/src/main/java/org/dromara/visor/framework/encrypt/configuration/OrionEncryptAutoConfiguration.java @@ -24,8 +24,8 @@ package org.dromara.visor.framework.encrypt.configuration; import org.dromara.visor.common.config.ConfigStore; import org.dromara.visor.common.constant.AutoConfigureOrderConst; -import org.dromara.visor.common.interfaces.AesEncryptor; -import org.dromara.visor.common.interfaces.RsaDecryptor; +import org.dromara.visor.common.cipher.AesEncryptor; +import org.dromara.visor.common.cipher.RsaDecryptor; import org.dromara.visor.common.utils.AesEncryptUtils; import org.dromara.visor.common.utils.RsaParamDecryptUtils; import org.dromara.visor.framework.encrypt.configuration.config.AesEncryptConfig; diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-cipher/src/main/java/org/dromara/visor/framework/encrypt/core/BaseAesEncryptor.java b/orion-visor-framework/orion-visor-spring-boot-starter-cipher/src/main/java/org/dromara/visor/framework/encrypt/core/BaseAesEncryptor.java index 27e0a390..4bd331a8 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-cipher/src/main/java/org/dromara/visor/framework/encrypt/core/BaseAesEncryptor.java +++ b/orion-visor-framework/orion-visor-spring-boot-starter-cipher/src/main/java/org/dromara/visor/framework/encrypt/core/BaseAesEncryptor.java @@ -22,7 +22,7 @@ */ package org.dromara.visor.framework.encrypt.core; -import org.dromara.visor.common.interfaces.AesEncryptor; +import org.dromara.visor.common.cipher.AesEncryptor; /** * 数据加密器 diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-cipher/src/main/java/org/dromara/visor/framework/encrypt/core/impl/RsaDecryptorImpl.java b/orion-visor-framework/orion-visor-spring-boot-starter-cipher/src/main/java/org/dromara/visor/framework/encrypt/core/impl/RsaDecryptorImpl.java index 1b482252..7c9cfff3 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-cipher/src/main/java/org/dromara/visor/framework/encrypt/core/impl/RsaDecryptorImpl.java +++ b/orion-visor-framework/orion-visor-spring-boot-starter-cipher/src/main/java/org/dromara/visor/framework/encrypt/core/impl/RsaDecryptorImpl.java @@ -26,7 +26,7 @@ import cn.orionsec.kit.lang.utils.crypto.RSA; import org.dromara.visor.common.config.ConfigRef; import org.dromara.visor.common.config.ConfigStore; import org.dromara.visor.common.constant.ConfigKeys; -import org.dromara.visor.common.interfaces.RsaDecryptor; +import org.dromara.visor.common.cipher.RsaDecryptor; import java.security.interfaces.RSAPrivateKey; import java.util.Arrays; diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-redis/src/main/java/org/dromara/visor/framework/redis/configuration/OrionMockRedisAutoConfiguration.java b/orion-visor-framework/orion-visor-spring-boot-starter-redis/src/main/java/org/dromara/visor/framework/redis/configuration/OrionMockRedisAutoConfiguration.java index be3188ec..559ac1c6 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-redis/src/main/java/org/dromara/visor/framework/redis/configuration/OrionMockRedisAutoConfiguration.java +++ b/orion-visor-framework/orion-visor-spring-boot-starter-redis/src/main/java/org/dromara/visor/framework/redis/configuration/OrionMockRedisAutoConfiguration.java @@ -24,7 +24,8 @@ package org.dromara.visor.framework.redis.configuration; import com.github.fppt.jedismock.RedisServer; import org.dromara.visor.common.constant.AutoConfigureOrderConst; -import org.dromara.visor.common.interfaces.Locker; +import org.dromara.visor.common.lock.EmptyLocker; +import org.dromara.visor.common.lock.Locker; import org.dromara.visor.common.utils.LockerUtils; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureOrder; @@ -35,7 +36,6 @@ import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import java.net.InetAddress; -import java.util.function.Supplier; /** * MockRedis @@ -79,18 +79,7 @@ public class OrionMockRedisAutoConfiguration { */ @Bean public Locker redisLocker() { - Locker locker = new Locker() { - @Override - public boolean tryLock(String key, Runnable run) { - run.run(); - return true; - } - - @Override - public T tryLock(String key, Supplier call) { - return call.get(); - } - }; + EmptyLocker locker = new EmptyLocker(); LockerUtils.setDelegate(locker); return locker; } diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-redis/src/main/java/org/dromara/visor/framework/redis/configuration/OrionRedisAutoConfiguration.java b/orion-visor-framework/orion-visor-spring-boot-starter-redis/src/main/java/org/dromara/visor/framework/redis/configuration/OrionRedisAutoConfiguration.java index 55ca9b83..49016f60 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-redis/src/main/java/org/dromara/visor/framework/redis/configuration/OrionRedisAutoConfiguration.java +++ b/orion-visor-framework/orion-visor-spring-boot-starter-redis/src/main/java/org/dromara/visor/framework/redis/configuration/OrionRedisAutoConfiguration.java @@ -24,7 +24,7 @@ package org.dromara.visor.framework.redis.configuration; import cn.orionsec.kit.lang.define.cache.key.CacheKeyDefine; import org.dromara.visor.common.constant.AutoConfigureOrderConst; -import org.dromara.visor.common.interfaces.Locker; +import org.dromara.visor.common.lock.Locker; import org.dromara.visor.common.utils.LockerUtils; import org.dromara.visor.framework.redis.configuration.config.RedissonConfig; import org.dromara.visor.framework.redis.core.lock.RedisLocker; diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-redis/src/main/java/org/dromara/visor/framework/redis/core/lock/RedisLocker.java b/orion-visor-framework/orion-visor-spring-boot-starter-redis/src/main/java/org/dromara/visor/framework/redis/core/lock/RedisLocker.java index 03d83c75..876fc88f 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-redis/src/main/java/org/dromara/visor/framework/redis/core/lock/RedisLocker.java +++ b/orion-visor-framework/orion-visor-spring-boot-starter-redis/src/main/java/org/dromara/visor/framework/redis/core/lock/RedisLocker.java @@ -22,12 +22,14 @@ */ package org.dromara.visor.framework.redis.core.lock; +import cn.orionsec.kit.lang.able.Executable; import cn.orionsec.kit.lang.utils.Exceptions; import lombok.extern.slf4j.Slf4j; -import org.dromara.visor.common.interfaces.Locker; +import org.dromara.visor.common.lock.Locker; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; +import java.util.concurrent.TimeUnit; import java.util.function.Supplier; /** @@ -40,6 +42,8 @@ import java.util.function.Supplier; @Slf4j public class RedisLocker implements Locker { + private static final String LOCK_KEY_PREFIX = "lock:"; + private final RedissonClient redissonClient; public RedisLocker(RedissonClient redissonClient) { @@ -47,37 +51,154 @@ public class RedisLocker implements Locker { } @Override - public boolean tryLock(String key, Runnable run) { + public boolean tryLockExecute(String key, Executable executable) { + return this.tryLockExecute(key, 0, executable); + } + + @Override + public boolean tryLockExecute(String key, long timeout, Executable executable) { // 获取锁 - RLock lock = redissonClient.getLock(key); + RLock lock = this.getLock(key); // 未获取到直接返回 - if (!lock.tryLock()) { - log.info("RedisLocker.tryLock failed {}", key); + if (this.tryLock(lock, timeout)) { return false; } // 执行 try { - run.run(); + executable.exec(); } finally { - lock.unlock(); + this.unlockSafe(lock); } return true; } @Override - public T tryLock(String key, Supplier call) { + public T tryLockExecute(String key, Supplier callable) { + return this.tryLockExecute(key, 0, callable); + } + + @Override + public T tryLockExecute(String key, long timeout, Supplier callable) { // 获取锁 - RLock lock = redissonClient.getLock(key); + RLock lock = this.getLock(key); // 未获取到直接返回 - if (!lock.tryLock()) { - log.info("RedisLocker.tryLock failed {}", key); + if (this.tryLock(lock, timeout)) { throw Exceptions.lock(); } // 执行 try { - return call.get(); + return callable.get(); } finally { + this.unlockSafe(lock); + } + } + + @Override + public void lockExecute(String key, Executable executable) { + this.lockExecute(key, 0, executable); + } + + @Override + public void lockExecute(String key, long timeout, Executable executable) { + // 获取锁 + RLock lock = this.getLock(key); + this.lock(lock, timeout); + // 执行 + try { + executable.exec(); + } finally { + this.unlockSafe(lock); + } + } + + @Override + public T lockExecute(String key, Supplier callable) { + return this.lockExecute(key, 0, callable); + } + + @Override + public T lockExecute(String key, long timeout, Supplier callable) { + // 获取锁 + RLock lock = this.getLock(key); + this.lock(lock, timeout); + // 执行 + try { + return callable.get(); + } finally { + this.unlockSafe(lock); + } + } + + /** + * 获取锁 + * + * @param key key + * @return lock + */ + private RLock getLock(String key) { + return redissonClient.getLock(LOCK_KEY_PREFIX + key); + } + + /** + * 尝试上锁 + * + * @param lock lock + * @param timeout timeout + * @return locked + */ + private boolean tryLock(RLock lock, long timeout) { + boolean result; + try { + if (timeout == 0) { + result = lock.tryLock(); + } else { + result = lock.tryLock(timeout, TimeUnit.MILLISECONDS); + } + if (!result) { + log.warn("RedisLocker.tryLock failed {}", lock.getName()); + } + } catch (InterruptedException e) { + log.error("RedisLocker.tryLock timed out {}", lock.getName(), e); + throw Exceptions.lock(e); + } catch (Exception e) { + log.error("RedisLocker.tryLock error {}", lock.getName(), e); + throw Exceptions.lock(e); + } + return result; + } + + /** + * 上锁 + * + * @param lock lock + * @param timeout timeout + */ + private void lock(RLock lock, long timeout) { + try { + if (timeout == 0) { + lock.lock(); + } else { + lock.lock(timeout, TimeUnit.MILLISECONDS); + } + } catch (Exception e) { + log.error("RedisLocker.lock lock error {}", lock.getName(), e); + throw Exceptions.lock(e); + } + } + + /** + * 安全的释放锁 + * + * @param lock lock + */ + private void unlockSafe(RLock lock) { + if (!lock.isHeldByCurrentThread()) { + return; + } + try { lock.unlock(); + } catch (Exception e) { + log.warn("RedisLocker.unlock failed {}", lock.getName(), e); } } diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-storage/src/main/java/org/dromara/visor/framework/storage/configuration/OrionStorageAutoConfiguration.java b/orion-visor-framework/orion-visor-spring-boot-starter-storage/src/main/java/org/dromara/visor/framework/storage/configuration/OrionStorageAutoConfiguration.java index 195ab9fb..9ef12898 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-storage/src/main/java/org/dromara/visor/framework/storage/configuration/OrionStorageAutoConfiguration.java +++ b/orion-visor-framework/orion-visor-spring-boot-starter-storage/src/main/java/org/dromara/visor/framework/storage/configuration/OrionStorageAutoConfiguration.java @@ -23,7 +23,7 @@ package org.dromara.visor.framework.storage.configuration; import org.dromara.visor.common.constant.AutoConfigureOrderConst; -import org.dromara.visor.common.interfaces.FileClient; +import org.dromara.visor.common.file.FileClient; import org.dromara.visor.framework.storage.configuration.config.LocalStorageConfig; import org.dromara.visor.framework.storage.configuration.config.LogsStorageConfig; import org.dromara.visor.framework.storage.core.client.local.LocalFileClient; diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-storage/src/main/java/org/dromara/visor/framework/storage/core/client/AbstractFileClient.java b/orion-visor-framework/orion-visor-spring-boot-starter-storage/src/main/java/org/dromara/visor/framework/storage/core/client/AbstractFileClient.java index 05f7a896..506701ec 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-storage/src/main/java/org/dromara/visor/framework/storage/core/client/AbstractFileClient.java +++ b/orion-visor-framework/orion-visor-spring-boot-starter-storage/src/main/java/org/dromara/visor/framework/storage/core/client/AbstractFileClient.java @@ -26,7 +26,7 @@ import cn.orionsec.kit.lang.utils.io.Files1; import cn.orionsec.kit.lang.utils.io.Streams; import cn.orionsec.kit.lang.utils.time.Dates; import org.dromara.visor.common.constant.Const; -import org.dromara.visor.common.interfaces.FileClient; +import org.dromara.visor.common.file.FileClient; import java.io.InputStream; import java.io.OutputStream; diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-test/src/main/java/org/dromara/visor/framework/test/configuration/OrionMockRedisTestConfiguration.java b/orion-visor-framework/orion-visor-spring-boot-starter-test/src/main/java/org/dromara/visor/framework/test/configuration/OrionMockRedisTestConfiguration.java index 7ffe8d85..b94e4231 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-test/src/main/java/org/dromara/visor/framework/test/configuration/OrionMockRedisTestConfiguration.java +++ b/orion-visor-framework/orion-visor-spring-boot-starter-test/src/main/java/org/dromara/visor/framework/test/configuration/OrionMockRedisTestConfiguration.java @@ -23,7 +23,8 @@ package org.dromara.visor.framework.test.configuration; import com.github.fppt.jedismock.RedisServer; -import org.dromara.visor.common.interfaces.Locker; +import org.dromara.visor.common.lock.EmptyLocker; +import org.dromara.visor.common.lock.Locker; import org.dromara.visor.common.utils.LockerUtils; import org.springframework.boot.autoconfigure.data.redis.RedisProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -33,7 +34,6 @@ import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Profile; import java.net.InetAddress; -import java.util.function.Supplier; /** * 单元测试 redis mock server 初始化 @@ -66,18 +66,7 @@ public class OrionMockRedisTestConfiguration { */ @Bean public Locker unitTestLocker() { - Locker locker = new Locker() { - @Override - public boolean tryLock(String key, Runnable run) { - run.run(); - return true; - } - - @Override - public T tryLock(String key, Supplier call) { - return call.get(); - } - }; + EmptyLocker locker = new EmptyLocker(); LockerUtils.setDelegate(locker); return locker; } diff --git a/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/handler/exec/command/handler/BaseExecCommandHandler.java b/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/handler/exec/command/handler/BaseExecCommandHandler.java index f40411cc..93d11313 100644 --- a/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/handler/exec/command/handler/BaseExecCommandHandler.java +++ b/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/handler/exec/command/handler/BaseExecCommandHandler.java @@ -44,7 +44,7 @@ import org.dromara.visor.common.constant.ErrorMessage; import org.dromara.visor.common.constant.FileConst; import org.dromara.visor.common.enums.BooleanBit; import org.dromara.visor.common.enums.EndpointDefine; -import org.dromara.visor.common.interfaces.FileClient; +import org.dromara.visor.common.file.FileClient; import org.dromara.visor.common.session.config.SshConnectConfig; import org.dromara.visor.common.session.ssh.SessionStores; import org.dromara.visor.common.utils.PathUtils; diff --git a/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/handler/exec/log/tracker/ExecLogTracker.java b/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/handler/exec/log/tracker/ExecLogTracker.java index 483c8360..a261a12b 100644 --- a/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/handler/exec/log/tracker/ExecLogTracker.java +++ b/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/handler/exec/log/tracker/ExecLogTracker.java @@ -38,7 +38,7 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.dromara.visor.common.constant.Const; import org.dromara.visor.common.constant.ErrorMessage; -import org.dromara.visor.common.interfaces.FileClient; +import org.dromara.visor.common.file.FileClient; import org.dromara.visor.common.utils.Valid; import org.dromara.visor.framework.websocket.core.utils.WebSockets; import org.dromara.visor.module.common.config.AppLogConfig; diff --git a/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/handler/upload/uploader/FileUploader.java b/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/handler/upload/uploader/FileUploader.java index c18b7080..49cb140c 100644 --- a/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/handler/upload/uploader/FileUploader.java +++ b/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/handler/upload/uploader/FileUploader.java @@ -35,7 +35,7 @@ import org.dromara.visor.common.constant.Const; import org.dromara.visor.common.constant.ErrorMessage; import org.dromara.visor.common.constant.ExtraFieldConst; import org.dromara.visor.common.enums.EndpointDefine; -import org.dromara.visor.common.interfaces.FileClient; +import org.dromara.visor.common.file.FileClient; import org.dromara.visor.common.session.config.SshConnectConfig; import org.dromara.visor.common.session.ssh.SessionStores; import org.dromara.visor.common.utils.PathUtils; diff --git a/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/service/impl/ExecLogServiceImpl.java b/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/service/impl/ExecLogServiceImpl.java index 6c7483ff..e872a4f4 100644 --- a/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/service/impl/ExecLogServiceImpl.java +++ b/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/service/impl/ExecLogServiceImpl.java @@ -39,7 +39,7 @@ import org.dromara.visor.common.constant.Const; import org.dromara.visor.common.constant.ErrorMessage; import org.dromara.visor.common.constant.FileConst; import org.dromara.visor.common.enums.EndpointDefine; -import org.dromara.visor.common.interfaces.FileClient; +import org.dromara.visor.common.file.FileClient; import org.dromara.visor.common.utils.SqlUtils; import org.dromara.visor.common.utils.Valid; import org.dromara.visor.framework.biz.operator.log.core.utils.OperatorLogs; diff --git a/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/service/impl/UploadTaskServiceImpl.java b/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/service/impl/UploadTaskServiceImpl.java index 26e1531a..1ef5955f 100644 --- a/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/service/impl/UploadTaskServiceImpl.java +++ b/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/service/impl/UploadTaskServiceImpl.java @@ -35,7 +35,7 @@ import lombok.extern.slf4j.Slf4j; import org.dromara.visor.common.constant.Const; import org.dromara.visor.common.constant.ErrorMessage; import org.dromara.visor.common.enums.EndpointDefine; -import org.dromara.visor.common.interfaces.FileClient; +import org.dromara.visor.common.file.FileClient; import org.dromara.visor.common.security.LoginUser; import org.dromara.visor.common.utils.SqlUtils; import org.dromara.visor.framework.biz.operator.log.core.utils.OperatorLogs; diff --git a/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/task/ExecLogFileAutoClearTask.java b/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/task/ExecLogFileAutoClearTask.java index 8e9e9c83..5f243940 100644 --- a/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/task/ExecLogFileAutoClearTask.java +++ b/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/task/ExecLogFileAutoClearTask.java @@ -70,7 +70,7 @@ public class ExecLogFileAutoClearTask { return; } // 获取锁并执行 - LockerUtils.tryLock(LOCK_KEY, this::doClear); + LockerUtils.tryLockExecute(LOCK_KEY, this::doClear); log.info("ExecLogFileAutoClearTask.clear finish"); } diff --git a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/handler/upload/FileUploadMessageDispatcher.java b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/handler/upload/FileUploadMessageDispatcher.java index b901508c..ccb09581 100644 --- a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/handler/upload/FileUploadMessageDispatcher.java +++ b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/handler/upload/FileUploadMessageDispatcher.java @@ -27,7 +27,7 @@ import cn.orionsec.kit.lang.utils.io.Streams; import com.alibaba.fastjson.JSON; import lombok.extern.slf4j.Slf4j; import org.dromara.visor.common.constant.ExtraFieldConst; -import org.dromara.visor.common.interfaces.FileClient; +import org.dromara.visor.common.file.FileClient; import org.dromara.visor.framework.websocket.core.utils.WebSockets; import org.dromara.visor.module.infra.entity.dto.FileUploadTokenDTO; import org.dromara.visor.module.infra.handler.upload.enums.FileUploadOperatorType; diff --git a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/handler/upload/handler/FileUploadHandler.java b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/handler/upload/handler/FileUploadHandler.java index 92fb91dc..7d9182b9 100644 --- a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/handler/upload/handler/FileUploadHandler.java +++ b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/handler/upload/handler/FileUploadHandler.java @@ -25,7 +25,7 @@ package org.dromara.visor.module.infra.handler.upload.handler; import cn.orionsec.kit.lang.utils.io.Streams; import com.alibaba.fastjson.JSON; import org.dromara.visor.common.constant.Const; -import org.dromara.visor.common.interfaces.FileClient; +import org.dromara.visor.common.file.FileClient; import org.dromara.visor.framework.websocket.core.utils.WebSockets; import org.dromara.visor.module.infra.handler.upload.enums.FileUploadReceiverType; import org.dromara.visor.module.infra.handler.upload.model.FileUploadResponse; diff --git a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/task/TagAutoClearTask.java b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/task/TagAutoClearTask.java index 3c617637..a82ac8c3 100644 --- a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/task/TagAutoClearTask.java +++ b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/task/TagAutoClearTask.java @@ -56,7 +56,7 @@ public class TagAutoClearTask { public void clear() { log.info("TagAutoClearTask.clear start"); // 获取锁并执行 - LockerUtils.tryLock(LOCK_KEY, tagService::clearUnusedTag); + LockerUtils.tryLockExecute(LOCK_KEY, tagService::clearUnusedTag); log.info("TagAutoClearTask.clear finish"); } diff --git a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/task/CommandSnippetGroupAutoClearTask.java b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/task/CommandSnippetGroupAutoClearTask.java index a0d77072..a75fdba3 100644 --- a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/task/CommandSnippetGroupAutoClearTask.java +++ b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/task/CommandSnippetGroupAutoClearTask.java @@ -56,7 +56,7 @@ public class CommandSnippetGroupAutoClearTask { public void clear() { log.info("CommandSnippetGroupAutoClearTask.clear start"); // 获取锁并执行 - LockerUtils.tryLock(LOCK_KEY, commandSnippetGroupService::clearUnusedGroup); + LockerUtils.tryLockExecute(LOCK_KEY, commandSnippetGroupService::clearUnusedGroup); log.info("CommandSnippetGroupAutoClearTask.clear finish"); } diff --git a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/task/PathBookmarkGroupAutoClearTask.java b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/task/PathBookmarkGroupAutoClearTask.java index f1af9525..4e763d21 100644 --- a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/task/PathBookmarkGroupAutoClearTask.java +++ b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/task/PathBookmarkGroupAutoClearTask.java @@ -56,7 +56,7 @@ public class PathBookmarkGroupAutoClearTask { public void clear() { log.info("PathBookmarkGroupAutoClearTask.clear start"); // 获取锁并执行 - LockerUtils.tryLock(LOCK_KEY, pathBookmarkGroupService::clearUnusedGroup); + LockerUtils.tryLockExecute(LOCK_KEY, pathBookmarkGroupService::clearUnusedGroup); log.info("PathBookmarkGroupAutoClearTask.clear finish"); } diff --git a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/task/TerminalConnectLogAutoClearTask.java b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/task/TerminalConnectLogAutoClearTask.java index 806f7820..73ba13dd 100644 --- a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/task/TerminalConnectLogAutoClearTask.java +++ b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/task/TerminalConnectLogAutoClearTask.java @@ -70,7 +70,7 @@ public class TerminalConnectLogAutoClearTask { return; } // 获取锁并执行 - LockerUtils.tryLock(LOCK_KEY, this::doClear); + LockerUtils.tryLockExecute(LOCK_KEY, this::doClear); log.info("TerminalConnectLogAutoClearTask.clear finish"); } From 0b7faa038a3eb1be92e88c516b64063f680199f4 Mon Sep 17 00:00:00 2001 From: lijiahangmax Date: Tue, 9 Sep 2025 21:25:44 +0800 Subject: [PATCH 08/17] =?UTF-8?q?:hammer:=20=E7=9B=91=E6=8E=A7=E9=80=BB?= =?UTF-8?q?=E8=BE=91.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/service/Dockerfile | 7 + docker/service/entrypoint.sh | 23 + .../constant/AutoConfigureOrderConst.java | 18 +- .../visor/common/constant/BeanOrderConst.java | 21 +- .../common/constant/CustomHeaderConst.java | 4 + .../visor/common/constant/ErrorCode.java | 2 + .../visor/common/constant/ErrorMessage.java | 47 +- .../common/constant/ExtraFieldConst.java | 4 + .../visor/common/constant/FieldConst.java | 16 + .../visor/common/constant/FileConst.java | 12 + .../common/entity/chart/TimeChartSeries.java | 60 +++ .../session/config/BaseConnectConfig.java | 3 + .../session/config/IBaseConnectConfig.java | 4 + .../visor/common/validator/group/Id.java | 2 +- .../visor/common/validator/group/Key.java | 33 ++ orion-visor-dependencies/pom.xml | 19 +- .../OrionEncryptAutoConfiguration.java | 6 +- .../pom.xml | 39 ++ .../OrionInfluxdbAutoConfiguration.java | 84 +++ .../configuration/config/InfluxdbConfig.java | 59 +++ .../influxdb/core/query/FluxQueryBuilder.java | 349 +++++++++++++ .../influxdb/core/utils/InfluxdbUtils.java | 183 +++++++ ...itional-spring-configuration-metadata.json | 37 ++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../OrionAsyncAutoConfiguration.java | 28 +- .../AbstractLogPrinterInterceptor.java | 31 +- .../PrettyLogPrinterInterceptor.java | 12 +- .../interceptor/RowLogPrinterInterceptor.java | 10 +- .../framework/mybatis/core/domain/BaseDO.java | 4 +- .../core/generator/CodeGenerators.java | 39 +- .../core/generator/core/CodeGenerator.java | 35 +- .../core/generator/core/DictParser.java | 2 +- .../core/generator/template/DictTemplate.java | 10 + ...rion-vue-views-components-card-list.vue.vm | 4 +- .../orion-vue-views-components-table.vue.vm | 2 +- .../orion-vue-views-types-table.columns.ts.vm | 2 + .../redis/core/utils/RedisStrings.java | 185 ++++--- .../AuthenticationEntryPointHandler.java | 1 + .../OrionWebAutoConfiguration.java | 26 +- .../configuration/config/ExposeApiConfig.java | 49 ++ .../web/core/annotation/ExposeApi.java | 46 ++ .../web/core/aspect/DemoDisableApiAspect.java | 2 - .../web/core/aspect/ExposeApiAspect.java | 79 +++ .../core/handler/GlobalExceptionHandler.java | 1 + ...itional-spring-configuration-metadata.json | 17 + orion-visor-framework/pom.xml | 3 +- orion-visor-launch/pom.xml | 9 + .../visor/launch/LaunchApplication.java | 8 +- .../orion-visor-module-asset-provider/pom.xml | 5 + .../visor/module/asset/api/HostAgentApi.java | 62 +++ .../visor/module/asset/api/HostApi.java | 35 ++ .../entity/dto/host/HostAgentLogDTO.java | 66 +++ .../asset/entity/dto/host/HostBaseDTO.java | 4 +- .../module/asset/entity/dto/host/HostDTO.java | 25 +- .../asset/entity/dto/host/HostQueryDTO.java | 110 ++++ .../asset/enums/AgentInstallStatusEnum.java | 65 +++ .../asset/enums/AgentOnlineStatusEnum.java | 65 +++ .../module/asset/enums/HostOsTypeEnum.java | 8 +- .../module/asset/enums/HostTypeEnum.java | 16 + .../orion-visor-module-asset-service/pom.xml | 5 + .../asset/api/impl/HostAgentApiImpl.java | 129 +++++ .../module/asset/api/impl/HostApiImpl.java | 44 ++ .../asset/controller/HostAgentController.java | 120 +++++ .../HostAgentEndpointController.java | 99 ++++ .../asset/controller/HostController.java | 5 +- .../asset/convert/HostAgentLogConvert.java | 44 ++ .../convert/HostAgentLogProviderConvert.java | 44 ++ .../module/asset/convert/HostConvert.java | 3 + .../asset/convert/HostProviderConvert.java | 4 + .../module/asset/dao/HostAgentLogDAO.java | 39 ++ .../visor/module/asset/dao/HostDAO.java | 45 ++ .../module/asset/dao/HostIdentityDAO.java | 1 + .../module/asset/define/AssetThreadPools.java | 53 ++ .../define/cache/HostCacheKeyDefine.java | 8 + .../define/operator/HostOperatorType.java | 9 + .../asset/entity/domain/HostAgentLogDO.java | 73 +++ .../module/asset/entity/domain/HostDO.java | 22 + .../module/asset/entity/dto/HostCacheDTO.java | 6 +- .../request/host/HostAgentInstallRequest.java | 52 ++ .../HostAgentInstallStatusUpdateRequest.java | 60 +++ .../entity/request/host/HostQueryRequest.java | 27 +- .../asset/entity/vo/HostAgentLogVO.java | 83 +++ .../asset/entity/vo/HostAgentStatusVO.java | 64 +++ .../entity/vo/HostOnlineAgentConfigVO.java | 52 ++ .../asset/enums/AgentLogStatusEnum.java | 54 ++ .../module/asset/enums/AgentLogTypeEnum.java | 59 +++ .../intstall/AbstractAgentInstaller.java | 191 +++++++ .../agent/intstall/AgentInstaller.java | 56 ++ .../agent/intstall/LinuxAgentInstaller.java | 79 +++ .../agent/intstall/WindowsAgentInstaller.java | 64 +++ .../agent/model/AgentInstallParams.java | 85 +++ .../host/extra/model/HostSpecExtraModel.java | 7 +- .../extra/strategy/HostSpecExtraStrategy.java | 5 + .../service/HostAgentEndpointService.java | 64 +++ .../asset/service/HostAgentLogService.java | 64 +++ .../asset/service/HostAgentService.java | 69 +++ .../asset/service/HostExtraService.java | 10 + .../module/asset/service/HostService.java | 5 +- .../impl/HostAgentEndpointServiceImpl.java | 257 ++++++++++ .../service/impl/HostAgentLogServiceImpl.java | 108 ++++ .../service/impl/HostAgentServiceImpl.java | 323 ++++++++++++ .../service/impl/HostConnectServiceImpl.java | 1 + .../service/impl/HostExtraServiceImpl.java | 46 ++ .../asset/service/impl/HostServiceImpl.java | 45 +- .../asset/task/AgentHeartbeatCheckTask.java | 52 ++ .../resources/mapper/HostAgentLogMapper.xml | 25 + .../src/main/resources/mapper/HostMapper.xml | 7 +- .../exec/controller/UploadTaskController.java | 3 +- .../module/exec/define/ExecThreadPools.java | 2 +- .../handler/BaseExecCommandHandler.java | 2 + .../service/impl/UploadTaskServiceImpl.java | 7 +- .../visor/module/infra/api/SystemUserApi.java | 8 + .../infra/api/impl/SystemUserApiImpl.java | 10 + .../pom.xml | 26 + .../module/monitor/api/MonitorHostApi.java | 51 ++ .../visor/module/monitor/entity/dto/.gitkeep | 0 .../pom.xml | 110 ++++ .../monitor/api/impl/MonitorHostApiImpl.java | 71 +++ .../module/monitor/constant/MetricsConst.java | 77 +++ .../MonitorAgentEndpointController.java | 78 +++ .../controller/MonitorHostController.java | 112 ++++ .../controller/MonitorMetricsController.java | 122 +++++ .../monitor/convert/MonitorHostConvert.java | 57 +++ .../convert/MonitorMetricsConvert.java | 62 +++ .../module/monitor/dao/MonitorHostDAO.java | 109 ++++ .../module/monitor/dao/MonitorMetricsDAO.java | 39 ++ .../cache/MonitorMetricsCacheKeyDefine.java | 49 ++ .../define/context/MonitorContext.java | 133 +++++ .../operator/MonitorHostOperatorType.java | 53 ++ .../operator/MonitorMetricsOperatorType.java | 56 ++ .../monitor/entity/domain/MonitorHostDO.java | 85 +++ .../entity/domain/MonitorMetricsDO.java | 77 +++ .../monitor/entity/dto/HostMetaDTO.java | 55 ++ .../module/monitor/entity/dto/MetricsDTO.java | 56 ++ .../monitor/entity/dto/MetricsDataDTO.java | 51 ++ .../entity/dto/MonitorHostConfigDTO.java | 56 ++ .../entity/dto/MonitorHostMetaDTO.java | 60 +++ .../entity/dto/MonitorMetricsCacheDTO.java | 84 +++ .../MonitorHostAgentConfigUpdateRequest.java | 56 ++ .../request/host/MonitorHostChartRequest.java | 75 +++ .../host/MonitorHostMetaSyncRequest.java | 54 ++ .../request/host/MonitorHostQueryRequest.java | 97 ++++ .../host/MonitorHostSwitchUpdateRequest.java | 58 +++ .../host/MonitorHostUpdatePolicyRequest.java | 58 +++ .../host/MonitorHostUpdateRequest.java | 75 +++ .../metrics/MonitorMetricsCreateRequest.java | 79 +++ .../metrics/MonitorMetricsQueryRequest.java | 70 +++ .../metrics/MonitorMetricsUpdateRequest.java | 84 +++ .../entity/vo/MonitorHostMetricsDataVO.java | 100 ++++ .../monitor/entity/vo/MonitorHostVO.java | 124 +++++ .../monitor/entity/vo/MonitorMetricsVO.java | 83 +++ .../monitor/enums/MeasurementFieldEnum.java | 159 ++++++ .../monitor/enums/MetricsAggregateEnum.java | 75 +++ .../module/monitor/enums/MetricsUnitEnum.java | 98 ++++ .../monitor/enums/MonitorAlarmSwitchEnum.java | 75 +++ .../service/MonitorAgentEndpointService.java | 53 ++ .../monitor/service/MonitorHostService.java | 85 +++ .../service/MonitorMetricsService.java | 107 ++++ .../impl/MonitorAgentEndpointServiceImpl.java | 168 ++++++ .../service/impl/MonitorHostServiceImpl.java | 472 +++++++++++++++++ .../impl/MonitorMetricsServiceImpl.java | 226 ++++++++ .../module/monitor/utils/MetricsUtils.java | 78 +++ .../resources/mapper/MonitorHostMapper.xml | 28 + .../resources/mapper/MonitorMetricsMapper.xml | 27 + .../visor/module/monitor/api/impl/.gitkeep | 0 .../module/monitor/service/impl/.gitkeep | 0 .../test/resources/application-unit-test.yaml | 30 ++ .../src/test/resources/sql/.gitkeep | 0 .../orion-visor-module-monitor/pom.xml | 23 + orion-visor-modules/pom.xml | 1 + orion-visor-ui/src/api/asset/host-agent.ts | 102 ++++ orion-visor-ui/src/api/asset/host-config.ts | 1 - orion-visor-ui/src/api/asset/host-extra.ts | 2 +- orion-visor-ui/src/api/asset/host.ts | 9 +- orion-visor-ui/src/api/interceptor.ts | 2 +- orion-visor-ui/src/api/monitor/metrics.ts | 95 ++++ .../src/api/monitor/monitor-host.ts | 168 ++++++ .../src/api/terminal/terminal-sftp.ts | 2 +- .../src/assets/style/arco-extends.less | 8 +- orion-visor-ui/src/assets/style/chart.less | 49 ++ orion-visor-ui/src/assets/style/global.less | 8 + orion-visor-ui/src/assets/style/layout.less | 6 +- .../src/components/app/setting/index.vue | 9 + .../src/components/app/tab-bar/index.vue | 29 +- .../src/components/app/tab-bar/tab-item.vue | 32 +- .../view/card-list/components/card-item.vue | 2 +- .../src/components/view/chart/index.vue | 5 +- .../src/components/view/exec-editor/const.ts | 3 + orion-visor-ui/src/main.ts | 1 + orion-visor-ui/src/router/routes/base.ts | 3 + .../src/router/routes/modules/monitor.ts | 38 ++ orion-visor-ui/src/router/typings.d.ts | 4 +- orion-visor-ui/src/store/modules/app/index.ts | 1 + orion-visor-ui/src/store/modules/app/types.ts | 1 + orion-visor-ui/src/types/chart.ts | 127 +++-- orion-visor-ui/src/types/global.ts | 7 + orion-visor-ui/src/utils/charts.ts | 10 + orion-visor-ui/src/utils/file.ts | 2 +- orion-visor-ui/src/utils/metrics.ts | 216 ++++++++ .../host-list/components/host-card-list.vue | 11 +- .../host-list/components/host-form-spec.vue | 78 +-- .../asset/host-list/components/host-table.vue | 49 +- .../src/views/asset/host-list/types/const.ts | 12 + .../asset/host-list/types/table.columns.ts | 4 +- .../components/operator-log-chart.vue | 7 +- .../components/workplace-statistics.vue | 7 +- .../metrics/components/metrics-form-modal.vue | 178 +++++++ .../metrics/components/metrics-table.vue | 221 ++++++++ .../src/views/monitor/metrics/index.vue | 46 ++ .../src/views/monitor/metrics/types/const.ts | 10 + .../views/monitor/metrics/types/form.rules.ts | 52 ++ .../monitor/metrics/types/table.columns.ts | 98 ++++ .../compoments/detail-header.vue | 207 ++++++++ .../compoments/metrics-chart-tab.vue | 179 +++++++ .../compoments/metrics-chart.vue | 241 +++++++++ .../views/monitor/monitor-detail/index.vue | 112 ++++ .../monitor/monitor-detail/types/const.ts | 45 ++ .../monitor-host/components/monitor-cell.vue | 68 +++ .../components/monitor-host-card-list.vue | 418 +++++++++++++++ .../components/monitor-host-form-drawer.vue | 150 ++++++ .../components/monitor-host-table.vue | 484 ++++++++++++++++++ .../components/release-upload-modal.vue | 112 ++++ .../src/views/monitor/monitor-host/index.vue | 65 +++ .../monitor/monitor-host/types/card.fields.ts | 81 +++ .../views/monitor/monitor-host/types/const.ts | 32 ++ .../monitor/monitor-host/types/form.rules.ts | 16 + .../monitor-host/types/table.columns.ts | 112 ++++ .../types/use-monitor-host-list.ts | 273 ++++++++++ .../components/view/rdp/rdp-action-bar.vue | 6 +- 229 files changed, 13303 insertions(+), 358 deletions(-) create mode 100644 docker/service/entrypoint.sh create mode 100644 orion-visor-common/src/main/java/org/dromara/visor/common/entity/chart/TimeChartSeries.java create mode 100644 orion-visor-common/src/main/java/org/dromara/visor/common/validator/group/Key.java create mode 100644 orion-visor-framework/orion-visor-spring-boot-starter-influxdb/pom.xml create mode 100644 orion-visor-framework/orion-visor-spring-boot-starter-influxdb/src/main/java/org/dromara/visor/framework/influxdb/configuration/OrionInfluxdbAutoConfiguration.java create mode 100644 orion-visor-framework/orion-visor-spring-boot-starter-influxdb/src/main/java/org/dromara/visor/framework/influxdb/configuration/config/InfluxdbConfig.java create mode 100644 orion-visor-framework/orion-visor-spring-boot-starter-influxdb/src/main/java/org/dromara/visor/framework/influxdb/core/query/FluxQueryBuilder.java create mode 100644 orion-visor-framework/orion-visor-spring-boot-starter-influxdb/src/main/java/org/dromara/visor/framework/influxdb/core/utils/InfluxdbUtils.java create mode 100644 orion-visor-framework/orion-visor-spring-boot-starter-influxdb/src/main/resources/META-INF/additional-spring-configuration-metadata.json create mode 100644 orion-visor-framework/orion-visor-spring-boot-starter-influxdb/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 orion-visor-framework/orion-visor-spring-boot-starter-web/src/main/java/org/dromara/visor/framework/web/configuration/config/ExposeApiConfig.java create mode 100644 orion-visor-framework/orion-visor-spring-boot-starter-web/src/main/java/org/dromara/visor/framework/web/core/annotation/ExposeApi.java create mode 100644 orion-visor-framework/orion-visor-spring-boot-starter-web/src/main/java/org/dromara/visor/framework/web/core/aspect/ExposeApiAspect.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/api/HostAgentApi.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/entity/dto/host/HostAgentLogDTO.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/entity/dto/host/HostQueryDTO.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/enums/AgentInstallStatusEnum.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/enums/AgentOnlineStatusEnum.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/api/impl/HostAgentApiImpl.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/controller/HostAgentController.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/controller/HostAgentEndpointController.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/convert/HostAgentLogConvert.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/convert/HostAgentLogProviderConvert.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/dao/HostAgentLogDAO.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/define/AssetThreadPools.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/domain/HostAgentLogDO.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/request/host/HostAgentInstallRequest.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/request/host/HostAgentInstallStatusUpdateRequest.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/vo/HostAgentLogVO.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/vo/HostAgentStatusVO.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/vo/HostOnlineAgentConfigVO.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/enums/AgentLogStatusEnum.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/enums/AgentLogTypeEnum.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/agent/intstall/AbstractAgentInstaller.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/agent/intstall/AgentInstaller.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/agent/intstall/LinuxAgentInstaller.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/agent/intstall/WindowsAgentInstaller.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/agent/model/AgentInstallParams.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/HostAgentEndpointService.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/HostAgentLogService.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/HostAgentService.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostAgentEndpointServiceImpl.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostAgentLogServiceImpl.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostAgentServiceImpl.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/task/AgentHeartbeatCheckTask.java create mode 100644 orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/resources/mapper/HostAgentLogMapper.xml create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-provider/pom.xml create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-provider/src/main/java/org/dromara/visor/module/monitor/api/MonitorHostApi.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-provider/src/main/java/org/dromara/visor/module/monitor/entity/dto/.gitkeep create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/pom.xml create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/api/impl/MonitorHostApiImpl.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/constant/MetricsConst.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/controller/MonitorAgentEndpointController.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/controller/MonitorHostController.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/controller/MonitorMetricsController.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/convert/MonitorHostConvert.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/convert/MonitorMetricsConvert.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/dao/MonitorHostDAO.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/dao/MonitorMetricsDAO.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/define/cache/MonitorMetricsCacheKeyDefine.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/define/context/MonitorContext.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/define/operator/MonitorHostOperatorType.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/define/operator/MonitorMetricsOperatorType.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/domain/MonitorHostDO.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/domain/MonitorMetricsDO.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/dto/HostMetaDTO.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/dto/MetricsDTO.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/dto/MetricsDataDTO.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/dto/MonitorHostConfigDTO.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/dto/MonitorHostMetaDTO.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/dto/MonitorMetricsCacheDTO.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/host/MonitorHostAgentConfigUpdateRequest.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/host/MonitorHostChartRequest.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/host/MonitorHostMetaSyncRequest.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/host/MonitorHostQueryRequest.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/host/MonitorHostSwitchUpdateRequest.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/host/MonitorHostUpdatePolicyRequest.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/host/MonitorHostUpdateRequest.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/metrics/MonitorMetricsCreateRequest.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/metrics/MonitorMetricsQueryRequest.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/metrics/MonitorMetricsUpdateRequest.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/vo/MonitorHostMetricsDataVO.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/vo/MonitorHostVO.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/vo/MonitorMetricsVO.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/enums/MeasurementFieldEnum.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/enums/MetricsAggregateEnum.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/enums/MetricsUnitEnum.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/enums/MonitorAlarmSwitchEnum.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/service/MonitorAgentEndpointService.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/service/MonitorHostService.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/service/MonitorMetricsService.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/service/impl/MonitorAgentEndpointServiceImpl.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/service/impl/MonitorHostServiceImpl.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/service/impl/MonitorMetricsServiceImpl.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/utils/MetricsUtils.java create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/resources/mapper/MonitorHostMapper.xml create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/resources/mapper/MonitorMetricsMapper.xml create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/test/java/org/dromara/visor/module/monitor/api/impl/.gitkeep create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/test/java/org/dromara/visor/module/monitor/service/impl/.gitkeep create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/test/resources/application-unit-test.yaml create mode 100644 orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/test/resources/sql/.gitkeep create mode 100644 orion-visor-modules/orion-visor-module-monitor/pom.xml create mode 100644 orion-visor-ui/src/api/asset/host-agent.ts create mode 100644 orion-visor-ui/src/api/monitor/metrics.ts create mode 100644 orion-visor-ui/src/api/monitor/monitor-host.ts create mode 100644 orion-visor-ui/src/assets/style/chart.less create mode 100644 orion-visor-ui/src/router/routes/modules/monitor.ts create mode 100644 orion-visor-ui/src/utils/charts.ts create mode 100644 orion-visor-ui/src/utils/metrics.ts create mode 100644 orion-visor-ui/src/views/monitor/metrics/components/metrics-form-modal.vue create mode 100644 orion-visor-ui/src/views/monitor/metrics/components/metrics-table.vue create mode 100644 orion-visor-ui/src/views/monitor/metrics/index.vue create mode 100644 orion-visor-ui/src/views/monitor/metrics/types/const.ts create mode 100644 orion-visor-ui/src/views/monitor/metrics/types/form.rules.ts create mode 100644 orion-visor-ui/src/views/monitor/metrics/types/table.columns.ts create mode 100644 orion-visor-ui/src/views/monitor/monitor-detail/compoments/detail-header.vue create mode 100644 orion-visor-ui/src/views/monitor/monitor-detail/compoments/metrics-chart-tab.vue create mode 100644 orion-visor-ui/src/views/monitor/monitor-detail/compoments/metrics-chart.vue create mode 100644 orion-visor-ui/src/views/monitor/monitor-detail/index.vue create mode 100644 orion-visor-ui/src/views/monitor/monitor-detail/types/const.ts create mode 100644 orion-visor-ui/src/views/monitor/monitor-host/components/monitor-cell.vue create mode 100644 orion-visor-ui/src/views/monitor/monitor-host/components/monitor-host-card-list.vue create mode 100644 orion-visor-ui/src/views/monitor/monitor-host/components/monitor-host-form-drawer.vue create mode 100644 orion-visor-ui/src/views/monitor/monitor-host/components/monitor-host-table.vue create mode 100644 orion-visor-ui/src/views/monitor/monitor-host/components/release-upload-modal.vue create mode 100644 orion-visor-ui/src/views/monitor/monitor-host/index.vue create mode 100644 orion-visor-ui/src/views/monitor/monitor-host/types/card.fields.ts create mode 100644 orion-visor-ui/src/views/monitor/monitor-host/types/const.ts create mode 100644 orion-visor-ui/src/views/monitor/monitor-host/types/form.rules.ts create mode 100644 orion-visor-ui/src/views/monitor/monitor-host/types/table.columns.ts create mode 100644 orion-visor-ui/src/views/monitor/monitor-host/types/use-monitor-host-list.ts diff --git a/docker/service/Dockerfile b/docker/service/Dockerfile index b846a739..1a2bc6d4 100644 --- a/docker/service/Dockerfile +++ b/docker/service/Dockerfile @@ -17,12 +17,19 @@ RUN \ ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime && \ echo "${TZ}" > /etc/timezone +# 复制启动脚本 +COPY ./service/entrypoint.sh /app/entrypoint.sh +RUN chmod +x /app/entrypoint.sh + # 复制 jar 包 COPY ./service/orion-visor-launch.jar /app/app.jar +# 复制探针包 +ADD ./service/agent-release.tar.gz /app/agent-release # 启动检测 HEALTHCHECK --interval=15s --timeout=5s --retries=5 --start-period=10s \ CMD wget -T5 -qO- http://127.0.0.1:9200/orion-visor/api/server/bootstrap/health | grep ok || exit 1 # 启动 +ENTRYPOINT ["/app/entrypoint.sh"] CMD ["java", "-jar", "/app/app.jar"] diff --git a/docker/service/entrypoint.sh b/docker/service/entrypoint.sh new file mode 100644 index 00000000..12ee8bf0 --- /dev/null +++ b/docker/service/entrypoint.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +AGENT_RELEASE_DIR="/root/orion/orion-visor/agent-release" +DEFAULT_AGENT_DIR="/app/agent-release" + +# 确保父目录存在 +mkdir -p "$(dirname "$AGENT_RELEASE_DIR")" + +# 加载探针 +if [ -d "$AGENT_RELEASE_DIR" ] && [ -n "$(ls -A "$AGENT_RELEASE_DIR" 2>/dev/null)" ]; then + echo "Using mounted agent release: $AGENT_RELEASE_DIR" +else + echo "Using default agent release: $DEFAULT_AGENT_DIR" + # 复制探针 + cp -rf "$DEFAULT_AGENT_DIR" "$AGENT_RELEASE_DIR" +fi + +# 打印探针版本信息 +if [ -f "$AGENT_RELEASE_DIR/.version" ]; then + echo "Agent version: $(cat "$AGENT_RELEASE_DIR/.version")" +fi + +exec "$@" \ No newline at end of file diff --git a/orion-visor-common/src/main/java/org/dromara/visor/common/constant/AutoConfigureOrderConst.java b/orion-visor-common/src/main/java/org/dromara/visor/common/constant/AutoConfigureOrderConst.java index e652083a..2b089ba3 100644 --- a/orion-visor-common/src/main/java/org/dromara/visor/common/constant/AutoConfigureOrderConst.java +++ b/orion-visor-common/src/main/java/org/dromara/visor/common/constant/AutoConfigureOrderConst.java @@ -53,21 +53,23 @@ public interface AutoConfigureOrderConst { int FRAMEWORK_REDIS_CACHE = Integer.MIN_VALUE + 2000; - int FRAMEWORK_CONFIG = Integer.MIN_VALUE + 2100; + int FRAMEWORK_INFLUXDB = Integer.MIN_VALUE + 2100; - int FRAMEWORK_ENCRYPT = Integer.MIN_VALUE + 2200; + int FRAMEWORK_CONFIG = Integer.MIN_VALUE + 2300; - int FRAMEWORK_STORAGE = Integer.MIN_VALUE + 2300; + int FRAMEWORK_CYPHER = Integer.MIN_VALUE + 2400; - int FRAMEWORK_JOB = Integer.MIN_VALUE + 2400; + int FRAMEWORK_STORAGE = Integer.MIN_VALUE + 2500; - int FRAMEWORK_JOB_QUARTZ = Integer.MIN_VALUE + 2500; + int FRAMEWORK_JOB = Integer.MIN_VALUE + 2600; - int FRAMEWORK_JOB_ASYNC = Integer.MIN_VALUE + 2600; + int FRAMEWORK_JOB_QUARTZ = Integer.MIN_VALUE + 2700; - int FRAMEWORK_MONITOR = Integer.MIN_VALUE + 2700; + int FRAMEWORK_JOB_ASYNC = Integer.MIN_VALUE + 2800; - int FRAMEWORK_BIZ_OPERATOR_LOG = Integer.MIN_VALUE + 2800; + int FRAMEWORK_MONITOR = Integer.MIN_VALUE + 2900; + + int FRAMEWORK_BIZ_OPERATOR_LOG = Integer.MIN_VALUE + 3000; int FRAMEWORK_BANNER = Integer.MIN_VALUE + 10000; diff --git a/orion-visor-common/src/main/java/org/dromara/visor/common/constant/BeanOrderConst.java b/orion-visor-common/src/main/java/org/dromara/visor/common/constant/BeanOrderConst.java index a84c8b3b..7c898e04 100644 --- a/orion-visor-common/src/main/java/org/dromara/visor/common/constant/BeanOrderConst.java +++ b/orion-visor-common/src/main/java/org/dromara/visor/common/constant/BeanOrderConst.java @@ -31,24 +31,29 @@ package org.dromara.visor.common.constant; */ public interface BeanOrderConst { - /** - * 公共返回值包装处理器 - */ - int RESPONSE_ADVICE_WRAPPER = Integer.MIN_VALUE + 1000; - /** * 演示模式切面 */ - int DEMO_DISABLE_API_ASPECT = Integer.MIN_VALUE + 10; + int DEMO_DISABLE_API_ASPECT = Integer.MIN_VALUE + 100; /** * 全局日志打印 */ - int LOG_PRINT_ASPECT = Integer.MIN_VALUE + 20; + int LOG_PRINT_ASPECT = Integer.MIN_VALUE + 200; + + /** + * 暴露接口切面 + */ + int EXPOSE_API_ASPECT = Integer.MIN_VALUE + 300; /** * 操作日志切面 */ - int OPERATOR_LOG_ASPECT = Integer.MIN_VALUE + 30; + int OPERATOR_LOG_ASPECT = Integer.MIN_VALUE + 400; + + /** + * 公共返回值包装处理器 + */ + int RESPONSE_ADVICE_WRAPPER = Integer.MIN_VALUE + 1000; } diff --git a/orion-visor-common/src/main/java/org/dromara/visor/common/constant/CustomHeaderConst.java b/orion-visor-common/src/main/java/org/dromara/visor/common/constant/CustomHeaderConst.java index 3f1017cc..5e96e40d 100644 --- a/orion-visor-common/src/main/java/org/dromara/visor/common/constant/CustomHeaderConst.java +++ b/orion-visor-common/src/main/java/org/dromara/visor/common/constant/CustomHeaderConst.java @@ -33,4 +33,8 @@ public interface CustomHeaderConst { String APP_VERSION = "X-App-Version"; + String AGENT_KEY_HEADER = "X-Agent-Key"; + + String AGENT_VERSION_HEADER = "X-Agent-Version"; + } diff --git a/orion-visor-common/src/main/java/org/dromara/visor/common/constant/ErrorCode.java b/orion-visor-common/src/main/java/org/dromara/visor/common/constant/ErrorCode.java index 9e981d47..d1dcd803 100644 --- a/orion-visor-common/src/main/java/org/dromara/visor/common/constant/ErrorCode.java +++ b/orion-visor-common/src/main/java/org/dromara/visor/common/constant/ErrorCode.java @@ -48,6 +48,8 @@ public enum ErrorCode implements CodeInfo { UNAUTHORIZED(401, "当前认证信息已失效, 请重新登录"), + EXPOSE_UNAUTHORIZED(401, "当前认证信息错误, 请检查后重试"), + FORBIDDEN(403, "无操作权限"), NOT_FOUND(404, "未找到该资源"), diff --git a/orion-visor-common/src/main/java/org/dromara/visor/common/constant/ErrorMessage.java b/orion-visor-common/src/main/java/org/dromara/visor/common/constant/ErrorMessage.java index ae874b64..c1463e93 100644 --- a/orion-visor-common/src/main/java/org/dromara/visor/common/constant/ErrorMessage.java +++ b/orion-visor-common/src/main/java/org/dromara/visor/common/constant/ErrorMessage.java @@ -102,7 +102,7 @@ public interface ErrorMessage { String HOST_TYPE_ERROR = "主机类型错误"; - String HOST_NOT_ENABLED = "主机未启用"; + String HOST_NOT_ENABLED = "{} 主机未启用"; String CONFIG_NOT_ENABLED = "配置未启用"; @@ -146,6 +146,8 @@ public interface ErrorMessage { String FILE_ABSENT = "文件不存在"; + String FILE_EXTENSION_TYPE = "文件类型不正确"; + String FILE_ABSENT_CLEAR = "文件不存在 (可能已被清理)"; String LOG_ABSENT = "日志不存在"; @@ -158,6 +160,8 @@ public interface ErrorMessage { String FILE_UPLOAD_ERROR = "文件上传失败"; + String CALC_SIGN_FAILED = "计算签名失败"; + String SCRIPT_UPLOAD_ERROR = "脚本上传失败"; String EXEC_ERROR = "执行失败"; @@ -182,6 +186,8 @@ public interface ErrorMessage { String COMPRESS_FILE_ABSENT = "压缩文件不存在"; + String DECOMPRESS_FILE_ABSENT = "压缩文件不存在"; + String UNABLE_DOWNLOAD_FOLDER = "无法下载文件夹"; String VALID_ERROR = "验证失败"; @@ -209,6 +215,27 @@ public interface ErrorMessage { || ex instanceof ApplicationException; } + /** + * 获取错误信息 + * + * @param ex ex + * @return message + */ + static String getErrorMessage(Exception ex) { + return getErrorMessage(ex, ErrorMessage.EXEC_ERROR, 0); + } + + /** + * 获取错误信息 + * + * @param ex ex + * @param len len + * @return message + */ + static String getErrorMessage(Exception ex, int len) { + return getErrorMessage(ex, ErrorMessage.EXEC_ERROR, len); + } + /** * 获取错误信息 * @@ -217,6 +244,18 @@ public interface ErrorMessage { * @return message */ static String getErrorMessage(Exception ex, String defaultMsg) { + return getErrorMessage(ex, defaultMsg, 0); + } + + /** + * 获取错误信息 + * + * @param ex ex + * @param defaultMsg defaultMsg + * @param len len + * @return message + */ + static String getErrorMessage(Exception ex, String defaultMsg, int len) { if (ex == null) { return null; } @@ -226,7 +265,11 @@ public interface ErrorMessage { } // 业务异常 if (isBizException(ex)) { - return message; + if (len > 0) { + return Strings.retain(message, len); + } else { + return message; + } } return defaultMsg; } diff --git a/orion-visor-common/src/main/java/org/dromara/visor/common/constant/ExtraFieldConst.java b/orion-visor-common/src/main/java/org/dromara/visor/common/constant/ExtraFieldConst.java index acbaf3e1..830d6739 100644 --- a/orion-visor-common/src/main/java/org/dromara/visor/common/constant/ExtraFieldConst.java +++ b/orion-visor-common/src/main/java/org/dromara/visor/common/constant/ExtraFieldConst.java @@ -35,6 +35,8 @@ public interface ExtraFieldConst extends FieldConst { String TRACE_ID = "traceId"; + String TASK_ID = "taskId"; + String IDENTITY = "identity"; String GROUP_NAME = "groupName"; @@ -69,4 +71,6 @@ public interface ExtraFieldConst extends FieldConst { String DARK = "dark"; + String AGENT_KEY = "agentKey"; + } diff --git a/orion-visor-common/src/main/java/org/dromara/visor/common/constant/FieldConst.java b/orion-visor-common/src/main/java/org/dromara/visor/common/constant/FieldConst.java index 9097021a..539875e1 100644 --- a/orion-visor-common/src/main/java/org/dromara/visor/common/constant/FieldConst.java +++ b/orion-visor-common/src/main/java/org/dromara/visor/common/constant/FieldConst.java @@ -45,12 +45,18 @@ public interface FieldConst { String LABEL = "label"; + String FIELD = "field"; + String TYPE = "type"; String COLOR = "color"; + String LOADING = "loading"; + String STATUS = "status"; + String SWITCH = "switch"; + String INFO = "info"; String EXTRA = "extra"; @@ -71,6 +77,8 @@ public interface FieldConst { String SEQ = "seq"; + String START = "start"; + String PATH = "path"; String ADDRESS = "address"; @@ -119,4 +127,12 @@ public interface FieldConst { String CONFIG = "config"; + String VERSION = "version"; + + String SYNCED = "synced"; + + String SIGN = "sign"; + + String SIGN_SHORT = "signShort"; + } diff --git a/orion-visor-common/src/main/java/org/dromara/visor/common/constant/FileConst.java b/orion-visor-common/src/main/java/org/dromara/visor/common/constant/FileConst.java index df4b1d5f..8995c4f3 100644 --- a/orion-visor-common/src/main/java/org/dromara/visor/common/constant/FileConst.java +++ b/orion-visor-common/src/main/java/org/dromara/visor/common/constant/FileConst.java @@ -37,4 +37,16 @@ public interface FileConst { String SCRIPT = "script"; + String AGENT = "agent"; + + String AGENT_RELEASE = "agent-release"; + + String AGENT_RELEASE_TEMP = "agent-release-temp"; + + String AGENT_RELEASE_TAR_GZ = "agent-release.tar.gz"; + + String VERSION = ".version"; + + String CONFIG_YAML = "config.yaml"; + } diff --git a/orion-visor-common/src/main/java/org/dromara/visor/common/entity/chart/TimeChartSeries.java b/orion-visor-common/src/main/java/org/dromara/visor/common/entity/chart/TimeChartSeries.java new file mode 100644 index 00000000..670b8c62 --- /dev/null +++ b/orion-visor-common/src/main/java/org/dromara/visor/common/entity/chart/TimeChartSeries.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.common.entity.chart; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * 时序图系列 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/9/3 21:08 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(name = "TimeChartSeries", description = "时序图系列") +public class TimeChartSeries { + + @Schema(description = "name") + private String name; + + @Schema(description = "颜色") + private String color; + + @Schema(description = "tags") + private Map tags; + + @Schema(description = "数据 [0]timestampMills [1]value") + private List> data; + +} diff --git a/orion-visor-common/src/main/java/org/dromara/visor/common/session/config/BaseConnectConfig.java b/orion-visor-common/src/main/java/org/dromara/visor/common/session/config/BaseConnectConfig.java index e9d0d82b..785d8ab9 100644 --- a/orion-visor-common/src/main/java/org/dromara/visor/common/session/config/BaseConnectConfig.java +++ b/orion-visor-common/src/main/java/org/dromara/visor/common/session/config/BaseConnectConfig.java @@ -63,6 +63,9 @@ public class BaseConnectConfig implements IBaseConnectConfig { @Schema(description = "主机端口") private Integer hostPort; + @Schema(description = "agentKey") + private String agentKey; + @Schema(description = "用户名") private String username; diff --git a/orion-visor-common/src/main/java/org/dromara/visor/common/session/config/IBaseConnectConfig.java b/orion-visor-common/src/main/java/org/dromara/visor/common/session/config/IBaseConnectConfig.java index f6893156..cbba6814 100644 --- a/orion-visor-common/src/main/java/org/dromara/visor/common/session/config/IBaseConnectConfig.java +++ b/orion-visor-common/src/main/java/org/dromara/visor/common/session/config/IBaseConnectConfig.java @@ -61,6 +61,10 @@ public interface IBaseConnectConfig { void setHostPort(Integer hostPort); + String getAgentKey(); + + void setAgentKey(String agentKey); + String getUsername(); void setUsername(String username); diff --git a/orion-visor-common/src/main/java/org/dromara/visor/common/validator/group/Id.java b/orion-visor-common/src/main/java/org/dromara/visor/common/validator/group/Id.java index 5fccd109..6bc5930d 100644 --- a/orion-visor-common/src/main/java/org/dromara/visor/common/validator/group/Id.java +++ b/orion-visor-common/src/main/java/org/dromara/visor/common/validator/group/Id.java @@ -23,7 +23,7 @@ package org.dromara.visor.common.validator.group; /** - * 分页验证分组 + * id 验证分组 * * @author Jiahang Li * @version 1.0.0 diff --git a/orion-visor-common/src/main/java/org/dromara/visor/common/validator/group/Key.java b/orion-visor-common/src/main/java/org/dromara/visor/common/validator/group/Key.java new file mode 100644 index 00000000..6bbe8ea6 --- /dev/null +++ b/orion-visor-common/src/main/java/org/dromara/visor/common/validator/group/Key.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.common.validator.group; + +/** + * key 验证分组 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/9/1 19:13 + */ +public interface Key { +} diff --git a/orion-visor-dependencies/pom.xml b/orion-visor-dependencies/pom.xml index cd7ee7c8..6991b4f3 100644 --- a/orion-visor-dependencies/pom.xml +++ b/orion-visor-dependencies/pom.xml @@ -31,6 +31,7 @@ 1.2.16 3.18.0 2.14.2 + 6.6.0 4.11.0 1.0.7 7.2.11.RELEASE @@ -54,8 +55,12 @@ ${orion.kit.version} - orion-log cn.orionsec.kit + orion-log + + + cn.orionsec.kit + orion-generator @@ -146,6 +151,11 @@ orion-visor-spring-boot-starter-test ${revision} + + org.dromara.visor + orion-visor-spring-boot-starter-influxdb + ${revision} + org.dromara.visor orion-visor-spring-boot-starter-biz-operator-log @@ -272,6 +282,13 @@ ${transmittable.thread.local.version} + + + com.influxdb + influxdb-client-java + ${influxdb.client.version} + + org.springframework.boot diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-cipher/src/main/java/org/dromara/visor/framework/encrypt/configuration/OrionEncryptAutoConfiguration.java b/orion-visor-framework/orion-visor-spring-boot-starter-cipher/src/main/java/org/dromara/visor/framework/encrypt/configuration/OrionEncryptAutoConfiguration.java index 36110dfa..27c5864b 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-cipher/src/main/java/org/dromara/visor/framework/encrypt/configuration/OrionEncryptAutoConfiguration.java +++ b/orion-visor-framework/orion-visor-spring-boot-starter-cipher/src/main/java/org/dromara/visor/framework/encrypt/configuration/OrionEncryptAutoConfiguration.java @@ -22,10 +22,10 @@ */ package org.dromara.visor.framework.encrypt.configuration; -import org.dromara.visor.common.config.ConfigStore; -import org.dromara.visor.common.constant.AutoConfigureOrderConst; import org.dromara.visor.common.cipher.AesEncryptor; import org.dromara.visor.common.cipher.RsaDecryptor; +import org.dromara.visor.common.config.ConfigStore; +import org.dromara.visor.common.constant.AutoConfigureOrderConst; import org.dromara.visor.common.utils.AesEncryptUtils; import org.dromara.visor.common.utils.RsaParamDecryptUtils; import org.dromara.visor.framework.encrypt.configuration.config.AesEncryptConfig; @@ -45,7 +45,7 @@ import org.springframework.context.annotation.Bean; */ @AutoConfiguration @EnableConfigurationProperties({AesEncryptConfig.class}) -@AutoConfigureOrder(AutoConfigureOrderConst.FRAMEWORK_ENCRYPT) +@AutoConfigureOrder(AutoConfigureOrderConst.FRAMEWORK_CYPHER) public class OrionEncryptAutoConfiguration { /** diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-influxdb/pom.xml b/orion-visor-framework/orion-visor-spring-boot-starter-influxdb/pom.xml new file mode 100644 index 00000000..fe47372d --- /dev/null +++ b/orion-visor-framework/orion-visor-spring-boot-starter-influxdb/pom.xml @@ -0,0 +1,39 @@ + + + + org.dromara.visor + orion-visor-framework + ${revision} + + + 4.0.0 + orion-visor-spring-boot-starter-influxdb + ${project.artifactId} + jar + + 项目 influxdb 配置包 + https://github.com/dromara/orion-visor + + + + + org.dromara.visor + orion-visor-common + + + + + com.influxdb + influxdb-client-java + + + + + org.springframework.boot + spring-boot-starter-web + + + + \ No newline at end of file diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-influxdb/src/main/java/org/dromara/visor/framework/influxdb/configuration/OrionInfluxdbAutoConfiguration.java b/orion-visor-framework/orion-visor-spring-boot-starter-influxdb/src/main/java/org/dromara/visor/framework/influxdb/configuration/OrionInfluxdbAutoConfiguration.java new file mode 100644 index 00000000..e6982f35 --- /dev/null +++ b/orion-visor-framework/orion-visor-spring-boot-starter-influxdb/src/main/java/org/dromara/visor/framework/influxdb/configuration/OrionInfluxdbAutoConfiguration.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.framework.influxdb.configuration; + +import cn.orionsec.kit.lang.utils.Strings; +import com.influxdb.LogLevel; +import com.influxdb.client.InfluxDBClient; +import com.influxdb.client.InfluxDBClientFactory; +import com.influxdb.client.InfluxDBClientOptions; +import org.dromara.visor.common.constant.AutoConfigureOrderConst; +import org.dromara.visor.framework.influxdb.configuration.config.InfluxdbConfig; +import org.dromara.visor.framework.influxdb.core.utils.InfluxdbUtils; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureOrder; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Lazy; + +import java.net.ConnectException; + +/** + * influxdb 配置类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/10 20:35 + */ +@Lazy(false) +@AutoConfiguration +@AutoConfigureOrder(AutoConfigureOrderConst.FRAMEWORK_INFLUXDB) +@ConditionalOnProperty(value = "spring.influxdb.enabled", havingValue = "true") +@EnableConfigurationProperties(InfluxdbConfig.class) +public class OrionInfluxdbAutoConfiguration { + + /** + * TODO 重连 + * + * @param config config + * @return influxdb 客户端 + */ + @Bean(name = "influxDBClient") + public InfluxDBClient influxDBClient(InfluxdbConfig config) throws ConnectException { + // 参数 + InfluxDBClientOptions options = InfluxDBClientOptions.builder() + .url(config.getUrl()) + .authenticateToken(config.getToken().toCharArray()) + .org(config.getOrg()) + .bucket(config.getBucket()) + .logLevel(LogLevel.NONE) + .build(); + // 客户端 + InfluxDBClient client = InfluxDBClientFactory.create(options); + // 尝试连接 + Boolean ping = client.ping(); + if (!ping) { + throw new ConnectException(Strings.format("connect to influxdb failed. url: {}, org: {}", config.getUrl(), config.getOrg())); + } + // 设置工具类 + InfluxdbUtils.setInfluxClient(config.getBucket(), client); + return client; + } + +} diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-influxdb/src/main/java/org/dromara/visor/framework/influxdb/configuration/config/InfluxdbConfig.java b/orion-visor-framework/orion-visor-spring-boot-starter-influxdb/src/main/java/org/dromara/visor/framework/influxdb/configuration/config/InfluxdbConfig.java new file mode 100644 index 00000000..c9d53f61 --- /dev/null +++ b/orion-visor-framework/orion-visor-spring-boot-starter-influxdb/src/main/java/org/dromara/visor/framework/influxdb/configuration/config/InfluxdbConfig.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.framework.influxdb.configuration.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * influxdb 配置 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/10 20:36 + */ +@Data +@ConfigurationProperties("spring.influxdb") +public class InfluxdbConfig { + + /** + * url + */ + private String url; + + /** + * org + */ + private String org; + + /** + * bucket + */ + private String bucket; + + /** + * apiToken + */ + private String token; + +} diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-influxdb/src/main/java/org/dromara/visor/framework/influxdb/core/query/FluxQueryBuilder.java b/orion-visor-framework/orion-visor-spring-boot-starter-influxdb/src/main/java/org/dromara/visor/framework/influxdb/core/query/FluxQueryBuilder.java new file mode 100644 index 00000000..1e479080 --- /dev/null +++ b/orion-visor-framework/orion-visor-spring-boot-starter-influxdb/src/main/java/org/dromara/visor/framework/influxdb/core/query/FluxQueryBuilder.java @@ -0,0 +1,349 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.framework.influxdb.core.query; + +import cn.orionsec.kit.lang.utils.collect.Collections; +import cn.orionsec.kit.lang.utils.collect.Lists; +import org.dromara.visor.common.constant.Const; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * flux 查询构建器 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/9/3 16:08 + */ +public class FluxQueryBuilder { + + private final StringBuilder query; + + private boolean hasFilter; + + private boolean pretty; + + private FluxQueryBuilder(String bucket) { + this.query = new StringBuilder(); + this.query.append(String.format("from(bucket: \"%s\")", bucket)); + } + + /** + * 创建构建器 + * + * @param bucket bucket + * @return builder + */ + public static FluxQueryBuilder from(String bucket) { + return new FluxQueryBuilder(bucket); + } + + /** + * 时间范围 + * + * @param start 开始时间 + * @param end 结束时间 + * @return this + */ + public FluxQueryBuilder range(long start, long end) { + query.append(String.format(" |> range(start: %s, stop: %s)", Instant.ofEpochMilli(start), Instant.ofEpochMilli(end))); + return this; + } + + /** + * 时间范围 + * + * @param range range + * @return this + */ + public FluxQueryBuilder range(String range) { + query.append(String.format(" |> range(start: %s)", range)); + return this; + } + + /** + * 过滤 measurement + * + * @param measurement measurement + * @return this + */ + public FluxQueryBuilder measurement(String measurement) { + this.appendFilter(String.format("r[\"_measurement\"] == \"%s\"", measurement)); + this.closeFilter(); + return this; + } + + /** + * 过滤单个 field + * + * @param field field + * @return this + */ + public FluxQueryBuilder field(String field) { + this.appendFilter(String.format("r[\"_field\"] == \"%s\"", field)); + this.closeFilter(); + return this; + } + + /** + * 过滤多个 field + * + * @param fields fields + * @return this + */ + public FluxQueryBuilder fields(Collection fields) { + if (Collections.isEmpty(fields)) { + return this; + } + List conditions = new ArrayList<>(); + for (String field : fields) { + conditions.add(String.format("r[\"_field\"] == \"%s\"", field)); + } + this.appendFilter(String.join(" or ", conditions)); + this.closeFilter(); + return this; + } + + /** + * 过滤 tag key + * + * @param value value + * @return this + */ + public FluxQueryBuilder key(String value) { + return this.tag(Const.KEY, value); + } + + /** + * 过滤 tag key + * + * @param values values + * @return this + */ + public FluxQueryBuilder key(Collection values) { + return this.tag(Const.KEY, values); + } + + /** + * 过滤 tag name + * + * @param value value + * @return this + */ + public FluxQueryBuilder name(String value) { + return this.tag(Const.NAME, value); + } + + /** + * 过滤 tag name + * + * @param values values + * @return this + */ + public FluxQueryBuilder name(Collection values) { + return this.tag(Const.NAME, values); + } + + /** + * 过滤 tag + * + * @return this + */ + public FluxQueryBuilder tag(String key, String value) { + this.appendFilter(String.format("r[\"%s\"] == \"%s\"", key, value)); + this.closeFilter(); + return this; + } + + /** + * 过滤 tag + * + * @param key key + * @param values values + * @return this + */ + public FluxQueryBuilder tag(String key, Collection values) { + if (values == null || values.isEmpty()) { + return this; + } + if (values.size() == 1) { + return this.tag(key, Collections.first(values)); + } + // + Collection conditions = values.stream() + .map(value -> String.format("r[\"%s\"] == \"%s\"", key, value)) + .collect(Collectors.toList()); + this.appendFilter(String.join(" or ", conditions)); + this.closeFilter(); + return this; + } + + /** + * 过滤多个 tag + * tag 使用 and + * value 使用 or + * + * @param tags tags + * @return this + */ + public FluxQueryBuilder tags(Map> tags) { + for (Map.Entry> entry : tags.entrySet()) { + String key = entry.getKey(); + Collection values = entry.getValue(); + if (Collections.isEmpty(values)) { + continue; + } + if (values.size() == 1) { + // 单值直接用等号 + String singleValue = values.iterator().next(); + this.appendFilter(String.format("r[\"%s\"] == \"%s\"", key, singleValue)); + } else { + // 多值用 OR + Collection conditions = values.stream() + .map(v -> String.format("r[\"%s\"] == \"%s\"", key, v)) + .collect(Collectors.toList()); + this.appendFilter("(" + String.join(" or ", conditions) + ")"); + } + } + this.closeFilter(); + return this; + } + + /** + * 聚合窗口 + */ + public FluxQueryBuilder aggregateWindow(String every, String fn) { + query.append(String.format(" |> aggregateWindow(every: %s, fn: %s)", every, fn)); + return this; + } + + /** + * 聚合窗口 + */ + public FluxQueryBuilder aggregateWindow(String every, String fn, boolean createEmpty) { + query.append(String.format(" |> aggregateWindow(every: %s, fn: %s, createEmpty: %b)", every, fn, createEmpty)); + return this; + } + + /** + * 排序 + * + * @param columns columns + * @return this + */ + public FluxQueryBuilder sort(List columns) { + StringBuilder cols = new StringBuilder(); + for (int i = 0; i < columns.size(); i++) { + cols.append("\"").append(columns.get(i)).append("\""); + if (i < columns.size() - 1) cols.append(", "); + } + query.append(String.format(" |> sort(columns: [%s])", cols)); + return this; + } + + /** + * 降序 + * + * @param column column + * @return this + */ + public FluxQueryBuilder sortDesc(String column) { + return this.sort(Lists.singleton("-" + column)); + } + + /** + * 升序 + * + * @param column column + * @return this + */ + public FluxQueryBuilder sortAsc(String column) { + return this.sort(Lists.singleton(column)); + } + + /** + * 限制条数 + * + * @param n limit + * @return this + */ + public FluxQueryBuilder limit(int n) { + query.append(String.format(" |> limit(n: %d)", n)); + return this; + } + + /** + * 基础过滤拼接 + */ + private void appendFilter(String condition) { + if (!hasFilter) { + query.append(" |> filter(fn: (r) => "); + this.hasFilter = true; + } else { + query.append(" and "); + } + query.append(condition); + } + + /** + * 结束 filter 并闭合括号 + */ + private void closeFilter() { + if (hasFilter) { + query.append(")"); + this.hasFilter = false; + } + } + + /** + * 设置美观输出 + * + * @return this + */ + public FluxQueryBuilder pretty() { + this.pretty = true; + return this; + } + + /** + * 构建查询 + */ + public String build() { + if (this.pretty) { + return query.toString().replaceAll("\\|>", "\n |>"); + } else { + return query.toString(); + } + } + + @Override + public String toString() { + return this.build(); + } + +} diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-influxdb/src/main/java/org/dromara/visor/framework/influxdb/core/utils/InfluxdbUtils.java b/orion-visor-framework/orion-visor-spring-boot-starter-influxdb/src/main/java/org/dromara/visor/framework/influxdb/core/utils/InfluxdbUtils.java new file mode 100644 index 00000000..71a7d901 --- /dev/null +++ b/orion-visor-framework/orion-visor-spring-boot-starter-influxdb/src/main/java/org/dromara/visor/framework/influxdb/core/utils/InfluxdbUtils.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.framework.influxdb.core.utils; + +import cn.orionsec.kit.lang.utils.Exceptions; +import cn.orionsec.kit.lang.utils.collect.Lists; +import com.influxdb.client.InfluxDBClient; +import com.influxdb.client.WriteApi; +import com.influxdb.client.write.Point; +import com.influxdb.query.FluxRecord; +import com.influxdb.query.FluxTable; +import org.dromara.visor.common.constant.Const; +import org.dromara.visor.common.entity.chart.TimeChartSeries; +import org.dromara.visor.framework.influxdb.core.query.FluxQueryBuilder; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * influxdb 工具类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/10 20:47 + */ +public class InfluxdbUtils { + + private static final String FIELD_KEY = "_field"; + + private static final List SKIP_EXTRA_KEY = Lists.of("result", "table", "_measurement", "_start", "_stop", "_time", "_value"); + + private static InfluxDBClient client; + + private static String bucket; + + private InfluxdbUtils() { + } + + /** + * 写入指标 + * + * @param points points + */ + public static void writePoints(List points) { + try (WriteApi api = client.makeWriteApi()) { + // 写入指标 + api.writePoints(points); + } + } + + /** + * 查询数据点 + * + * @param query query + * @return points + */ + public static List queryTable(String query) { + return client.getQueryApi().query(query); + } + + /** + * 查询数据点 + * + * @param query query + * @return points + */ + public static FluxTable querySingleTable(String query) { + return Lists.first(queryTable(query)); + } + + /** + * 查询时序系列 + * + * @param query query + * @return points + */ + public static List querySeries(String query) { + return toSeries(queryTable(query)); + } + + /** + * 查询时序系列 + * + * @param query query + * @return points + */ + public static TimeChartSeries querySingleSeries(String query) { + return toSeries(querySingleTable(query)); + } + + /** + * 转为时序系列 + * + * @param table table + * @return series + */ + public static TimeChartSeries toSeries(FluxTable table) { + // 数据 + Map tags = new HashMap<>(); + List> dataList = new ArrayList<>(); + for (FluxRecord record : table.getRecords()) { + Instant time = record.getTime(); + if (time == null) { + continue; + } + // 设置数据 + List data = new ArrayList<>(2); + data.add(time.toEpochMilli()); + data.add(record.getValue()); + dataList.add(data); + // 设置额外值 + record.getValues().forEach((k, v) -> { + if (SKIP_EXTRA_KEY.contains(k)) { + return; + } + tags.put(k, v); + }); + } + // 设置 field + tags.put(Const.FIELD, tags.get(FIELD_KEY)); + tags.remove(FIELD_KEY); + // 创建 series + return TimeChartSeries.builder() + .data(dataList) + .tags(tags) + .build(); + } + + /** + * 转为时序系列 + * + * @param tables tables + * @return series + */ + public static List toSeries(List tables) { + return tables.stream() + .map(InfluxdbUtils::toSeries) + .collect(Collectors.toList()); + } + + /** + * 获取查询构建器 + * + * @return builder + */ + public static FluxQueryBuilder query() { + return FluxQueryBuilder.from(bucket); + } + + public static void setInfluxClient(String bucket, InfluxDBClient client) { + if (InfluxdbUtils.client != null) { + // unmodified + throw Exceptions.state(); + } + InfluxdbUtils.client = client; + InfluxdbUtils.bucket = bucket; + } + +} diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-influxdb/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/orion-visor-framework/orion-visor-spring-boot-starter-influxdb/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 00000000..28c7773f --- /dev/null +++ b/orion-visor-framework/orion-visor-spring-boot-starter-influxdb/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,37 @@ +{ + "groups": [ + { + "name": "spring.influxdb", + "type": "org.dromara.visor.framework.influxdb.configuration.config.InfluxdbConfig", + "sourceType": "org.dromara.visor.framework.influxdb.configuration.config.InfluxdbConfig" + } + ], + "properties": [ + { + "name": "spring.influxdb.enabled", + "type": "java.lang.Boolean", + "description": "是否启用 influxdb.", + "defaultValue": "false" + }, + { + "name": "spring.influxdb.url", + "type": "java.lang.String", + "description": "influxdb 地址." + }, + { + "name": "spring.influxdb.org", + "type": "java.lang.String", + "description": "influxdb org." + }, + { + "name": "spring.influxdb.bucket", + "type": "java.lang.String", + "description": "influxdb bucket." + }, + { + "name": "spring.influxdb.token", + "type": "java.lang.String", + "description": "influxdb token." + } + ] +} \ No newline at end of file diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-influxdb/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/orion-visor-framework/orion-visor-spring-boot-starter-influxdb/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..3eda3a8a --- /dev/null +++ b/orion-visor-framework/orion-visor-spring-boot-starter-influxdb/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.dromara.visor.framework.influxdb.configuration.OrionInfluxdbAutoConfiguration \ No newline at end of file diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-job/src/main/java/org/dromara/visor/framework/job/configuration/OrionAsyncAutoConfiguration.java b/orion-visor-framework/orion-visor-spring-boot-starter-job/src/main/java/org/dromara/visor/framework/job/configuration/OrionAsyncAutoConfiguration.java index 30427ded..7544e5b8 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-job/src/main/java/org/dromara/visor/framework/job/configuration/OrionAsyncAutoConfiguration.java +++ b/orion-visor-framework/orion-visor-spring-boot-starter-job/src/main/java/org/dromara/visor/framework/job/configuration/OrionAsyncAutoConfiguration.java @@ -23,13 +23,13 @@ package org.dromara.visor.framework.job.configuration; import org.dromara.visor.common.constant.AutoConfigureOrderConst; +import org.dromara.visor.common.constant.Const; import org.dromara.visor.common.thread.ThreadPoolMdcTaskExecutor; import org.dromara.visor.framework.job.configuration.config.AsyncExecutorConfig; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureOrder; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Primary; import org.springframework.core.task.TaskExecutor; import org.springframework.scheduling.annotation.EnableAsync; @@ -49,13 +49,10 @@ import java.util.concurrent.ThreadPoolExecutor; public class OrionAsyncAutoConfiguration { /** - * 支持 MDC 的异步线程池 - *

* {@code @Async("asyncExecutor")} * - * @return 异步线程池 + * @return 支持 MDC 的异步线程池 */ - @Primary @Bean(name = "asyncExecutor") public TaskExecutor asyncExecutor(AsyncExecutorConfig config) { ThreadPoolMdcTaskExecutor executor = new ThreadPoolMdcTaskExecutor(); @@ -75,4 +72,25 @@ public class OrionAsyncAutoConfiguration { return executor; } + /** + * {@code @Async("metricsExecutor")} + * + * @return 指标线程池 + */ + @Bean(name = "metricsExecutor") + public TaskExecutor metricsExecutor() { + ThreadPoolMdcTaskExecutor executor = new ThreadPoolMdcTaskExecutor(); + executor.setCorePoolSize(4); + executor.setMaxPoolSize(8); + executor.setQueueCapacity(1000); + executor.setKeepAliveSeconds(Const.MS_S_60); + executor.setAllowCoreThreadTimeOut(true); + executor.setThreadNamePrefix("metrics-task-"); + executor.setWaitForTasksToCompleteOnShutdown(true); + executor.setAwaitTerminationSeconds(60); + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + executor.initialize(); + return executor; + } + } diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-log/src/main/java/org/dromara/visor/framework/log/core/interceptor/AbstractLogPrinterInterceptor.java b/orion-visor-framework/orion-visor-spring-boot-starter-log/src/main/java/org/dromara/visor/framework/log/core/interceptor/AbstractLogPrinterInterceptor.java index 2b219c1a..f0411deb 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-log/src/main/java/org/dromara/visor/framework/log/core/interceptor/AbstractLogPrinterInterceptor.java +++ b/orion-visor-framework/orion-visor-spring-boot-starter-log/src/main/java/org/dromara/visor/framework/log/core/interceptor/AbstractLogPrinterInterceptor.java @@ -22,6 +22,7 @@ */ package org.dromara.visor.framework.log.core.interceptor; +import cn.orionsec.kit.lang.utils.Strings; import cn.orionsec.kit.lang.utils.collect.Maps; import cn.orionsec.kit.lang.utils.reflect.Classes; import com.alibaba.fastjson.JSON; @@ -31,8 +32,8 @@ import com.alibaba.fastjson.serializer.ValueFilter; import org.aopalliance.intercept.MethodInvocation; import org.dromara.visor.common.json.FieldDesensitizeFilter; import org.dromara.visor.common.json.FieldIgnoreFilter; -import org.dromara.visor.common.trace.TraceIdHolder; import org.dromara.visor.common.security.SecurityHolder; +import org.dromara.visor.common.trace.TraceIdHolder; import org.dromara.visor.framework.log.configuration.config.LogPrinterConfig; import org.dromara.visor.framework.log.core.annotation.IgnoreLog; import org.dromara.visor.framework.log.core.enums.IgnoreLogMode; @@ -42,12 +43,13 @@ import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.Date; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; -import java.util.function.Predicate; /** * 日志打印拦截器 基类 @@ -60,11 +62,6 @@ public abstract class AbstractLogPrinterInterceptor implements LogPrinterInterce private static final ThreadLocal IGNORE_LOG_MODE = new ThreadLocal<>(); - /** - * 请求头过滤器 - */ - protected Predicate headerFilter; - /** * 字段过滤器 */ @@ -93,8 +90,6 @@ public abstract class AbstractLogPrinterInterceptor implements LogPrinterInterce @Override public void init() { - // 请求头过滤器 - this.headerFilter = header -> config.getHeaders().contains(header); // 参数过滤器 this.serializeFilters = new SerializeFilter[]{ // 忽略字段过滤器 @@ -136,6 +131,24 @@ public abstract class AbstractLogPrinterInterceptor implements LogPrinterInterce } } + /** + * 获取请求头 + * + * @param request request + * @return headers + */ + protected Map getHeaderMap(HttpServletRequest request) { + Map headers = new LinkedHashMap<>(); + for (String headerName : config.getHeaders()) { + String headerValue = request.getHeader(headerName); + if (Strings.isBlank(headerValue)) { + continue; + } + headers.put(headerName, headerValue); + } + return headers; + } + /** * 打印请求信息 * diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-log/src/main/java/org/dromara/visor/framework/log/core/interceptor/PrettyLogPrinterInterceptor.java b/orion-visor-framework/orion-visor-spring-boot-starter-log/src/main/java/org/dromara/visor/framework/log/core/interceptor/PrettyLogPrinterInterceptor.java index 1031ad25..d0aee0dc 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-log/src/main/java/org/dromara/visor/framework/log/core/interceptor/PrettyLogPrinterInterceptor.java +++ b/orion-visor-framework/orion-visor-spring-boot-starter-log/src/main/java/org/dromara/visor/framework/log/core/interceptor/PrettyLogPrinterInterceptor.java @@ -84,13 +84,11 @@ public class PrettyLogPrinterInterceptor extends AbstractLogPrinterInterceptor { if (request != null) { // remoteAddr requestLog.append("\tremoteAddr: ").append(IpUtils.getRemoteAddr(request)).append('\n'); - // header - Servlets.getHeaderMap(request).forEach((hk, hv) -> { - if (headerFilter.test(hk.toLowerCase())) { - requestLog.append('\t') - .append(hk).append(": ") - .append(hv).append('\n'); - } + // headers + this.getHeaderMap(request).forEach((hk, hv) -> { + requestLog.append('\t') + .append(hk).append(": ") + .append(hv).append('\n'); }); } Method method = invocation.getMethod(); diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-log/src/main/java/org/dromara/visor/framework/log/core/interceptor/RowLogPrinterInterceptor.java b/orion-visor-framework/orion-visor-spring-boot-starter-log/src/main/java/org/dromara/visor/framework/log/core/interceptor/RowLogPrinterInterceptor.java index 6e7696a0..20d47322 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-log/src/main/java/org/dromara/visor/framework/log/core/interceptor/RowLogPrinterInterceptor.java +++ b/orion-visor-framework/orion-visor-spring-boot-starter-log/src/main/java/org/dromara/visor/framework/log/core/interceptor/RowLogPrinterInterceptor.java @@ -85,14 +85,8 @@ public class RowLogPrinterInterceptor extends AbstractLogPrinterInterceptor impl if (request != null) { // remoteAddr fields.put(REMOTE_ADDR, IpUtils.getRemoteAddr(request)); - // header - Map headers = new LinkedHashMap<>(); - Servlets.getHeaderMap(request).forEach((hk, hv) -> { - if (headerFilter.test(hk.toLowerCase())) { - headers.put(hk, hv); - } - }); - fields.put(HEADERS, headers); + // headers + fields.put(HEADERS, this.getHeaderMap(request)); } Method method = invocation.getMethod(); // 方法签名 diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/java/org/dromara/visor/framework/mybatis/core/domain/BaseDO.java b/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/java/org/dromara/visor/framework/mybatis/core/domain/BaseDO.java index e15ccd3e..03feba02 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/java/org/dromara/visor/framework/mybatis/core/domain/BaseDO.java +++ b/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/java/org/dromara/visor/framework/mybatis/core/domain/BaseDO.java @@ -66,7 +66,7 @@ public class BaseDO implements Serializable { private String creator; @Schema(description = "修改人") - @TableField(fill = FieldFill.INSERT_UPDATE, jdbcType = JdbcType.VARCHAR) + @TableField(fill = FieldFill.INSERT_UPDATE, update = "IFNULL(#{et.updater}, updater)", jdbcType = JdbcType.VARCHAR) private String updater; /** @@ -78,4 +78,4 @@ public class BaseDO implements Serializable { @TableField(fill = FieldFill.INSERT, jdbcType = JdbcType.TINYINT) private Boolean deleted; -} \ No newline at end of file +} diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/java/org/dromara/visor/framework/mybatis/core/generator/CodeGenerators.java b/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/java/org/dromara/visor/framework/mybatis/core/generator/CodeGenerators.java index 1cd3bd16..1396b731 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/java/org/dromara/visor/framework/mybatis/core/generator/CodeGenerators.java +++ b/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/java/org/dromara/visor/framework/mybatis/core/generator/CodeGenerators.java @@ -54,12 +54,13 @@ public class CodeGenerators { // 作者 String author = Const.ORION_AUTHOR; // 模块 - String module = "infra"; + String module = "asset"; // 生成的表 Table[] tables = { // Template.create("dict_key", "字典配置项", "dict") // .enableProviderApi() // .disableUnitTest() + // .enableDeleteUseBatch() // .cache("dict:keys", "字典配置项") // .expire(8, TimeUnit.HOURS) // .vue("system", "dict-key") @@ -73,23 +74,28 @@ public class CodeGenerators { // .color("blue", "gray", "red", "green", "white") // .valueUseFields() // .build(), - // Template.create("exec_template_host", "执行模板主机", "exec") - // .enableProviderApi() - // .cache("sl", "22") - // .vue("exec", "exec-template-host") - // .build(), - Template.create("system_message", "系统消息", "message") + + Template.create("host_agent_log", "主机探针日志", "agent") .disableUnitTest() - .enableProviderApi() - .vue("system", "message") - .dict("messageType", "type", "messageType") - .comment("消息类型") - .fields("EXEC_FAILED", "UPLOAD_FAILED") - .labels("执行失败", "上传失败") - .extra("tagLabel", "执行失败", "上传失败") - .extra("tagVisible", true, true) - .extra("tagColor", "red", "red") + .vue("monitor", "monitor-host") + .disableRowSelection() + .enableCardView() + .enableDrawerForm() + + .dict("agentLogType", "type") + .comment("探针日志类型") + .fields("OFFLINE", "ONLINE", "INSTALL", "START", "STOP") + .labels("下线", "上线", "安装", "启动", "停止") .valueUseFields() + + .dict("agentLogStatus", "status") + .comment("探针日志状态") + .fields("WAIT", "RUNNING", "SUCCESS", "FAILED") + .labels("等待中", "运行中", "成功", "失败") + .color("green", "green", "arcoblue", "red") + .loading(true, true, false, false) + .valueUseFields() + .build(), }; // jdbc 配置 - 使用配置文件 @@ -98,7 +104,6 @@ public class CodeGenerators { String url = resolveConfigValue(yaml.getValue("spring.datasource.druid.url")); String username = resolveConfigValue(yaml.getValue("spring.datasource.druid.username")); String password = resolveConfigValue(yaml.getValue("spring.datasource.druid.password")); - // 执行 runGenerator(outputDir, author, tables, module, diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/java/org/dromara/visor/framework/mybatis/core/generator/core/CodeGenerator.java b/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/java/org/dromara/visor/framework/mybatis/core/generator/core/CodeGenerator.java index 462ada82..258e70c7 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/java/org/dromara/visor/framework/mybatis/core/generator/core/CodeGenerator.java +++ b/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/java/org/dromara/visor/framework/mybatis/core/generator/core/CodeGenerator.java @@ -194,6 +194,8 @@ public class CodeGenerator implements Executable { case Types.BIT: case Types.TINYINT: return DbColumnType.INTEGER; + case Types.DOUBLE: + return DbColumnType.DOUBLE; default: return typeRegistry.getColumnType(metaInfo); } @@ -327,9 +329,14 @@ public class CodeGenerator implements Executable { */ private InjectionConfig getInjectionConfig() { String[][] customFileDefineArr = new String[][]{ - // -------------------- 后端 - module -------------------- + // -------------------- 后端 -------------------- // http 文件 new String[]{"/templates/orion-server-module-controller.http.vm", "${type}Controller.http", "controller"}, + // operator log define 文件 + new String[]{"/templates/orion-server-module-operator-key-define.java.vm", "${type}OperatorType.java", "define.operator"}, + // convert 文件 + new String[]{"/templates/orion-server-module-convert.java.vm", "${type}Convert.java", "convert"}, + // -------------------- 后端 - 实体 -------------------- // vo 文件 new String[]{"/templates/orion-server-module-entity-vo.java.vm", "${type}VO.java", "entity.vo"}, // create request 文件 @@ -338,19 +345,19 @@ public class CodeGenerator implements Executable { new String[]{"/templates/orion-server-module-entity-request-update.java.vm", "${type}UpdateRequest.java", "entity.request.${bizPackage}"}, // query request 文件 new String[]{"/templates/orion-server-module-entity-request-query.java.vm", "${type}QueryRequest.java", "entity.request.${bizPackage}"}, - // convert 文件 - new String[]{"/templates/orion-server-module-convert.java.vm", "${type}Convert.java", "convert"}, + // -------------------- 后端 - 缓存 -------------------- // cache dto 文件 new String[]{"/templates/orion-server-module-cache-dto.java.vm", "${type}CacheDTO.java", "entity.dto"}, // cache key define 文件 new String[]{"/templates/orion-server-module-cache-key-define.java.vm", "${type}CacheKeyDefine.java", "define.cache"}, - // operator log define 文件 - new String[]{"/templates/orion-server-module-operator-key-define.java.vm", "${type}OperatorType.java", "define.operator"}, // -------------------- 后端 - provider -------------------- // api 文件 new String[]{"/templates/orion-server-provider-api.java.vm", "${type}Api.java", "api"}, // api impl 文件 new String[]{"/templates/orion-server-provider-api-impl.java.vm", "${type}ApiImpl.java", "api.impl"}, + // convert 文件 + new String[]{"/templates/orion-server-provider-convert.java.vm", "${type}ProviderConvert.java", "convert"}, + // -------------------- 后端 - provider 实体 -------------------- // dto 文件 new String[]{"/templates/orion-server-provider-entity-dto.java.vm", "${type}DTO.java", "entity.dto.${bizPackage}"}, // create dto 文件 @@ -359,8 +366,6 @@ public class CodeGenerator implements Executable { new String[]{"/templates/orion-server-provider-entity-dto-update.java.vm", "${type}UpdateDTO.java", "entity.dto.${bizPackage}"}, // query dto 文件 new String[]{"/templates/orion-server-provider-entity-dto-query.java.vm", "${type}QueryDTO.java", "entity.dto.${bizPackage}"}, - // convert 文件 - new String[]{"/templates/orion-server-provider-convert.java.vm", "${type}ProviderConvert.java", "convert"}, // -------------------- 后端 - test -------------------- // service unit test 文件 new String[]{"/templates/orion-server-test-service-impl-tests.java.vm", "${type}ServiceImplTests.java", "service.impl"}, @@ -375,22 +380,26 @@ public class CodeGenerator implements Executable { new String[]{"/templates/orion-vue-router.ts.vm", "${feature}.ts", "vue/router/routes/modules"}, // views index.ts 文件 new String[]{"/templates/orion-vue-views-index.vue.vm", "index.vue", "vue/views/${module}/${feature}"}, + // const.ts 文件 + new String[]{"/templates/orion-vue-views-types-const.ts.vm", "const.ts", "vue/views/${module}/${feature}/types"}, + // -------------------- 前端 - form -------------------- // form-modal.vue 文件 new String[]{"/templates/orion-vue-views-components-form-modal.vue.vm", "${feature}-form-modal.vue", "vue/views/${module}/${feature}/components"}, // form-drawer.vue 文件 new String[]{"/templates/orion-vue-views-components-form-drawer.vue.vm", "${feature}-form-drawer.vue", "vue/views/${module}/${feature}/components"}, - // table.vue 文件 - new String[]{"/templates/orion-vue-views-components-table.vue.vm", "${feature}-table.vue", "vue/views/${module}/${feature}/components"}, - // card-list.vue 文件 - new String[]{"/templates/orion-vue-views-components-card-list.vue.vm", "${feature}-card-list.vue", "vue/views/${module}/${feature}/components"}, - // const.ts 文件 - new String[]{"/templates/orion-vue-views-types-const.ts.vm", "const.ts", "vue/views/${module}/${feature}/types"}, // form.rules.ts 文件 new String[]{"/templates/orion-vue-views-types-form.rules.ts.vm", "form.rules.ts", "vue/views/${module}/${feature}/types"}, + // -------------------- 前端 - table -------------------- + // table.vue 文件 + new String[]{"/templates/orion-vue-views-components-table.vue.vm", "${feature}-table.vue", "vue/views/${module}/${feature}/components"}, // table.columns.ts 文件 new String[]{"/templates/orion-vue-views-types-table.columns.ts.vm", "table.columns.ts", "vue/views/${module}/${feature}/types"}, + // -------------------- 前端 - card -------------------- + // card-list.vue 文件 + new String[]{"/templates/orion-vue-views-components-card-list.vue.vm", "${feature}-card-list.vue", "vue/views/${module}/${feature}/components"}, // card.fields.ts 文件 new String[]{"/templates/orion-vue-views-types-card.fields.ts.vm", "card.fields.ts", "vue/views/${module}/${feature}/types"}, + // -------------------- sql -------------------- // menu.sql 文件 new String[]{"/templates/orion-sql-menu.sql.vm", "${tableName}-menu.sql", "sql"}, // dict.sql 文件 diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/java/org/dromara/visor/framework/mybatis/core/generator/core/DictParser.java b/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/java/org/dromara/visor/framework/mybatis/core/generator/core/DictParser.java index e66a8487..10f163e8 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/java/org/dromara/visor/framework/mybatis/core/generator/core/DictParser.java +++ b/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/java/org/dromara/visor/framework/mybatis/core/generator/core/DictParser.java @@ -81,7 +81,7 @@ public class DictParser { meta.setComment(Strings.def(tableField.getComment(), meta.getField())); } // 设置额外参数 schema - if (meta.getExtraValues().size() > 0) { + if (!meta.getExtraValues().isEmpty()) { List> extraSchema = meta.getExtraValues().get(0) .keySet() .stream() diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/java/org/dromara/visor/framework/mybatis/core/generator/template/DictTemplate.java b/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/java/org/dromara/visor/framework/mybatis/core/generator/template/DictTemplate.java index 376ca74b..4ddd1100 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/java/org/dromara/visor/framework/mybatis/core/generator/template/DictTemplate.java +++ b/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/java/org/dromara/visor/framework/mybatis/core/generator/template/DictTemplate.java @@ -165,6 +165,16 @@ public class DictTemplate extends Template { return this.extra(Const.COLOR, colors); } + /** + * 添加 loading + * + * @param loading loading + * @return this + */ + public DictTemplate loading(Object... loading) { + return this.extra(Const.LOADING, loading); + } + /** * 添加额外值 * diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-components-card-list.vue.vm b/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-components-card-list.vue.vm index d0be119d..81142dbf 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-components-card-list.vue.vm +++ b/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-components-card-list.vue.vm @@ -133,14 +133,14 @@ const cardColLayout = useCardColLayout(); const pagination = useCardPagination(); + const { loading, setLoading } = useLoading(); const queryOrder = useQueryOrder(TableName, ASC); const { cardFieldConfig, fieldsHook } = useCardFieldConfig(TableName, fieldConfig); - const { loading, setLoading } = useLoading(); #if($dictMap.entrySet().size() > 0) const { toOptions, getDictValue } = useDictStore(); #end - const list = ref<${vue.featureEntity}QueryResponse[]>([]); + const list = ref>([]); const formRef = ref(); const formModel = reactive<${vue.featureEntity}QueryRequest>({ searchValue: undefined, diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-components-table.vue.vm b/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-components-table.vue.vm index 909163ed..0b0b2bce 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-components-table.vue.vm +++ b/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-components-table.vue.vm @@ -172,9 +172,9 @@ const rowSelection = useRowSelection(); #end const pagination = useTablePagination(); + const { loading, setLoading } = useLoading(); const queryOrder = useQueryOrder(TableName, ASC); const { tableColumns, columnsHook } = useTableColumns(TableName, columns); - const { loading, setLoading } = useLoading(); #if($dictMap.entrySet().size() > 0) const { toOptions, getDictValue } = useDictStore(); #end diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-types-table.columns.ts.vm b/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-types-table.columns.ts.vm index a33cfb03..94c7f49e 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-types-table.columns.ts.vm +++ b/orion-visor-framework/orion-visor-spring-boot-starter-mybatis/src/main/resources/templates/orion-vue-views-types-table.columns.ts.vm @@ -19,6 +19,8 @@ const columns = [ minWidth: 238, ellipsis: true, tooltip: true, + #elseif(${field.propertyType} == 'Integer' || ${field.propertyType} == 'Long') + width: 120, #elseif(${field.propertyType} == 'Date') width: 180, render: ({ record }) => { diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-redis/src/main/java/org/dromara/visor/framework/redis/core/utils/RedisStrings.java b/orion-visor-framework/orion-visor-spring-boot-starter-redis/src/main/java/org/dromara/visor/framework/redis/core/utils/RedisStrings.java index 5cc8a1cd..65c18d9f 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-redis/src/main/java/org/dromara/visor/framework/redis/core/utils/RedisStrings.java +++ b/orion-visor-framework/orion-visor-spring-boot-starter-redis/src/main/java/org/dromara/visor/framework/redis/core/utils/RedisStrings.java @@ -32,6 +32,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.function.Consumer; +import java.util.function.Function; import java.util.stream.Collectors; /** @@ -49,6 +50,42 @@ public class RedisStrings extends RedisUtils { private RedisStrings() { } + /** + * 获取值 + * + * @param key key + * @return value + */ + public static String get(String key) { + return redisTemplate.opsForValue().get(key); + } + + /** + * 获取值 + * + * @param define define + * @return value + */ + public static String get(CacheKeyDefine define) { + return get(define.getKey()); + } + + /** + * 获取值 + * + * @param key key + * @param mapper mapper + * @param T + * @return value + */ + public static T get(String key, Function mapper) { + String value = redisTemplate.opsForValue().get(key); + if (value == null) { + return null; + } + return mapper.apply(value); + } + /** * 获取 json * @@ -56,11 +93,7 @@ public class RedisStrings extends RedisUtils { * @return JSONObject */ public static JSONObject getJson(String key) { - String value = redisTemplate.opsForValue().get(key); - if (value == null) { - return null; - } - return JSON.parseObject(value); + return get(key, JSON::parseObject); } /** @@ -95,57 +128,7 @@ public class RedisStrings extends RedisUtils { * @return T */ public static T getJson(String key, Class type) { - String value = redisTemplate.opsForValue().get(key); - if (value == null) { - return null; - } - return (T) JSON.parseObject(value, type); - } - - /** - * 获取 json 列表 - * - * @param keys keys - * @return cache - */ - public static List getJsonList(Collection keys) { - List values = redisTemplate.opsForValue().multiGet(keys); - if (values == null) { - return new ArrayList<>(); - } - return values.stream() - .map(JSON::parseObject) - .collect(Collectors.toList()); - } - - /** - * 获取 json 列表 - * - * @param keys keys - * @param define define - * @param T - * @return cache - */ - public static List getJsonList(Collection keys, CacheKeyDefine define) { - return getJsonList(keys, (Class) define.getType()); - } - - /** - * 获取 json 列表 - * - * @param keys keys - * @param type type - * @param T - * @return cache - */ - public static List getJsonList(Collection keys, Class type) { - List values = redisTemplate.opsForValue().multiGet(keys); - if (values == null) { - return new ArrayList<>(); - } - return values.stream() - .map(s -> JSON.parseObject(s, type)) - .collect(Collectors.toList()); + return get(key, s -> JSON.parseObject(s, type)); } /** @@ -155,11 +138,7 @@ public class RedisStrings extends RedisUtils { * @return JSONArray */ public static JSONArray getJsonArray(String key) { - String value = redisTemplate.opsForValue().get(key); - if (value == null) { - return null; - } - return JSON.parseArray(value); + return get(key, JSON::parseArray); } /** @@ -194,11 +173,69 @@ public class RedisStrings extends RedisUtils { * @return T */ public static List getJsonArray(String key, Class type) { - String value = redisTemplate.opsForValue().get(key); - if (value == null) { - return null; + return get(key, s -> JSON.parseArray(s, type)); + } + + /** + * 获取 json 列表 + * + * @param keys keys + * @return cache + */ + public static List getList(Collection keys) { + return getList(keys, Function.identity()); + } + + /** + * 获取 json 列表 + * + * @param keys keys + * @param mapper mapper + * @param T + * @return cache + */ + public static List getList(Collection keys, Function mapper) { + List values = redisTemplate.opsForValue().multiGet(keys); + if (values == null) { + return new ArrayList<>(); } - return JSON.parseArray(value, type); + return values.stream() + .map(mapper) + .collect(Collectors.toList()); + } + + /** + * 获取 json 列表 + * + * @param keys keys + * @return cache + */ + public static List getJsonList(Collection keys) { + return getList(keys, JSON::parseObject); + } + + /** + * 获取 json 列表 + * + * @param keys keys + * @param define define + * @param T + * @return cache + */ + public static List getJsonList(Collection keys, CacheKeyDefine define) { + return getList(keys, s -> JSON.parseObject(s, (Class) define.getType())); + } + + /** + * 获取 json 列表 + * + * @param keys keys + * @param type type + * @param T + * @return cache + */ + public static List getJsonList(Collection keys, Class type) { + return getList(keys, s -> JSON.parseObject(s, type)); } /** @@ -208,13 +245,7 @@ public class RedisStrings extends RedisUtils { * @return cache */ public static List getJsonArrayList(Collection keys) { - List values = redisTemplate.opsForValue().multiGet(keys); - if (values == null) { - return new ArrayList<>(); - } - return values.stream() - .map(JSON::parseArray) - .collect(Collectors.toList()); + return getList(keys, JSON::parseArray); } /** @@ -226,7 +257,7 @@ public class RedisStrings extends RedisUtils { * @return cache */ public static List> getJsonArrayList(Collection keys, CacheKeyDefine define) { - return getJsonArrayList(keys, (Class) define.getType()); + return getList(keys, s -> JSON.parseArray(s, (Class) define.getType())); } /** @@ -238,13 +269,7 @@ public class RedisStrings extends RedisUtils { * @return cache */ public static List> getJsonArrayList(Collection keys, Class type) { - List values = redisTemplate.opsForValue().multiGet(keys); - if (values == null) { - return new ArrayList<>(); - } - return values.stream() - .map(s -> JSON.parseArray(s, type)) - .collect(Collectors.toList()); + return getList(keys, s -> JSON.parseArray(s, type)); } /** diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-security/src/main/java/org/dromara/visor/framework/security/core/handler/AuthenticationEntryPointHandler.java b/orion-visor-framework/orion-visor-spring-boot-starter-security/src/main/java/org/dromara/visor/framework/security/core/handler/AuthenticationEntryPointHandler.java index 6e96dbf6..3f50e731 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-security/src/main/java/org/dromara/visor/framework/security/core/handler/AuthenticationEntryPointHandler.java +++ b/orion-visor-framework/orion-visor-spring-boot-starter-security/src/main/java/org/dromara/visor/framework/security/core/handler/AuthenticationEntryPointHandler.java @@ -46,6 +46,7 @@ public class AuthenticationEntryPointHandler implements AuthenticationEntryPoint @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException { + log.warn("AuthenticationEntryPoint-commence-401 {}", request.getRequestURI()); log.debug("AuthenticationEntryPoint-commence-unauthorized {}", request.getRequestURI(), e); Servlets.writeHttpWrapper(response, ErrorCode.UNAUTHORIZED.getWrapper()); } diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-web/src/main/java/org/dromara/visor/framework/web/configuration/OrionWebAutoConfiguration.java b/orion-visor-framework/orion-visor-spring-boot-starter-web/src/main/java/org/dromara/visor/framework/web/configuration/OrionWebAutoConfiguration.java index 46b53817..3c90dcbe 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-web/src/main/java/org/dromara/visor/framework/web/configuration/OrionWebAutoConfiguration.java +++ b/orion-visor-framework/orion-visor-spring-boot-starter-web/src/main/java/org/dromara/visor/framework/web/configuration/OrionWebAutoConfiguration.java @@ -28,7 +28,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.dromara.visor.common.constant.AutoConfigureOrderConst; import org.dromara.visor.common.constant.FilterOrderConst; import org.dromara.visor.common.web.WebFilterCreator; +import org.dromara.visor.framework.web.configuration.config.ExposeApiConfig; import org.dromara.visor.framework.web.core.aspect.DemoDisableApiAspect; +import org.dromara.visor.framework.web.core.aspect.ExposeApiAspect; import org.dromara.visor.framework.web.core.filter.TraceIdFilter; import org.dromara.visor.framework.web.core.handler.GlobalExceptionHandler; import org.dromara.visor.framework.web.core.handler.WrapperResultHandler; @@ -37,6 +39,7 @@ import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureOrder; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.http.HttpMessageConverters; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.http.MediaType; @@ -50,6 +53,7 @@ import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.nio.charset.StandardCharsets; @@ -67,8 +71,12 @@ import java.util.List; */ @AutoConfiguration @AutoConfigureOrder(AutoConfigureOrderConst.FRAMEWORK_WEB) +@EnableConfigurationProperties(ExposeApiConfig.class) public class OrionWebAutoConfiguration implements WebMvcConfigurer { + @Value("${orion.prefix}") + private String orionPrefix; + @Value("${orion.api.prefix}") private String orionApiPrefix; @@ -77,7 +85,14 @@ public class OrionWebAutoConfiguration implements WebMvcConfigurer { // 公共 api 前缀 AntPathMatcher antPathMatcher = new AntPathMatcher("."); configurer.addPathPrefix(orionApiPrefix, clazz -> clazz.isAnnotationPresent(RestController.class) - && antPathMatcher.match("org.dromara.visor.**.controller.**", clazz.getPackage().getName())); + && antPathMatcher.match("org.dromara.visor.module.**.controller.**", clazz.getPackage().getName())); + } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + // 公共模板前缀 + registry.addResourceHandler(orionPrefix + "/template/**") + .addResourceLocations("classpath:/public/template/"); } /** @@ -171,4 +186,13 @@ public class OrionWebAutoConfiguration implements WebMvcConfigurer { return new DemoDisableApiAspect(); } + /** + * @param config config + * @return 对外服务 api 切面 + */ + @Bean + public ExposeApiAspect exposeApiAspect(ExposeApiConfig config) { + return new ExposeApiAspect(config); + } + } diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-web/src/main/java/org/dromara/visor/framework/web/configuration/config/ExposeApiConfig.java b/orion-visor-framework/orion-visor-spring-boot-starter-web/src/main/java/org/dromara/visor/framework/web/configuration/config/ExposeApiConfig.java new file mode 100644 index 00000000..49e39feb --- /dev/null +++ b/orion-visor-framework/orion-visor-spring-boot-starter-web/src/main/java/org/dromara/visor/framework/web/configuration/config/ExposeApiConfig.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.framework.web.configuration.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 对外服务配置属性 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/22 19:57 + */ +@Data +@ConfigurationProperties("orion.api.expose") +public class ExposeApiConfig { + + /** + * 对外服务请求头 + */ + private String header; + + /** + * 对外服务请求值 + */ + private String token; + +} diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-web/src/main/java/org/dromara/visor/framework/web/core/annotation/ExposeApi.java b/orion-visor-framework/orion-visor-spring-boot-starter-web/src/main/java/org/dromara/visor/framework/web/core/annotation/ExposeApi.java new file mode 100644 index 00000000..a51ea6c3 --- /dev/null +++ b/orion-visor-framework/orion-visor-spring-boot-starter-web/src/main/java/org/dromara/visor/framework/web/core/annotation/ExposeApi.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.framework.web.core.annotation; + +import javax.annotation.security.PermitAll; +import java.lang.annotation.*; + +/** + * 对外服务 api 注解 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/22 9:59 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@PermitAll +public @interface ExposeApi { + + /** + * @return 请求来源 + */ + String source() default ""; + +} diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-web/src/main/java/org/dromara/visor/framework/web/core/aspect/DemoDisableApiAspect.java b/orion-visor-framework/orion-visor-spring-boot-starter-web/src/main/java/org/dromara/visor/framework/web/core/aspect/DemoDisableApiAspect.java index 2dc45e96..54bf5724 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-web/src/main/java/org/dromara/visor/framework/web/core/aspect/DemoDisableApiAspect.java +++ b/orion-visor-framework/orion-visor-spring-boot-starter-web/src/main/java/org/dromara/visor/framework/web/core/aspect/DemoDisableApiAspect.java @@ -22,7 +22,6 @@ */ package org.dromara.visor.framework.web.core.aspect; -import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; @@ -39,7 +38,6 @@ import org.springframework.core.annotation.Order; * @since 2024/5/21 16:52 */ @Aspect -@Slf4j @Order(BeanOrderConst.DEMO_DISABLE_API_ASPECT) public class DemoDisableApiAspect { diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-web/src/main/java/org/dromara/visor/framework/web/core/aspect/ExposeApiAspect.java b/orion-visor-framework/orion-visor-spring-boot-starter-web/src/main/java/org/dromara/visor/framework/web/core/aspect/ExposeApiAspect.java new file mode 100644 index 00000000..d413438b --- /dev/null +++ b/orion-visor-framework/orion-visor-spring-boot-starter-web/src/main/java/org/dromara/visor/framework/web/core/aspect/ExposeApiAspect.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.framework.web.core.aspect; + +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.annotation.Pointcut; +import org.dromara.visor.common.constant.BeanOrderConst; +import org.dromara.visor.common.constant.ErrorCode; +import org.dromara.visor.framework.web.configuration.config.ExposeApiConfig; +import org.dromara.visor.framework.web.core.annotation.ExposeApi; +import org.springframework.core.annotation.Order; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; +import java.util.Optional; + +/** + * 对外服务 api 切面 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/22 16:52 + */ +@Slf4j +@Aspect +@Order(BeanOrderConst.EXPOSE_API_ASPECT) +public class ExposeApiAspect { + + private final ExposeApiConfig config; + + public ExposeApiAspect(ExposeApiConfig config) { + this.config = config; + } + + @Pointcut("@annotation(e)") + public void exposeApi(ExposeApi e) { + } + + @Before(value = "exposeApi(e)", argNames = "e") + public void beforeExposeApi(ExposeApi e) { + // 获取请求 + HttpServletRequest request = Optional.ofNullable(RequestContextHolder.getRequestAttributes()) + .map(s -> (ServletRequestAttributes) s) + .map(ServletRequestAttributes::getRequest) + .orElse(null); + if (request == null) { + throw ErrorCode.EXPOSE_UNAUTHORIZED.exception(); + } + // 验证对外服务参数 + if (!config.getToken().equals(request.getHeader(config.getHeader()))) { + log.warn("expose api unauthorized, url: {}", request.getRequestURI()); + throw ErrorCode.EXPOSE_UNAUTHORIZED.exception(); + } + } + +} diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-web/src/main/java/org/dromara/visor/framework/web/core/handler/GlobalExceptionHandler.java b/orion-visor-framework/orion-visor-spring-boot-starter-web/src/main/java/org/dromara/visor/framework/web/core/handler/GlobalExceptionHandler.java index 0a47cd58..cbfaf0b2 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-web/src/main/java/org/dromara/visor/framework/web/core/handler/GlobalExceptionHandler.java +++ b/orion-visor-framework/orion-visor-spring-boot-starter-web/src/main/java/org/dromara/visor/framework/web/core/handler/GlobalExceptionHandler.java @@ -149,6 +149,7 @@ public class GlobalExceptionHandler { } @ExceptionHandler(value = { + LockException.class, TimeoutException.class, java.util.concurrent.TimeoutException.class }) diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-web/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/orion-visor-framework/orion-visor-spring-boot-starter-web/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 996c79b1..ec187a88 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-web/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/orion-visor-framework/orion-visor-spring-boot-starter-web/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -1,4 +1,11 @@ { + "groups": [ + { + "name": "orion.api.expose", + "type": "org.dromara.visor.framework.web.configuration.config.ExposeApiConfig", + "sourceType": "org.dromara.visor.framework.web.configuration.config.ExposeApiConfig" + } + ], "properties": [ { "name": "orion.version", @@ -25,6 +32,16 @@ "name": "orion.api.cors", "type": "java.lang.Boolean", "description": "是否开启 cors 过滤器." + }, + { + "name": "orion.api.expose.header", + "type": "java.lang.String", + "description": "对外服务请求头." + }, + { + "name": "orion.api.expose.token", + "type": "java.lang.String", + "description": "对外服务请求值." } ] } \ No newline at end of file diff --git a/orion-visor-framework/pom.xml b/orion-visor-framework/pom.xml index aab6e488..7cec0d7f 100644 --- a/orion-visor-framework/pom.xml +++ b/orion-visor-framework/pom.xml @@ -21,17 +21,18 @@ orion-visor-spring-boot-starter-swagger orion-visor-spring-boot-starter-datasource orion-visor-spring-boot-starter-mybatis + orion-visor-spring-boot-starter-cipher orion-visor-spring-boot-starter-config orion-visor-spring-boot-starter-job orion-visor-spring-boot-starter-websocket orion-visor-spring-boot-starter-redis orion-visor-spring-boot-starter-desensitize - orion-visor-spring-boot-starter-cipher orion-visor-spring-boot-starter-log orion-visor-spring-boot-starter-storage orion-visor-spring-boot-starter-security orion-visor-spring-boot-starter-monitor orion-visor-spring-boot-starter-test + orion-visor-spring-boot-starter-influxdb orion-visor-spring-boot-starter-biz-operator-log diff --git a/orion-visor-launch/pom.xml b/orion-visor-launch/pom.xml index a27591db..177d1b91 100644 --- a/orion-visor-launch/pom.xml +++ b/orion-visor-launch/pom.xml @@ -63,6 +63,11 @@ orion-visor-module-terminal-service ${revision} + + org.dromara.visor + orion-visor-module-monitor-service + ${revision} + @@ -125,6 +130,10 @@ org.dromara.visor orion-visor-spring-boot-starter-monitor + + org.dromara.visor + orion-visor-spring-boot-starter-influxdb + org.dromara.visor orion-visor-spring-boot-starter-biz-operator-log diff --git a/orion-visor-launch/src/main/java/org/dromara/visor/launch/LaunchApplication.java b/orion-visor-launch/src/main/java/org/dromara/visor/launch/LaunchApplication.java index fae60a26..a69d47ba 100644 --- a/orion-visor-launch/src/main/java/org/dromara/visor/launch/LaunchApplication.java +++ b/orion-visor-launch/src/main/java/org/dromara/visor/launch/LaunchApplication.java @@ -32,7 +32,9 @@ import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.core.type.AnnotationMetadata; +import org.springframework.stereotype.Component; +import java.util.Objects; import java.util.Optional; /** @@ -60,7 +62,7 @@ public class LaunchApplication { */ public static class CustomBeanNameGenerator implements BeanNameGenerator { - private static final String BEAN_ANNOTATION_CLASS_NAME = "org.springframework.stereotype.Component"; + private static final String BEAN_ANNOTATION_CLASS_NAME = Component.class.getName(); @Override public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) { @@ -68,12 +70,12 @@ public class LaunchApplication { if (definition instanceof AnnotatedBeanDefinition) { AnnotationMetadata metadata = ((AnnotatedBeanDefinition) definition).getMetadata(); // 处理自定义 bean 名称 - return Optional.of(metadata) + return Objects.requireNonNull(Optional.of(metadata) .map(s -> s.getAnnotationAttributes(BEAN_ANNOTATION_CLASS_NAME)) .map(s -> s.get(Const.VALUE)) .map(Object::toString) .filter(Strings::isNotBlank) - .orElseGet(definition::getBeanClassName); + .orElseGet(definition::getBeanClassName)); } else { // 非注解形式默认使用默认名称 return BeanDefinitionReaderUtils.generateBeanName(definition, registry); diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/pom.xml b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/pom.xml index b9ddc97a..f8b19f9c 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/pom.xml +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/pom.xml @@ -20,6 +20,11 @@ org.dromara.visor orion-visor-common + + org.dromara.visor + orion-visor-module-infra-provider + ${revision} + \ No newline at end of file diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/api/HostAgentApi.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/api/HostAgentApi.java new file mode 100644 index 00000000..49c8cde6 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/api/HostAgentApi.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.asset.api; + +import org.dromara.visor.module.asset.entity.dto.host.HostAgentLogDTO; + +import java.util.List; +import java.util.Map; + +/** + * 主机探针对外服务 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/9/1 21:14 + */ +public interface HostAgentApi { + + /** + * 查询主机探针安装日志 + * + * @param hostIdList hostIdList + * @return rows + */ + List selectAgentInstallLog(List hostIdList); + + /** + * 获取缓存名称 + * + * @param agentKeyList agentKeyList + * @return nameMap + */ + Map getCacheNameByAgentKey(List agentKeyList); + + /** + * 获取探针版本 + * + * @return version + */ + String getAgentVersion(); + +} diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/api/HostApi.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/api/HostApi.java index 33a4dc1e..cc745ff4 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/api/HostApi.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/api/HostApi.java @@ -22,7 +22,9 @@ */ package org.dromara.visor.module.asset.api; +import cn.orionsec.kit.lang.define.wrapper.DataGrid; import org.dromara.visor.module.asset.entity.dto.host.HostDTO; +import org.dromara.visor.module.asset.entity.dto.host.HostQueryDTO; import java.util.List; @@ -51,4 +53,37 @@ public interface HostApi { */ List selectByIdList(List idList); + /** + * 通过 id 查询 agentKey + * + * @param id id + * @return agentKey + */ + String selectAgentKeyById(Long id); + + /** + * 分页查询主机信息 + * + * @param query query + * @return rows + */ + DataGrid getHostPage(HostQueryDTO query); + + + /** + * 通过 agentKey 查询 + * + * @param agentKey agentKey + * @return row + */ + HostDTO selectByAgentKey(String agentKey); + + /** + * 通过 agentKey 查询 + * + * @param agentKeys agentKeys + * @return row + */ + List selectByAgentKeys(List agentKeys); + } diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/entity/dto/host/HostAgentLogDTO.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/entity/dto/host/HostAgentLogDTO.java new file mode 100644 index 00000000..daeff621 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/entity/dto/host/HostAgentLogDTO.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.asset.entity.dto.host; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.Date; + +/** + * 主机探针安装日志 业务响应对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/9/1 17:34 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(name = "HostAgentInstallLogDTO", description = "主机探针安装日志 业务响应对象") +public class HostAgentLogDTO implements Serializable { + + @Schema(description = "id") + private Long id; + + @Schema(description = "hostId") + private Long hostId; + + @Schema(description = "类型") + private String type; + + @Schema(description = "状态") + private String status; + + @Schema(description = "消息") + private String message; + + @Schema(description = "创建时间") + private Date createTime; + +} diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/entity/dto/host/HostBaseDTO.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/entity/dto/host/HostBaseDTO.java index e9fba21f..6b0a3bf8 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/entity/dto/host/HostBaseDTO.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/entity/dto/host/HostBaseDTO.java @@ -67,7 +67,7 @@ public class HostBaseDTO implements Serializable { @Schema(description = "主机地址") private String address; - @Schema(description = "主机端口") - private Integer port; + @Schema(description = "agentKey") + private String agentKey; } diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/entity/dto/host/HostDTO.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/entity/dto/host/HostDTO.java index eaaf3346..06b6da2d 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/entity/dto/host/HostDTO.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/entity/dto/host/HostDTO.java @@ -27,9 +27,11 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import org.dromara.visor.module.infra.entity.dto.tag.TagDTO; import java.io.Serializable; import java.util.Date; +import java.util.List; import java.util.Set; /** @@ -69,12 +71,24 @@ public class HostDTO implements Serializable { @Schema(description = "主机地址") private String address; - @Schema(description = "主机端口") - private Integer port; - @Schema(description = "主机状态") private String status; + @Schema(description = "agentKey") + private String agentKey; + + @Schema(description = "探针版本") + private String agentVersion; + + @Schema(description = "探针安装状态") + private Integer agentInstallStatus; + + @Schema(description = "探针在线状态") + private Integer agentOnlineStatus; + + @Schema(description = "探针切换在线状态时间") + private Date agentOnlineChangeTime; + @Schema(description = "描述") private String description; @@ -93,6 +107,9 @@ public class HostDTO implements Serializable { @Schema(description = "是否收藏") private Boolean favorite; + @Schema(description = "tags") + private List tags; + @Schema(description = "分组 id") private Set groupIdList; @@ -114,7 +131,7 @@ public class HostDTO implements Serializable { .name(this.name) .code(this.code) .address(this.address) - .port(this.port) + .agentKey(this.agentKey) .build(); } diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/entity/dto/host/HostQueryDTO.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/entity/dto/host/HostQueryDTO.java new file mode 100644 index 00000000..126c0aa4 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/entity/dto/host/HostQueryDTO.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.asset.entity.dto.host; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.dromara.visor.common.entity.BaseQueryRequest; + +import javax.validation.constraints.Size; +import java.util.List; + +/** + * 主机 查询请求对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023-9-11 14:16 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(name = "HostQueryDTO", description = "主机 查询请求对象") +public class HostQueryDTO extends BaseQueryRequest { + + @Schema(description = "搜索") + private String searchValue; + + @Schema(description = "id") + private Long id; + + @Schema(description = "id") + private List idList; + + @Size(max = 8) + @Schema(description = "主机类型") + private String type; + + @Size(max = 64) + @Schema(description = "主机名称") + private String name; + + @Size(max = 64) + @Schema(description = "主机编码") + private String code; + + @Size(max = 128) + @Schema(description = "主机地址") + private String address; + + @Size(max = 12) + @Schema(description = "系统类型") + private String osType; + + @Size(max = 12) + @Schema(description = "系统架构") + private String archType; + + @Size(max = 8) + @Schema(description = "主机状态") + private String status; + + @Size(max = 255) + @Schema(description = "描述") + private String description; + + @Schema(description = "agentKey") + private String agentKey; + + @Schema(description = "探针安装状态") + private Integer agentInstallStatus; + + @Schema(description = "探针在线状态") + private Integer agentOnlineStatus; + + @Schema(description = "通过探针排序") + private Boolean orderByAgent; + + @Schema(description = "tag") + private List tags; + + @Schema(description = "是否查询分组信息") + private Boolean queryGroup; + + @Schema(description = "是否查询 tag 信息") + private Boolean queryTag; + + @Schema(description = "是否查询规格信息") + private Boolean querySpec; + +} diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/enums/AgentInstallStatusEnum.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/enums/AgentInstallStatusEnum.java new file mode 100644 index 00000000..69d07dfa --- /dev/null +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/enums/AgentInstallStatusEnum.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.asset.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 探针安装状态 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/13 23:31 + */ +@Getter +@AllArgsConstructor +public enum AgentInstallStatusEnum { + + /** + * 未安装 + */ + NOT_INSTALL(0), + + /** + * 已安装 + */ + INSTALLED(1), + + ; + + private final Integer status; + + public static AgentInstallStatusEnum of(Integer status) { + if (status == null) { + return NOT_INSTALL; + } + for (AgentInstallStatusEnum value : AgentInstallStatusEnum.values()) { + if (value.status.equals(status)) { + return value; + } + } + return NOT_INSTALL; + } + +} diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/enums/AgentOnlineStatusEnum.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/enums/AgentOnlineStatusEnum.java new file mode 100644 index 00000000..cd94e89c --- /dev/null +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/enums/AgentOnlineStatusEnum.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.asset.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 探针在线状态 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/13 23:31 + */ +@Getter +@AllArgsConstructor +public enum AgentOnlineStatusEnum { + + /** + * 离线 + */ + OFFLINE(0), + + /** + * 在线 + */ + ONLINE(1), + + ; + + private final Integer value; + + public static AgentOnlineStatusEnum of(Integer value) { + if (value == null) { + return OFFLINE; + } + for (AgentOnlineStatusEnum e : AgentOnlineStatusEnum.values()) { + if (e.value.equals(value)) { + return e; + } + } + return OFFLINE; + } + +} diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/enums/HostOsTypeEnum.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/enums/HostOsTypeEnum.java index ca4fc5a1..50051da4 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/enums/HostOsTypeEnum.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/enums/HostOsTypeEnum.java @@ -39,22 +39,24 @@ public enum HostOsTypeEnum { /** * linux */ - LINUX(".sh"), + LINUX(".sh", ""), /** * windows */ - WINDOWS(".cmd"), + WINDOWS(".cmd", ".exe"), /** * darwin */ - DARWIN(".sh"), + DARWIN(".sh", ""), ; private final String scriptSuffix; + private final String binarySuffix; + public boolean is(String type) { if (type == null) { return false; diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/enums/HostTypeEnum.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/enums/HostTypeEnum.java index e4bf6760..09765a0e 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/enums/HostTypeEnum.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-provider/src/main/java/org/dromara/visor/module/asset/enums/HostTypeEnum.java @@ -76,6 +76,12 @@ public enum HostTypeEnum { return null; } + /** + * 获取类型 + * + * @param types types + * @return types + */ public static List split(String types) { if (types == null) { return new ArrayList<>(); @@ -87,6 +93,16 @@ public enum HostTypeEnum { .collect(Collectors.toList()); } + /** + * 是否包含此类型 + * + * @param types types + * @return contains + */ + public boolean contains(String types) { + return split(types).contains(this.name()); + } + @SuppressWarnings("unchecked") public T parse(String config) { return (T) JSON.parseObject(config, this.clazz); diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/pom.xml b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/pom.xml index 65bda5cb..c0e1beaf 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/pom.xml +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/pom.xml @@ -37,6 +37,11 @@ orion-visor-module-exec-provider ${revision} + + org.dromara.visor + orion-visor-module-monitor-provider + ${revision} + diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/api/impl/HostAgentApiImpl.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/api/impl/HostAgentApiImpl.java new file mode 100644 index 00000000..84a13b0c --- /dev/null +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/api/impl/HostAgentApiImpl.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.asset.api.impl; + +import cn.orionsec.kit.lang.define.cache.TimedCache; +import cn.orionsec.kit.lang.define.cache.TimedCacheBuilder; +import cn.orionsec.kit.lang.utils.Strings; +import cn.orionsec.kit.lang.utils.collect.Lists; +import org.dromara.visor.common.constant.Const; +import org.dromara.visor.framework.redis.core.utils.RedisStrings; +import org.dromara.visor.module.asset.api.HostAgentApi; +import org.dromara.visor.module.asset.convert.HostAgentLogProviderConvert; +import org.dromara.visor.module.asset.dao.HostAgentLogDAO; +import org.dromara.visor.module.asset.dao.HostDAO; +import org.dromara.visor.module.asset.define.cache.HostCacheKeyDefine; +import org.dromara.visor.module.asset.entity.domain.HostDO; +import org.dromara.visor.module.asset.entity.dto.host.HostAgentLogDTO; +import org.dromara.visor.module.asset.service.HostAgentService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 主机探针对外服务 实现 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/9/1 21:18 + */ +@Service +public class HostAgentApiImpl implements HostAgentApi { + + private static final TimedCache AGENT_NAME_CACHE = TimedCacheBuilder.create() + .expiredDelay(Const.MS_S_60 * 30) + .checkDelay(Const.MS_S_60) + .build(); + + @Resource + private HostDAO hostDAO; + + @Resource + private HostAgentLogDAO hostAgentLogDAO; + + @Resource + private HostAgentService hostAgentService; + + @Override + public List selectAgentInstallLog(List hostIdList) { + if (Lists.isEmpty(hostIdList)) { + return Lists.empty(); + } + // 查询缓存 + List keys = hostIdList.stream() + .map(HostCacheKeyDefine.HOST_INSTALL_LOG::format) + .collect(Collectors.toList()); + List logIdList = RedisStrings.getList(keys) + .stream() + .filter(Strings::isNotBlank) + .map(Long::parseLong) + .collect(Collectors.toList()); + // 查询数据库 + return hostAgentLogDAO.selectBatchIds(logIdList) + .stream() + .map(HostAgentLogProviderConvert.MAPPER::to) + .collect(Collectors.toList()); + } + + @Override + public Map getCacheNameByAgentKey(List agentKeyList) { + Map result = new HashMap<>(); + List queryList = new ArrayList<>(); + // 查询缓存 + for (String agentKey : agentKeyList) { + String name = AGENT_NAME_CACHE.get(agentKey); + if (name != null) { + result.put(agentKey, name); + } else { + queryList.add(agentKey); + } + } + // 查询数据库 + if (!queryList.isEmpty()) { + // 查询数据 + hostDAO.of() + .createWrapper() + .select(HostDO::getName, HostDO::getAgentKey) + .in(HostDO::getAgentKey, queryList) + .then() + .list() + .forEach(s -> result.put(s.getAgentKey(), s.getName())); + for (String agentKey : queryList) { + result.putIfAbsent(agentKey, Const.EMPTY); + AGENT_NAME_CACHE.put(agentKey, result.get(agentKey)); + } + } + return result; + } + + @Override + public String getAgentVersion() { + return hostAgentService.getAgentVersion(); + } + +} diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/api/impl/HostApiImpl.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/api/impl/HostApiImpl.java index 419a98f6..b18b1407 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/api/impl/HostApiImpl.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/api/impl/HostApiImpl.java @@ -22,12 +22,17 @@ */ package org.dromara.visor.module.asset.api.impl; +import cn.orionsec.kit.lang.define.wrapper.DataGrid; import cn.orionsec.kit.lang.utils.collect.Lists; import lombok.extern.slf4j.Slf4j; import org.dromara.visor.module.asset.api.HostApi; import org.dromara.visor.module.asset.convert.HostProviderConvert; import org.dromara.visor.module.asset.dao.HostDAO; +import org.dromara.visor.module.asset.entity.domain.HostDO; import org.dromara.visor.module.asset.entity.dto.host.HostDTO; +import org.dromara.visor.module.asset.entity.dto.host.HostQueryDTO; +import org.dromara.visor.module.asset.entity.request.host.HostQueryRequest; +import org.dromara.visor.module.asset.service.HostService; import org.springframework.stereotype.Service; import javax.annotation.Resource; @@ -48,6 +53,9 @@ public class HostApiImpl implements HostApi { @Resource private HostDAO hostDAO; + @Resource + private HostService hostService; + @Override public HostDTO selectById(Long id) { return HostProviderConvert.MAPPER.to(hostDAO.selectById(id)); @@ -64,4 +72,40 @@ public class HostApiImpl implements HostApi { .collect(Collectors.toList()); } + @Override + public String selectAgentKeyById(Long id) { + return hostDAO.of() + .createWrapper() + .select(HostDO::getAgentKey) + .eq(HostDO::getId, id) + .then() + .getOne(HostDO::getAgentKey); + } + + @Override + public DataGrid getHostPage(HostQueryDTO query) { + // 转换 + HostQueryRequest queryRequest = HostProviderConvert.MAPPER.to(query); + // 查询 + return hostService.getHostPage(queryRequest).map(HostProviderConvert.MAPPER::to); + } + + @Override + public HostDTO selectByAgentKey(String agentKey) { + return hostDAO.of() + .createWrapper() + .eq(HostDO::getAgentKey, agentKey) + .then() + .getOne(HostProviderConvert.MAPPER::to); + } + + @Override + public List selectByAgentKeys(List agentKeys) { + return hostDAO.of() + .createWrapper() + .in(HostDO::getAgentKey, agentKeys) + .then() + .list(HostProviderConvert.MAPPER::to); + } + } diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/controller/HostAgentController.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/controller/HostAgentController.java new file mode 100644 index 00000000..e409bba3 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/controller/HostAgentController.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.asset.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.dromara.visor.framework.biz.operator.log.core.annotation.OperatorLog; +import org.dromara.visor.framework.log.core.annotation.IgnoreLog; +import org.dromara.visor.framework.log.core.enums.IgnoreLogMode; +import org.dromara.visor.framework.web.core.annotation.DemoDisableApi; +import org.dromara.visor.framework.web.core.annotation.RestWrapper; +import org.dromara.visor.module.asset.define.operator.HostOperatorType; +import org.dromara.visor.module.asset.entity.request.host.HostAgentInstallRequest; +import org.dromara.visor.module.asset.entity.request.host.HostAgentInstallStatusUpdateRequest; +import org.dromara.visor.module.asset.entity.vo.HostAgentLogVO; +import org.dromara.visor.module.asset.entity.vo.HostAgentStatusVO; +import org.dromara.visor.module.asset.service.HostAgentLogService; +import org.dromara.visor.module.asset.service.HostAgentService; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 主机探针端点 api + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/22 14:33 + */ +@Tag(name = "asset - 主机探针") +@Slf4j +@Validated +@RestWrapper +@RestController +@RequestMapping("/asset/host-agent") +public class HostAgentController { + + @Resource + private HostAgentService hostAgentService; + + @Resource + private HostAgentLogService hostAgentLogService; + + @IgnoreLog(IgnoreLogMode.ALL) + @GetMapping("/status") + @Operation(summary = "查询探针状态") + @Parameter(name = "idList", description = "idList", required = true) + @PreAuthorize("@ss.hasPermission('asset:host:query')") + public List getAgentStatus(@RequestParam("idList") List idList) { + return hostAgentService.getAgentStatus(idList); + } + + @IgnoreLog(IgnoreLogMode.ALL) + @GetMapping("/install-status") + @Operation(summary = "查询探针安装状态") + @Parameter(name = "idList", description = "idList", required = true) + @PreAuthorize("@ss.hasPermission('asset:host:query')") + public List getAgentInstallLogStatus(@RequestParam("idList") List idList) { + return hostAgentLogService.getAgentInstallLogStatus(idList); + } + + @DemoDisableApi + @OperatorLog(HostOperatorType.INSTALL_AGENT) + @PostMapping("/install") + @Operation(summary = "安装主机探针") + @PreAuthorize("@ss.hasPermission('asset:host:install-agent')") + public Boolean installAgent(@Validated @RequestBody HostAgentInstallRequest request) { + hostAgentService.installAgent(request); + return true; + } + + @DemoDisableApi + @OperatorLog(HostOperatorType.UPDATE_AGENT_INSTALL_STATUS) + @PutMapping("/update-install-status") + @Operation(summary = "修改探针安装状态") + @PreAuthorize("@ss.hasPermission('asset:host:install-agent')") + public Boolean updateAgentInstallStatus(@Validated @RequestBody HostAgentInstallStatusUpdateRequest request) { + hostAgentLogService.updateStatus(request.getId(), request.getStatus(), request.getMessage()); + return true; + } + + @DemoDisableApi + @OperatorLog(HostOperatorType.UPLOAD_AGENT_RELEASE) + @PostMapping("/upload-agent-release") + @Operation(summary = "上传探针发布包") + @PreAuthorize("@ss.hasPermission('asset:host:install-agent')") + public String uploadAgentRelease(@RequestParam("file") MultipartFile file) { + // 上传 + hostAgentService.uploadAgentRelease(file); + // 获取最新版本 + return hostAgentService.getAgentVersion(); + } + +} diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/controller/HostAgentEndpointController.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/controller/HostAgentEndpointController.java new file mode 100644 index 00000000..14beda8d --- /dev/null +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/controller/HostAgentEndpointController.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.asset.controller; + +import com.alibaba.fastjson.JSONObject; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.dromara.visor.common.constant.CustomHeaderConst; +import org.dromara.visor.common.constant.ExtraFieldConst; +import org.dromara.visor.framework.log.core.annotation.IgnoreLog; +import org.dromara.visor.framework.log.core.enums.IgnoreLogMode; +import org.dromara.visor.framework.web.core.annotation.ExposeApi; +import org.dromara.visor.framework.web.core.annotation.RestWrapper; +import org.dromara.visor.module.asset.entity.vo.HostOnlineAgentConfigVO; +import org.dromara.visor.module.asset.service.HostAgentEndpointService; +import org.dromara.visor.module.asset.service.HostExtraService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; + +/** + * 主机探针端点 api + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/22 14:33 + */ +@Tag(name = "asset - 主机探针端点") +@Slf4j +@Validated +@RestWrapper +@RestController +@RequestMapping("/host/agent-endpoint") +public class HostAgentEndpointController { + + @Resource + private HostExtraService hostExtraService; + + @Resource + private HostAgentEndpointService hostAgentEndpointService; + + @ExposeApi + @GetMapping("/set-online") + @Operation(summary = "设置探针已上线") + public HostOnlineAgentConfigVO setAgentOnline(@RequestHeader(CustomHeaderConst.AGENT_KEY_HEADER) String agentKey, + @RequestHeader(CustomHeaderConst.AGENT_VERSION_HEADER) String version) { + return hostAgentEndpointService.setAgentOnline(agentKey, version); + } + + @ExposeApi + @GetMapping("/set-offline") + @Operation(summary = "设置探针已下线") + public Boolean setAgentOffline(@RequestHeader(CustomHeaderConst.AGENT_KEY_HEADER) String agentKey) { + hostAgentEndpointService.setAgentOffline(agentKey); + return true; + } + + @ExposeApi + @IgnoreLog(IgnoreLogMode.RET) + @GetMapping("/heartbeat") + @Operation(summary = "设置探针心跳") + public Boolean setAgentHeartbeat(@RequestHeader(CustomHeaderConst.AGENT_KEY_HEADER) String key) { + hostAgentEndpointService.setAgentHeartbeat(key); + return true; + } + + @ExposeApi + @PostMapping("/sync-host-spec") + @Operation(summary = "同步主机规格") + public Boolean syncHostSpec(@RequestHeader(CustomHeaderConst.AGENT_KEY_HEADER) String key, + @RequestParam(ExtraFieldConst.TASK_ID) String taskId, + @RequestBody JSONObject spec) { + hostExtraService.syncHostSpec(key, taskId, spec); + return true; + } + +} diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/controller/HostController.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/controller/HostController.java index ad09fc43..e8d13d33 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/controller/HostController.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/controller/HostController.java @@ -118,8 +118,9 @@ public class HostController { @Operation(summary = "通过 id 查询主机") @Parameter(name = "id", description = "id", required = true) @PreAuthorize("@ss.hasPermission('asset:host:query')") - public HostVO getHost(@RequestParam("id") Long id) { - return hostService.getHostById(id); + public HostVO getHost(@RequestParam("id") Long id, + @RequestParam(value = "base", required = false) Boolean base) { + return hostService.getHostById(id, base); } @IgnoreLog(IgnoreLogMode.RET) diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/convert/HostAgentLogConvert.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/convert/HostAgentLogConvert.java new file mode 100644 index 00000000..ca0f4744 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/convert/HostAgentLogConvert.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.asset.convert; + +import org.dromara.visor.module.asset.entity.domain.HostAgentLogDO; +import org.dromara.visor.module.asset.entity.vo.HostAgentLogVO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 主机探针日志 对象转换器 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023-9-11 14:16 + */ +@Mapper +public interface HostAgentLogConvert { + + HostAgentLogConvert MAPPER = Mappers.getMapper(HostAgentLogConvert.class); + + HostAgentLogVO to(HostAgentLogDO domain); + +} diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/convert/HostAgentLogProviderConvert.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/convert/HostAgentLogProviderConvert.java new file mode 100644 index 00000000..e828371c --- /dev/null +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/convert/HostAgentLogProviderConvert.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.asset.convert; + +import org.dromara.visor.module.asset.entity.domain.HostAgentLogDO; +import org.dromara.visor.module.asset.entity.dto.host.HostAgentLogDTO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 主机探针日志 对外对象转换器 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023-9-11 14:16 + */ +@Mapper +public interface HostAgentLogProviderConvert { + + HostAgentLogProviderConvert MAPPER = Mappers.getMapper(HostAgentLogProviderConvert.class); + + HostAgentLogDTO to(HostAgentLogDO domain); + +} diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/convert/HostConvert.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/convert/HostConvert.java index 55fc70e1..1f4ff1ef 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/convert/HostConvert.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/convert/HostConvert.java @@ -28,6 +28,7 @@ import org.dromara.visor.module.asset.entity.dto.HostCacheDTO; import org.dromara.visor.module.asset.entity.request.host.HostCreateRequest; import org.dromara.visor.module.asset.entity.request.host.HostQueryRequest; import org.dromara.visor.module.asset.entity.request.host.HostUpdateRequest; +import org.dromara.visor.module.asset.entity.vo.HostAgentStatusVO; import org.dromara.visor.module.asset.entity.vo.HostBaseVO; import org.dromara.visor.module.asset.entity.vo.HostVO; import org.mapstruct.Mapper; @@ -61,6 +62,8 @@ public interface HostConvert { HostBaseVO toBase(HostDO domain); + HostAgentStatusVO toAgentStatus(HostDO domain); + HostCreateRequest toCreate(HostUpdateRequest request); List toList(List domain); diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/convert/HostProviderConvert.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/convert/HostProviderConvert.java index 13071c56..00f40637 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/convert/HostProviderConvert.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/convert/HostProviderConvert.java @@ -26,6 +26,8 @@ import org.dromara.visor.common.mapstruct.StringConversion; import org.dromara.visor.module.asset.entity.domain.HostDO; import org.dromara.visor.module.asset.entity.dto.host.HostBaseDTO; import org.dromara.visor.module.asset.entity.dto.host.HostDTO; +import org.dromara.visor.module.asset.entity.dto.host.HostQueryDTO; +import org.dromara.visor.module.asset.entity.request.host.HostQueryRequest; import org.dromara.visor.module.asset.entity.vo.HostVO; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; @@ -42,6 +44,8 @@ public interface HostProviderConvert { HostProviderConvert MAPPER = Mappers.getMapper(HostProviderConvert.class); + HostQueryRequest to(HostQueryDTO dto); + HostDO to(HostDTO host); HostDTO to(HostDO domain); diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/dao/HostAgentLogDAO.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/dao/HostAgentLogDAO.java new file mode 100644 index 00000000..4c8d4f84 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/dao/HostAgentLogDAO.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.asset.dao; + +import org.apache.ibatis.annotations.Mapper; +import org.dromara.visor.framework.mybatis.core.mapper.IMapper; +import org.dromara.visor.module.asset.entity.domain.HostAgentLogDO; + +/** + * 主机探针日志 Mapper 接口 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025-8-28 09:55 + */ +@Mapper +public interface HostAgentLogDAO extends IMapper { + +} diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/dao/HostDAO.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/dao/HostDAO.java index 0a2e7916..80842317 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/dao/HostDAO.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/dao/HostDAO.java @@ -24,8 +24,10 @@ package org.dromara.visor.module.asset.dao; import org.apache.ibatis.annotations.Mapper; import org.dromara.visor.framework.mybatis.core.mapper.IMapper; +import org.dromara.visor.framework.mybatis.core.query.Conditions; import org.dromara.visor.module.asset.entity.domain.HostDO; +import java.util.Date; import java.util.List; /** @@ -57,4 +59,47 @@ public interface HostDAO extends IMapper { .list(HostDO::getId); } + /** + * 通过 agentKey 查询 id + * + * @param agentKey agentKey + * @return id + */ + default Long selectIdByAgentKey(String agentKey) { + return this.of() + .createWrapper() + .select(HostDO::getId) + .eq(HostDO::getAgentKey, agentKey) + .then() + .getOne(HostDO::getId); + } + + /** + * 通过 agentKey 查询 id + * + * @param agentKeys agentKeys + * @return id + */ + default List selectIdByAgentKeys(List agentKeys) { + return this.of() + .createWrapper() + .select(HostDO::getId, HostDO::getAgentKey) + .in(HostDO::getAgentKey, agentKeys) + .then() + .list(); + } + + /** + * 更新探针信息 + * + * @param keys agentKeyList + * @param update update + * @return effect + */ + default int updateByAgentKeys(List keys, HostDO update) { + update.setUpdateTime(new Date()); + // 更新 + return this.update(update, Conditions.in(HostDO::getAgentKey, keys)); + } + } diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/dao/HostIdentityDAO.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/dao/HostIdentityDAO.java index 7c36b924..83088e1d 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/dao/HostIdentityDAO.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/dao/HostIdentityDAO.java @@ -48,6 +48,7 @@ public interface HostIdentityDAO extends IMapper { */ default int setKeyWithNull(List keyIdList) { LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate() + .setSql("type = IF(type = 'KEY', 'PASSWORD', type)") .set(HostIdentityDO::getKeyId, null) .in(HostIdentityDO::getKeyId, keyIdList); return this.update(null, updateWrapper); diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/define/AssetThreadPools.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/define/AssetThreadPools.java new file mode 100644 index 00000000..b38fe855 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/define/AssetThreadPools.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.asset.define; + +import cn.orionsec.kit.lang.define.thread.ExecutorBuilder; +import cn.orionsec.kit.lang.utils.Systems; +import org.dromara.visor.common.constant.Const; + +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * 执行线程池 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/1/3 11:21 + */ +public interface AssetThreadPools { + + /** + * 批量执行主机命令线程池 + */ + ThreadPoolExecutor AGENT_INSTALL = ExecutorBuilder.create() + .namedThreadFactory("agent-install-") + .corePoolSize(Systems.PROCESS_NUM) + .maxPoolSize(Systems.PROCESS_NUM) + .keepAliveTime(Const.MS_S_60) + .workQueue(new LinkedBlockingQueue<>()) + .allowCoreThreadTimeout(true) + .build(); + +} diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/define/cache/HostCacheKeyDefine.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/define/cache/HostCacheKeyDefine.java index 9d412b53..b7f4b10f 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/define/cache/HostCacheKeyDefine.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/define/cache/HostCacheKeyDefine.java @@ -64,4 +64,12 @@ public interface HostCacheKeyDefine { .timeout(8, TimeUnit.HOURS) .build(); + CacheKeyDefine HOST_INSTALL_LOG = new CacheKeyBuilder() + .key("host:inst-log:{}") + .desc("最新的主机安装记录 ${hostId}") + .noPrefix() + .type(Long.class) + .struct(RedisCacheStruct.STRING) + .build(); + } diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/define/operator/HostOperatorType.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/define/operator/HostOperatorType.java index f0e614ad..2b579eb7 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/define/operator/HostOperatorType.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/define/operator/HostOperatorType.java @@ -50,6 +50,12 @@ public class HostOperatorType extends InitializingOperatorTypes { public static final String UPDATE_SPEC = "host:update-spec"; + public static final String INSTALL_AGENT = "host:install-agent"; + + public static final String UPDATE_AGENT_INSTALL_STATUS = "host:update-install-status"; + + public static final String UPLOAD_AGENT_RELEASE = "host:upload-agent-release"; + @Override public OperatorType[] types() { return new OperatorType[]{ @@ -59,6 +65,9 @@ public class HostOperatorType extends InitializingOperatorTypes { new OperatorType(M, UPDATE_STATUS, "修改主机状态 ${name} - ${status}"), new OperatorType(M, UPDATE_CONFIG, "修改主机配置 ${name} - ${type}"), new OperatorType(M, UPDATE_SPEC, "修改主机规格信息 ${name}"), + new OperatorType(L, INSTALL_AGENT, "安装主机探针"), + new OperatorType(L, UPDATE_AGENT_INSTALL_STATUS, "修改探针安装状态为 ${status}"), + new OperatorType(H, UPLOAD_AGENT_RELEASE, "上传探针发布包 ${name} ${signShort}"), }; } diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/domain/HostAgentLogDO.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/domain/HostAgentLogDO.java new file mode 100644 index 00000000..7788ff23 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/domain/HostAgentLogDO.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.asset.entity.domain; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import org.dromara.visor.framework.mybatis.core.domain.BaseDO; + +/** + * 主机探针日志 实体对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025-8-28 09:55 + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@TableName(value = "host_agent_log", autoResultMap = true) +@Schema(name = "HostAgentLogDO", description = "主机探针日志 实体对象") +public class HostAgentLogDO extends BaseDO { + + private static final long serialVersionUID = 1L; + + @Schema(description = "主机id") + @TableField("host_id") + private Long hostId; + + @Schema(description = "agentKey") + @TableField("agent_key") + private String agentKey; + + @Schema(description = "类型") + @TableField("type") + private String type; + + @Schema(description = "状态") + @TableField("status") + private String status; + + @Schema(description = "消息") + @TableField("message") + private String message; + +} diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/domain/HostDO.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/domain/HostDO.java index c12a40c1..79119828 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/domain/HostDO.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/domain/HostDO.java @@ -32,6 +32,8 @@ import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; import org.dromara.visor.framework.mybatis.core.domain.BaseDO; +import java.util.Date; + /** * 主机 实体对象 * @@ -78,6 +80,26 @@ public class HostDO extends BaseDO { @TableField("status") private String status; + @Schema(description = "agentKey") + @TableField("agent_key") + private String agentKey; + + @Schema(description = "探针版本") + @TableField("agent_version") + private String agentVersion; + + @Schema(description = "探针安装状态") + @TableField("agent_install_status") + private Integer agentInstallStatus; + + @Schema(description = "探针在线状态") + @TableField("agent_online_status") + private Integer agentOnlineStatus; + + @Schema(description = "探针切换在线状态时间") + @TableField("agent_online_change_time") + private Date agentOnlineChangeTime; + @Schema(description = "主机描述") @TableField("description") private String description; diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/dto/HostCacheDTO.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/dto/HostCacheDTO.java index f82eda5c..2aafbe45 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/dto/HostCacheDTO.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/dto/HostCacheDTO.java @@ -66,12 +66,12 @@ public class HostCacheDTO implements LongCacheIdModel, Serializable { @Schema(description = "主机地址") private String address; - @Schema(description = "主机端口") - private Integer port; - @Schema(description = "主机状态") private String status; + @Schema(description = "agentKey") + private String agentKey; + @Schema(description = "主机描述") private String description; diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/request/host/HostAgentInstallRequest.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/request/host/HostAgentInstallRequest.java new file mode 100644 index 00000000..a6981f41 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/request/host/HostAgentInstallRequest.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.asset.entity.request.host; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import java.util.List; + +/** + * 主机 安装探针请求对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023-9-11 14:16 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(name = "HostAgentInstallRequest", description = "主机 安装探针请求对象") +public class HostAgentInstallRequest { + + @NotEmpty + @Schema(description = "id") + private List idList; + +} diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/request/host/HostAgentInstallStatusUpdateRequest.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/request/host/HostAgentInstallStatusUpdateRequest.java new file mode 100644 index 00000000..5bf821c9 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/request/host/HostAgentInstallStatusUpdateRequest.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.asset.entity.request.host; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * 主机安装探针更新状态请求 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023-9-11 14:16 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(name = "HostAgentInstallStatusUpdateRequest", description = "主机安装探针更新状态请求") +public class HostAgentInstallStatusUpdateRequest { + + @NotNull + @Schema(description = "id") + private Long id; + + @NotNull + @Schema(description = "状态") + private String status; + + @Size(max = 1000) + @Schema(description = "消息") + private String message; + +} diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/request/host/HostQueryRequest.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/request/host/HostQueryRequest.java index 0099ceaf..6bf632a6 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/request/host/HostQueryRequest.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/request/host/HostQueryRequest.java @@ -23,7 +23,8 @@ package org.dromara.visor.module.asset.entity.request.host; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.*; +import lombok.Data; +import lombok.EqualsAndHashCode; import org.dromara.visor.common.entity.BaseQueryRequest; import javax.validation.constraints.Size; @@ -37,9 +38,6 @@ import java.util.List; * @since 2023-9-11 14:16 */ @Data -@Builder -@NoArgsConstructor -@AllArgsConstructor @EqualsAndHashCode(callSuper = true) @Schema(name = "HostQueryRequest", description = "主机 查询请求对象") public class HostQueryRequest extends BaseQueryRequest { @@ -50,6 +48,9 @@ public class HostQueryRequest extends BaseQueryRequest { @Schema(description = "id") private Long id; + @Schema(description = "id") + private List idList; + @Size(max = 8) @Schema(description = "主机类型") private String type; @@ -78,13 +79,25 @@ public class HostQueryRequest extends BaseQueryRequest { @Schema(description = "主机状态") private String status; - @Schema(description = "tag") - private List tags; - @Size(max = 255) @Schema(description = "描述") private String description; + @Schema(description = "agentKey") + private String agentKey; + + @Schema(description = "探针安装状态") + private Integer agentInstallStatus; + + @Schema(description = "探针在线状态") + private Integer agentOnlineStatus; + + @Schema(description = "通过探针排序") + private Boolean orderByAgent; + + @Schema(description = "tag") + private List tags; + @Schema(description = "是否查询分组信息") private Boolean queryGroup; diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/vo/HostAgentLogVO.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/vo/HostAgentLogVO.java new file mode 100644 index 00000000..32b90bae --- /dev/null +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/vo/HostAgentLogVO.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.asset.entity.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.Date; + +/** + * 主机探针日志 视图响应对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025-8-28 09:55 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(name = "HostAgentLogVO", description = "主机探针日志 视图响应对象") +public class HostAgentLogVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "id") + private Long id; + + @Schema(description = "主机id") + private Long hostId; + + @Schema(description = "agentKey") + private String agentKey; + + @Schema(description = "类型") + private String type; + + @Schema(description = "状态") + private String status; + + @Schema(description = "消息") + private String message; + + @Schema(description = "创建时间") + private Date createTime; + + @Schema(description = "修改时间") + private Date updateTime; + + @Schema(description = "创建人") + private String creator; + + @Schema(description = "修改人") + private String updater; + + @Schema(description = "探针状态") + private HostAgentStatusVO agentStatus; + +} diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/vo/HostAgentStatusVO.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/vo/HostAgentStatusVO.java new file mode 100644 index 00000000..d0d0ceee --- /dev/null +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/vo/HostAgentStatusVO.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.asset.entity.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 主机探针状态 视图响应对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025-8-28 09:55 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(name = "HostAgentStatusVO", description = "主机探针状态 视图响应对象") +public class HostAgentStatusVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "id") + private Long id; + + @Schema(description = "探针版本") + private String agentVersion; + + @Schema(description = "最新版本") + private String latestVersion; + + @Schema(description = "探针安装状态") + private Integer agentInstallStatus; + + @Schema(description = "探针在线状态") + private Integer agentOnlineStatus; + +} diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/vo/HostOnlineAgentConfigVO.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/vo/HostOnlineAgentConfigVO.java new file mode 100644 index 00000000..84d5852d --- /dev/null +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/vo/HostOnlineAgentConfigVO.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.asset.entity.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 主机密钥 视图响应对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023-9-20 11:55 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(name = "HostOnlineAgentConfigVO", description = "主机上线探针配置 响应对象") +public class HostOnlineAgentConfigVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "是否已同步过规格") + private Boolean specSynced; + +} diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/enums/AgentLogStatusEnum.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/enums/AgentLogStatusEnum.java new file mode 100644 index 00000000..629ba2d9 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/enums/AgentLogStatusEnum.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.asset.enums; + +/** + * 探针日志状态 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/28 10:50 + */ +public enum AgentLogStatusEnum { + + /** + * 等待中 + */ + WAIT, + + /** + * 安装中 + */ + RUNNING, + + /** + * 成功 + */ + SUCCESS, + + /** + * 失败 + */ + FAILED, + +} diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/enums/AgentLogTypeEnum.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/enums/AgentLogTypeEnum.java new file mode 100644 index 00000000..4a695033 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/enums/AgentLogTypeEnum.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.asset.enums; + +/** + * 探针日志类型 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/28 10:50 + */ +public enum AgentLogTypeEnum { + + /** + * 下线 + */ + OFFLINE, + + /** + * 上线 + */ + ONLINE, + + /** + * 安装 + */ + INSTALL, + + /** + * 启动 + */ + START, + + /** + * 停止 + */ + STOP, + +} diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/agent/intstall/AbstractAgentInstaller.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/agent/intstall/AbstractAgentInstaller.java new file mode 100644 index 00000000..6d1684c8 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/agent/intstall/AbstractAgentInstaller.java @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.asset.handler.agent.intstall; + +import cn.orionsec.kit.lang.utils.Strings; +import cn.orionsec.kit.lang.utils.Valid; +import cn.orionsec.kit.lang.utils.io.FileReaders; +import cn.orionsec.kit.lang.utils.io.Streams; +import cn.orionsec.kit.net.host.SessionStore; +import cn.orionsec.kit.net.host.sftp.SftpExecutor; +import cn.orionsec.kit.net.host.ssh.command.CommandExecutor; +import cn.orionsec.kit.spring.SpringHolder; +import lombok.extern.slf4j.Slf4j; +import org.dromara.visor.common.constant.Const; +import org.dromara.visor.common.constant.ErrorMessage; +import org.dromara.visor.common.constant.FileConst; +import org.dromara.visor.common.session.config.SshConnectConfig; +import org.dromara.visor.common.session.ssh.SessionStores; +import org.dromara.visor.common.utils.PathUtils; +import org.dromara.visor.module.asset.entity.domain.HostAgentLogDO; +import org.dromara.visor.module.asset.enums.AgentLogStatusEnum; +import org.dromara.visor.module.asset.enums.HostOsTypeEnum; +import org.dromara.visor.module.asset.handler.agent.model.AgentInstallParams; +import org.dromara.visor.module.asset.service.HostAgentLogService; +import org.dromara.visor.module.asset.service.HostConnectService; + +import java.io.IOException; + +/** + * 探针安装器 基类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/29 11:12 + */ +@Slf4j +public abstract class AbstractAgentInstaller implements AgentInstaller { + + private static final String REPLACEMENT = "$$"; + + private static final HostAgentLogService hostAgentLogService = SpringHolder.getBean(HostAgentLogService.class); + + private static final HostConnectService hostConnectService = SpringHolder.getBean(HostConnectService.class); + + protected final Long logId; + + protected final AgentInstallParams params; + + protected final String startScriptName; + + protected final String uploadAgentName; + + protected String agentHomePath; + + protected HostAgentLogDO record; + + protected SshConnectConfig sshConfig; + + protected SessionStore sessionStore; + + protected CommandExecutor commandExecutor; + + protected SftpExecutor sftpExecutor; + + public AbstractAgentInstaller(AgentInstallParams params) { + this.params = params; + this.logId = params.getLogId(); + this.startScriptName = Const.START + HostOsTypeEnum.of(params.getOsType()).getScriptSuffix(); + this.uploadAgentName = FileConst.AGENT + HostOsTypeEnum.of(params.getOsType()).getBinarySuffix(); + } + + @Override + public void run() { + log.info("AgentInstaller install start {}", logId); + // 查询记录 + this.record = hostAgentLogService.selectById(logId); + Valid.notNull(record, "AgentInstaller record is null {}", logId); + Valid.eq(record.getStatus(), AgentLogStatusEnum.WAIT.name(), "AgentInstaller record status is not WAIT {}", logId); + try { + // 更新状态 + this.updateStatus(AgentLogStatusEnum.RUNNING, null); + // 打开会话 + this.initSession(); + log.info("AgentInstaller install session init {}", logId); + // 上传文件 + this.uploadFile(); + log.info("AgentInstaller install upload finish {}", logId); + // 启动探针 + this.startAgent(); + log.info("AgentInstaller install stated {}", logId); + // 更新状态 + this.updateStatus(AgentLogStatusEnum.SUCCESS, null); + } catch (Exception e) { + // 更新状态 + log.error("AgentInstaller install error {}", logId, e); + this.updateStatus(AgentLogStatusEnum.FAILED, ErrorMessage.getErrorMessage(e, 1000)); + } finally { + this.close(); + } + } + + /** + * 初始化会话 + */ + private void initSession() { + // 获取 ssh 配置 + this.sshConfig = hostConnectService.getSshConnectConfig(params.getHostId()); + // 设置探针家目录 + this.agentHomePath = this.getAgentHomePath(); + // 打开会话 + this.sessionStore = SessionStores.openSessionStore(sshConfig); + // 打开 sftp + this.sftpExecutor = sessionStore.getSftpExecutor(sshConfig.getFileNameCharset()); + sftpExecutor.connect(); + } + + /** + * 上传文件 + * + * @throws IOException IOException + */ + protected abstract void uploadFile() throws IOException; + + /** + * 启动 agent + */ + protected abstract void startAgent() throws IOException; + + /** + * 获取探针家目录 + * + * @return path + */ + protected String getAgentHomePath() { + return PathUtils.buildAppPath(HostOsTypeEnum.WINDOWS.name().equals(params.getOsType()), + sshConfig.getUsername(), + FileConst.AGENT) + Const.SLASH; + } + + /** + * 替换文件内容 + * + * @param path path + * @return content + */ + protected String replaceContent(String path) { + // 读取文件 + byte[] contentBytes = FileReaders.readAllBytesFast(path); + // 格式化文件 + return Strings.format(new String(contentBytes), REPLACEMENT, params.getReplaceVars()); + } + + /** + * 更新状态 + * + * @param status 状态 + * @param message 消息 + */ + protected void updateStatus(AgentLogStatusEnum status, String message) { + log.info("AgentInstaller update status {}, {}", logId, status); + hostAgentLogService.updateStatus(logId, status.name(), message); + } + + @Override + public void close() { + Streams.close(sftpExecutor); + Streams.close(commandExecutor); + Streams.close(sessionStore); + } + +} diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/agent/intstall/AgentInstaller.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/agent/intstall/AgentInstaller.java new file mode 100644 index 00000000..0011da0e --- /dev/null +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/agent/intstall/AgentInstaller.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.asset.handler.agent.intstall; + +import cn.orionsec.kit.lang.able.SafeCloseable; +import org.dromara.visor.module.asset.define.AssetThreadPools; +import org.dromara.visor.module.asset.enums.HostOsTypeEnum; +import org.dromara.visor.module.asset.handler.agent.model.AgentInstallParams; + +/** + * 探针安装器 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/29 10:28 + */ +public interface AgentInstaller extends Runnable, SafeCloseable { + + /** + * 启动安装 + * + * @param params params + */ + static void start(AgentInstallParams params) { + AgentInstaller installer; + if (HostOsTypeEnum.WINDOWS.name().equals(params.getOsType())) { + // windows 安装 + installer = new WindowsAgentInstaller(params); + } else { + // 其他安装 + installer = new LinuxAgentInstaller(params); + } + AssetThreadPools.AGENT_INSTALL.execute(installer); + } + +} diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/agent/intstall/LinuxAgentInstaller.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/agent/intstall/LinuxAgentInstaller.java new file mode 100644 index 00000000..856a29bf --- /dev/null +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/agent/intstall/LinuxAgentInstaller.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.asset.handler.agent.intstall; + +import cn.orionsec.kit.lang.utils.Exceptions; +import cn.orionsec.kit.net.host.ssh.command.CommandExecutors; +import lombok.extern.slf4j.Slf4j; +import org.dromara.visor.common.constant.FileConst; +import org.dromara.visor.module.asset.handler.agent.model.AgentInstallParams; + +import java.io.IOException; + +/** + * linux 探针安装器 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/29 11:12 + */ +@Slf4j +public class LinuxAgentInstaller extends AbstractAgentInstaller { + + public LinuxAgentInstaller(AgentInstallParams params) { + super(params); + } + + @Override + protected void uploadFile() throws IOException { + // 写入配置文件 + sftpExecutor.write(agentHomePath + FileConst.CONFIG_YAML, this.replaceContent(params.getConfigFilePath())); + log.info("写入配置文件成功"); + // 写入启动脚本 + sftpExecutor.write(agentHomePath + startScriptName, this.replaceContent(params.getStartScriptPath())); + log.info("写入启动脚本成功"); + // 上传探针文件 + sftpExecutor.uploadFile(agentHomePath + uploadAgentName, params.getAgentFilePath()); + log.info("上传探针文件成功"); + // 脚本提权 + sftpExecutor.changeMode(agentHomePath + startScriptName, 777); + log.info("脚本提权成功"); + // 探针提权 + sftpExecutor.changeMode(agentHomePath + uploadAgentName, 777); + log.info("探针提权成功"); + } + + @Override + protected void startAgent() throws IOException { + String command = "cd " + agentHomePath + " && sh " + startScriptName; + log.info("LinuxAgentInstaller command: {}", command); + this.commandExecutor = sessionStore.getCommandExecutor(command); + this.commandExecutor.pty(false); + byte[] result = CommandExecutors.getCommandOutputResult(this.commandExecutor); + // 如果不是成功启动则抛异常 + if (!commandExecutor.isSuccessExit()) { + throw Exceptions.app("exit: " + commandExecutor.getExitCode() + ". " + new String(result)); + } + } + +} diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/agent/intstall/WindowsAgentInstaller.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/agent/intstall/WindowsAgentInstaller.java new file mode 100644 index 00000000..a9fab0db --- /dev/null +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/agent/intstall/WindowsAgentInstaller.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.asset.handler.agent.intstall; + +import lombok.extern.slf4j.Slf4j; +import org.dromara.visor.common.constant.Const; +import org.dromara.visor.common.constant.FileConst; +import org.dromara.visor.module.asset.handler.agent.model.AgentInstallParams; + +import java.io.IOException; + +/** + * windows 探针安装器 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/29 11:12 + */ +@Slf4j +public class WindowsAgentInstaller extends AbstractAgentInstaller { + + public WindowsAgentInstaller(AgentInstallParams params) { + super(params); + } + + @Override + protected void uploadFile() throws IOException { + // 写入配置文件 + sftpExecutor.write(Const.SLASH + agentHomePath + FileConst.CONFIG_YAML, this.replaceContent(params.getConfigFilePath())); + log.info("写入配置文件成功"); + // 写入启动脚本 + sftpExecutor.write(Const.SLASH + agentHomePath + startScriptName, this.replaceContent(params.getStartScriptPath())); + log.info("写入启动脚本成功"); + // 上传探针文件 + sftpExecutor.uploadFile(Const.SLASH + agentHomePath + uploadAgentName, params.getAgentFilePath()); + log.info("上传探针文件成功"); + } + + @Override + protected void startAgent() { + // TODO + } + +} diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/agent/model/AgentInstallParams.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/agent/model/AgentInstallParams.java new file mode 100644 index 00000000..ff06fd29 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/agent/model/AgentInstallParams.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.asset.handler.agent.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * 探针安装参数 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/28 18:01 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AgentInstallParams { + + /** + * logId + */ + private Long logId; + + /** + * hostId + */ + private Long hostId; + + /** + * 系统类型 + */ + private String osType; + + /** + * agentKey + */ + private String agentKey; + + /** + * 配置文件路径 + */ + private String configFilePath; + + /** + * 探针文件路径 + */ + private String agentFilePath; + + /** + * 启动脚本路径 + */ + private String startScriptPath; + + /** + * 替换变量 + */ + private Map replaceVars; + +} diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/host/extra/model/HostSpecExtraModel.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/host/extra/model/HostSpecExtraModel.java index 5c85129d..42982e9a 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/host/extra/model/HostSpecExtraModel.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/host/extra/model/HostSpecExtraModel.java @@ -41,6 +41,11 @@ import java.util.List; @AllArgsConstructor public class HostSpecExtraModel implements GenericsDataModel { + /** + * 是否已同步 + */ + private Boolean synced; + /** * sn */ @@ -109,7 +114,7 @@ public class HostSpecExtraModel implements GenericsDataModel { /** * 负责人 */ - private String chargePerson; + private String ownerPerson; /** * 创建时间 diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/host/extra/strategy/HostSpecExtraStrategy.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/host/extra/strategy/HostSpecExtraStrategy.java index 5d4019c4..1b790973 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/host/extra/strategy/HostSpecExtraStrategy.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/host/extra/strategy/HostSpecExtraStrategy.java @@ -45,4 +45,9 @@ public class HostSpecExtraStrategy extends AbstractGenericsDataStrategy getAgentInstallLogStatus(List idList); + + /** + * 更新日志状态 + * + * @param id id + * @param status status + * @param message message + */ + void updateStatus(Long id, String status, String message); + +} diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/HostAgentService.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/HostAgentService.java new file mode 100644 index 00000000..72a7d9d4 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/HostAgentService.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.asset.service; + +import org.dromara.visor.module.asset.entity.request.host.HostAgentInstallRequest; +import org.dromara.visor.module.asset.entity.vo.HostAgentStatusVO; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + * 主机探针 服务类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/27 16:06 + */ +public interface HostAgentService { + + /** + * 获取探针状态 + * + * @param idList idList + * @return rows + */ + List getAgentStatus(List idList); + + /** + * 安装探针 + * + * @param request request + */ + void installAgent(HostAgentInstallRequest request); + + /** + * 上传探针发布包 + * + * @param file file + */ + void uploadAgentRelease(MultipartFile file); + + /** + * 获取探针版本 + * + * @return version + */ + String getAgentVersion(); + +} diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/HostExtraService.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/HostExtraService.java index 6d9a2ae5..5a167bf3 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/HostExtraService.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/HostExtraService.java @@ -22,6 +22,7 @@ */ package org.dromara.visor.module.asset.service; +import com.alibaba.fastjson.JSONObject; import org.dromara.visor.common.handler.data.model.GenericsDataModel; import org.dromara.visor.module.asset.entity.request.host.HostExtraUpdateRequest; import org.dromara.visor.module.asset.handler.host.extra.HostExtraItemEnum; @@ -83,4 +84,13 @@ public interface HostExtraService { */ void copyHostExtra(Long originId, Long newId); + /** + * 同步主机规格 + * + * @param key key + * @param taskId taskId + * @param spec spec + */ + void syncHostSpec(String key, String taskId, JSONObject spec); + } diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/HostService.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/HostService.java index f9f33e10..13767252 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/HostService.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/HostService.java @@ -83,10 +83,11 @@ public interface HostService { /** * 通过 id 查询主机 * - * @param id id + * @param id id + * @param base base * @return row */ - HostVO getHostById(Long id); + HostVO getHostById(Long id, Boolean base); /** * 查询主机 diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostAgentEndpointServiceImpl.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostAgentEndpointServiceImpl.java new file mode 100644 index 00000000..a3cb9f1f --- /dev/null +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostAgentEndpointServiceImpl.java @@ -0,0 +1,257 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.asset.service.impl; + +import cn.orionsec.kit.lang.utils.Booleans; +import cn.orionsec.kit.lang.utils.Valid; +import cn.orionsec.kit.lang.utils.collect.Lists; +import lombok.extern.slf4j.Slf4j; +import org.dromara.visor.common.constant.Const; +import org.dromara.visor.common.constant.ErrorMessage; +import org.dromara.visor.module.asset.dao.HostAgentLogDAO; +import org.dromara.visor.module.asset.dao.HostDAO; +import org.dromara.visor.module.asset.entity.domain.HostAgentLogDO; +import org.dromara.visor.module.asset.entity.domain.HostDO; +import org.dromara.visor.module.asset.entity.vo.HostOnlineAgentConfigVO; +import org.dromara.visor.module.asset.enums.AgentInstallStatusEnum; +import org.dromara.visor.module.asset.enums.AgentLogStatusEnum; +import org.dromara.visor.module.asset.enums.AgentLogTypeEnum; +import org.dromara.visor.module.asset.enums.AgentOnlineStatusEnum; +import org.dromara.visor.module.asset.handler.host.extra.HostExtraItemEnum; +import org.dromara.visor.module.asset.handler.host.extra.model.HostSpecExtraModel; +import org.dromara.visor.module.asset.service.HostAgentEndpointService; +import org.dromara.visor.module.asset.service.HostExtraService; +import org.dromara.visor.module.monitor.api.MonitorHostApi; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * 监控探针端点 服务实现类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/27 16:07 + */ +@Slf4j +@Service +public class HostAgentEndpointServiceImpl implements HostAgentEndpointService { + + /** + * 标记离线阈值 + * 上报间隔(60s) * 2 + 5s + */ + private static final int MARK_OFFLINE_THRESHOLD = (60 * 2 + 5) * 1000; + + private static final ConcurrentHashMap ONLINE_STATUS_CACHE = new ConcurrentHashMap<>(); + + private static final ConcurrentHashMap HEARTBEAT_RECV_CACHE = new ConcurrentHashMap<>(); + + @Resource + private HostDAO hostDAO; + + @Resource + private HostAgentLogDAO hostAgentLogDAO; + + @Resource + private HostExtraService hostExtraService; + + @Resource + private MonitorHostApi monitorHostApi; + + /** + * 初始化主机在线状态 + */ + @PostConstruct + public void initHostOnlineStatus() { + List hosts = hostDAO.selectList(null); + for (HostDO host : hosts) { + Integer agentOnlineStatus = host.getAgentOnlineStatus(); + if (agentOnlineStatus != null) { + ONLINE_STATUS_CACHE.put(host.getAgentKey(), agentOnlineStatus); + } + } + } + + @Override + public HostOnlineAgentConfigVO setAgentOnline(String agentKey, String version) { + log.info("HostAgentEndpointService setAgentOnline agentKey: {}. version: {}", agentKey, version); + try { + // 查询主机信息 + Long hostId = hostDAO.selectIdByAgentKey(agentKey); + Valid.notNull(hostId, ErrorMessage.HOST_ABSENT); + // 查询主机规格信息 + HostSpecExtraModel spec = hostExtraService.getHostExtra(Const.SYSTEM_USER_ID, hostId, HostExtraItemEnum.SPEC); + Boolean synced = Optional.ofNullable(spec) + .map(HostSpecExtraModel::getSynced) + .map(Booleans::isTrue) + .orElse(false); + // 提前修正缓存 + ONLINE_STATUS_CACHE.put(agentKey, AgentOnlineStatusEnum.ONLINE.getValue()); + // 设置心跳 + this.setAgentHeartbeat(agentKey); + // 修改状态 + HostDO update = HostDO.builder() + .agentVersion(version) + .agentOnlineStatus(AgentOnlineStatusEnum.ONLINE.getValue()) + .agentOnlineChangeTime(new Date()) + .agentInstallStatus(AgentInstallStatusEnum.INSTALLED.getStatus()) + .build(); + hostDAO.updateByAgentKeys(Lists.singleton(agentKey), update); + // 插入日志 + HostAgentLogDO agentLog = HostAgentLogDO.builder() + .hostId(hostId) + .agentKey(agentKey) + .type(AgentLogTypeEnum.ONLINE.name()) + .status(AgentLogStatusEnum.SUCCESS.name()) + .build(); + hostAgentLogDAO.insert(agentLog); + // 返回 + return HostOnlineAgentConfigVO.builder() + .specSynced(synced) + .build(); + } catch (Exception e) { + // 由心跳修正 + ONLINE_STATUS_CACHE.put(agentKey, AgentOnlineStatusEnum.OFFLINE.getValue()); + throw e; + } + } + + @Override + public void setAgentOffline(String agentKey) { + log.info("HostAgentEndpointService setAgentOffline agentKey: {}", agentKey); + // 查询主机信息 + Long hostId = hostDAO.selectIdByAgentKey(agentKey); + Valid.notNull(hostId, ErrorMessage.HOST_ABSENT); + // 修改缓存 + ONLINE_STATUS_CACHE.put(agentKey, AgentOnlineStatusEnum.OFFLINE.getValue()); + HEARTBEAT_RECV_CACHE.put(agentKey, 0L); + // 修改状态 + HostDO update = HostDO.builder() + .agentOnlineStatus(AgentOnlineStatusEnum.OFFLINE.getValue()) + .agentOnlineChangeTime(new Date()) + .build(); + hostDAO.updateByAgentKeys(Lists.singleton(agentKey), update); + // 插入日志 + HostAgentLogDO agentLog = HostAgentLogDO.builder() + .hostId(hostId) + .agentKey(agentKey) + .type(AgentLogTypeEnum.OFFLINE.name()) + .status(AgentLogStatusEnum.SUCCESS.name()) + .build(); + hostAgentLogDAO.insert(agentLog); + // 设置监控上下文为已下线 + monitorHostApi.setAgentOffline(Lists.singleton(agentKey)); + } + + @Override + public void setAgentHeartbeat(String agentKey) { + // 设置心跳时间 + HEARTBEAT_RECV_CACHE.put(agentKey, System.currentTimeMillis()); + } + + @Override + public void checkHeartbeat() { + long now = System.currentTimeMillis(); + List markOnlineList = new ArrayList<>(); + List markOfflineList = new ArrayList<>(); + // 状态检查 + ONLINE_STATUS_CACHE.forEach((key, status) -> { + // 上次心跳时间 + Long lastHeartbeatTime = HEARTBEAT_RECV_CACHE.getOrDefault(key, 0L); + // 超过阈值标记离线 + if (now - lastHeartbeatTime > MARK_OFFLINE_THRESHOLD) { + // 如果当前状态是在线则标记离线 + if (AgentOnlineStatusEnum.ONLINE.getValue().equals(status)) { + markOfflineList.add(key); + } + } else { + // 如果当前状态是离线则标记在线 + if (AgentOnlineStatusEnum.OFFLINE.getValue().equals(status)) { + markOnlineList.add(key); + } + } + }); + // 更新在线状态 + if (!markOnlineList.isEmpty()) { + this.markOnlineStatus(markOnlineList, AgentOnlineStatusEnum.ONLINE); + } + // 更新离线状态 + if (!markOfflineList.isEmpty()) { + this.markOnlineStatus(markOfflineList, AgentOnlineStatusEnum.OFFLINE); + } + } + + /** + * 标记在线状态 + * + * @param agentKeyList agentKeyList + * @param status status + */ + private void markOnlineStatus(List agentKeyList, AgentOnlineStatusEnum status) { + if (Lists.isEmpty(agentKeyList)) { + return; + } + log.info("HostAgentEndpointService mark {}. count: {}, keys: {}", status, agentKeyList.size(), agentKeyList); + // 更新数据 + HostDO update = HostDO.builder() + .agentOnlineStatus(status.getValue()) + .agentOnlineChangeTime(new Date()) + .build(); + int effect = hostDAO.updateByAgentKeys(agentKeyList, update); + // 更新缓存 + agentKeyList.forEach(s -> ONLINE_STATUS_CACHE.put(s, status.getValue())); + log.info("HostAgentEndpointService mark {}. effect: {}", status, effect); + // 插入日志 + List logList = hostDAO.selectIdByAgentKeys(agentKeyList) + .stream() + .map(s -> { + HostAgentLogDO agentLog = HostAgentLogDO.builder() + .hostId(s.getId()) + .agentKey(s.getAgentKey()) + .status(AgentLogStatusEnum.SUCCESS.name()) + .build(); + if (AgentOnlineStatusEnum.ONLINE.equals(status)) { + agentLog.setType(AgentLogTypeEnum.ONLINE.name()); + } else { + agentLog.setType(AgentLogTypeEnum.OFFLINE.name()); + } + return agentLog; + }).collect(Collectors.toList()); + if (!logList.isEmpty()) { + hostAgentLogDAO.insertBatch(logList); + } + // 设置监控上下文为已下线 + if (AgentOnlineStatusEnum.OFFLINE.equals(status)) { + monitorHostApi.setAgentOffline(agentKeyList); + } + } + +} diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostAgentLogServiceImpl.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostAgentLogServiceImpl.java new file mode 100644 index 00000000..e75ece0a --- /dev/null +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostAgentLogServiceImpl.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.asset.service.impl; + +import cn.orionsec.kit.lang.function.Functions; +import cn.orionsec.kit.lang.utils.collect.Lists; +import org.dromara.visor.module.asset.convert.HostAgentLogConvert; +import org.dromara.visor.module.asset.dao.HostAgentLogDAO; +import org.dromara.visor.module.asset.entity.domain.HostAgentLogDO; +import org.dromara.visor.module.asset.entity.vo.HostAgentLogVO; +import org.dromara.visor.module.asset.entity.vo.HostAgentStatusVO; +import org.dromara.visor.module.asset.enums.AgentLogStatusEnum; +import org.dromara.visor.module.asset.service.HostAgentLogService; +import org.dromara.visor.module.asset.service.HostAgentService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 主机探针日志 服务实现类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/9/2 16:31 + */ +@Service +public class HostAgentLogServiceImpl implements HostAgentLogService { + + @Resource + private HostAgentLogDAO hostAgentLogDAO; + + @Resource + private HostAgentService hostAgentService; + + @Override + public HostAgentLogDO selectById(Long id) { + return hostAgentLogDAO.selectById(id); + } + + @Override + public List getAgentInstallLogStatus(List idList) { + if (Lists.isEmpty(idList)) { + return Lists.empty(); + } + // 查询日志 + List records = hostAgentLogDAO.selectBatchIds(idList) + .stream() + .map(HostAgentLogConvert.MAPPER::to) + .collect(Collectors.toList()); + // 如果是已完成需要查询 + List successRecords = records.stream() + .filter(s -> AgentLogStatusEnum.SUCCESS.name().equals(s.getStatus())) + .collect(Collectors.toList()); + if (!successRecords.isEmpty()) { + // 完成后查询主机信息 + List hostIdList = successRecords.stream() + .map(HostAgentLogVO::getHostId) + .distinct() + .collect(Collectors.toList()); + // 查询状态信息 + Map agentStatusMap = hostAgentService.getAgentStatus(hostIdList) + .stream() + .collect(Collectors.toMap(HostAgentStatusVO::getId, + Function.identity(), + Functions.right())); + // 设置状态信息 + for (HostAgentLogVO successRecord : successRecords) { + successRecord.setAgentStatus(agentStatusMap.get(successRecord.getHostId())); + } + } + return records; + } + + @Override + public void updateStatus(Long id, String status, String message) { + HostAgentLogDO update = HostAgentLogDO.builder() + .id(id) + .status(status) + .message(message) + .build(); + hostAgentLogDAO.updateById(update); + } + +} diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostAgentServiceImpl.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostAgentServiceImpl.java new file mode 100644 index 00000000..e4278506 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostAgentServiceImpl.java @@ -0,0 +1,323 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.asset.service.impl; + +import cn.orionsec.kit.lang.utils.Exceptions; +import cn.orionsec.kit.lang.utils.Strings; +import cn.orionsec.kit.lang.utils.collect.Lists; +import cn.orionsec.kit.lang.utils.collect.Maps; +import cn.orionsec.kit.lang.utils.crypto.enums.HashDigest; +import cn.orionsec.kit.lang.utils.io.FileReaders; +import cn.orionsec.kit.lang.utils.io.Files1; +import cn.orionsec.kit.lang.utils.io.compress.CompressTypeEnum; +import cn.orionsec.kit.lang.utils.io.compress.FileDecompressor; +import cn.orionsec.kit.lang.utils.net.IPs; +import lombok.extern.slf4j.Slf4j; +import org.dromara.visor.common.constant.Const; +import org.dromara.visor.common.constant.ErrorMessage; +import org.dromara.visor.common.constant.ExtraFieldConst; +import org.dromara.visor.common.constant.FileConst; +import org.dromara.visor.common.utils.PathUtils; +import org.dromara.visor.common.utils.Valid; +import org.dromara.visor.framework.biz.operator.log.core.utils.OperatorLogs; +import org.dromara.visor.framework.redis.core.utils.RedisStrings; +import org.dromara.visor.module.asset.convert.HostConvert; +import org.dromara.visor.module.asset.dao.HostAgentLogDAO; +import org.dromara.visor.module.asset.dao.HostDAO; +import org.dromara.visor.module.asset.define.cache.HostCacheKeyDefine; +import org.dromara.visor.module.asset.entity.domain.HostAgentLogDO; +import org.dromara.visor.module.asset.entity.domain.HostDO; +import org.dromara.visor.module.asset.entity.request.host.HostAgentInstallRequest; +import org.dromara.visor.module.asset.entity.vo.HostAgentStatusVO; +import org.dromara.visor.module.asset.enums.*; +import org.dromara.visor.module.asset.handler.agent.intstall.AgentInstaller; +import org.dromara.visor.module.asset.handler.agent.model.AgentInstallParams; +import org.dromara.visor.module.asset.service.HostAgentService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 主机探针 服务实现类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/28 10:10 + */ +@Slf4j +@Service +public class HostAgentServiceImpl implements HostAgentService { + + private static final String AGENT_FILE_FORMAT = "agent_{}_{}{}"; + + private String localVersion; + + @Value("${orion.api.expose.token}") + private String exposeToken; + + @Resource + private HostDAO hostDAO; + + @Resource + private HostAgentLogDAO hostAgentLogDAO; + + /** + * 读取本地探针版本 + */ + @PostConstruct + public void readLocalAgentVersion() { + log.info("HostAgentService-readLocalAgentVersion start"); + // 文件路径 + String path = PathUtils.getOrionPath(FileConst.AGENT_RELEASE + Const.SLASH + FileConst.VERSION); + log.info("HostAgentService-readLocalAgentVersion path: {}", path); + try { + if (!Files1.isFile(path)) { + log.error("HostAgentService-readLocalAgentVersion not file"); + return; + } + // 读取文件内容 + byte[] bytes = FileReaders.readAllBytesFast(path); + this.localVersion = new String(bytes).trim(); + log.info("HostAgentService-readLocalAgentVersion version: {}", localVersion); + } catch (Exception e) { + log.error("HostAgentService-readLocalAgentVersion error", e); + } + } + + @Override + public List getAgentStatus(List idList) { + if (Lists.isEmpty(idList)) { + return Lists.empty(); + } + return hostDAO.of() + .createWrapper() + .select(HostDO::getId, + HostDO::getAgentVersion, + HostDO::getAgentInstallStatus, + HostDO::getAgentOnlineStatus) + .in(HostDO::getId, idList) + .then() + .stream() + .map(HostConvert.MAPPER::toAgentStatus) + .peek(s -> s.setLatestVersion(localVersion)) + .collect(Collectors.toList()); + } + + @Override + public void installAgent(HostAgentInstallRequest request) { + // 查询主机信息 + List idList = request.getIdList(); + List hosts = hostDAO.selectBatchIds(idList); + Valid.eq(hosts.size(), idList.size(), ErrorMessage.HOST_ABSENT); + + // 检查并创建安装任务参数 + List installTaskParams = this.createInstallTaskParams(hosts); + + // 查询当前状态 + boolean hasRunning = hostAgentLogDAO.of() + .createWrapper() + .in(HostAgentLogDO::getHostId, idList) + .eq(HostAgentLogDO::getType, AgentLogTypeEnum.INSTALL.name()) + .in(HostAgentLogDO::getStatus, AgentLogStatusEnum.WAIT.name(), AgentLogStatusEnum.RUNNING.name()) + .then() + .present(); + Valid.isFalse(hasRunning, ErrorMessage.ILLEGAL_STATUS); + + // 创建日志记录 + List agentLogs = hosts.stream() + .map(s -> HostAgentLogDO.builder() + .hostId(s.getId()) + .agentKey(s.getAgentKey()) + .type(AgentLogTypeEnum.INSTALL.name()) + .status(AgentLogStatusEnum.WAIT.name()) + .build()) + .collect(Collectors.toList()); + hostAgentLogDAO.insertBatch(agentLogs); + + // 设置缓存 + for (HostAgentLogDO agentLog : agentLogs) { + String key = HostCacheKeyDefine.HOST_INSTALL_LOG.format(agentLog.getHostId()); + RedisStrings.set(key, HostCacheKeyDefine.HOST_INSTALL_LOG, agentLog.getId()); + } + + // 获取替换变量 + Map replaceVars = this.getReplaceVars(); + + // 提交任务 + for (int i = 0; i < installTaskParams.size(); i++) { + AgentInstallParams params = installTaskParams.get(i); + HostAgentLogDO agentLog = agentLogs.get(i); + params.setLogId(agentLog.getId()); + params.setReplaceVars(this.getHostReplaceVars(replaceVars, params)); + // 执行任务 + AgentInstaller.start(params); + } + } + + @Override + public void uploadAgentRelease(MultipartFile file) { + // 检查文件名 + String fileName = Optional.of(file) + .map(MultipartFile::getOriginalFilename) + .map(String::toLowerCase) + .orElse(Const.EMPTY); + Valid.notBlank(fileName, ErrorMessage.FILE_EXTENSION_TYPE); + Valid.isTrue(fileName.endsWith(Const.SUFFIX_TAR_GZ), ErrorMessage.FILE_EXTENSION_TYPE); + // 保存文件 + String releaseDir = PathUtils.getOrionPath(FileConst.AGENT_RELEASE); + String releaseTempDir = PathUtils.getOrionPath(FileConst.AGENT_RELEASE_TEMP); + File releaseTempFile = new File(releaseTempDir + Const.SLASH + FileConst.AGENT_RELEASE_TAR_GZ); + log.info("HostAgentService.installAgent start releaseTempDir: {}, releaseTempFile: {}", releaseTempDir, releaseTempFile.getAbsolutePath()); + try { + // 创建目录 + Files1.mkdirs(releaseTempFile.getParentFile()); + // 传输文件 + file.transferTo(releaseTempFile); + } catch (IOException e) { + throw Exceptions.app(ErrorMessage.FILE_UPLOAD_ERROR, e); + } + // 计算签名 + try { + String sign = Files1.sign(releaseTempFile, HashDigest.SHA256); + OperatorLogs.add(ExtraFieldConst.SIGN, sign); + OperatorLogs.add(ExtraFieldConst.SIGN_SHORT, sign.substring(0, 8)); + log.error("HostAgentService.installAgent calc sha256 sign: {}", sign); + } catch (Exception e) { + log.error("HostAgentService.installAgent calc sha256 error", e); + throw Exceptions.app(ErrorMessage.CALC_SIGN_FAILED, e); + } + // 解压缩文件 + try { + FileDecompressor decompressor = CompressTypeEnum.TAR_GZ.decompressor().get(); + decompressor.setDecompressFile(releaseTempFile); + decompressor.setDecompressTargetPath(releaseTempDir); + decompressor.decompress(); + log.info("HostAgentService.installAgent decompress success"); + } catch (Exception e) { + log.error("HostAgentService.installAgent decompress error", e); + throw Exceptions.app(ErrorMessage.CALC_SIGN_FAILED, e); + } + // 获取全部文件 + List decompressFiles = Files1.listFiles(releaseTempDir); + log.info("HostAgentService.installAgent decompressFiles: {}", Lists.map(decompressFiles, File::getName)); + // 检查版本文件 + String versionFile = releaseTempDir + Const.SLASH + FileConst.VERSION; + Valid.isTrue(Files1.isFile(versionFile), ErrorMessage.DECOMPRESS_FILE_ABSENT + Const.SPACE + FileConst.VERSION); + // 移动文件 + for (File decompressFile : decompressFiles) { + String releaseFile = releaseDir + Const.SLASH + decompressFile.getName(); + // 删除原始文件 + Files1.deleteFile(releaseFile); + // 复制文件 + Files1.copy(decompressFile.getAbsolutePath(), releaseFile); + log.info("HostAgentService.installAgent move: {}", releaseFile); + } + // 删除临时文件夹 + Files1.delete(releaseTempDir); + // 重新加载版本 + this.readLocalAgentVersion(); + } + + @Override + public String getAgentVersion() { + return localVersion; + } + + /** + * 检查并创建安装任务参数 + * + * @param hosts hosts + * @return taskParams + */ + private List createInstallTaskParams(List hosts) { + List taskParams = new ArrayList<>(); + // 待检查的文件列表 + Set checkFileList = new HashSet<>(); + // 任务参数 + for (HostDO host : hosts) { + // 是否启用 + Valid.eq(HostStatusEnum.ENABLED.name(), host.getStatus(), ErrorMessage.HOST_NOT_ENABLED, host.getName()); + // 是否支持 ssh + boolean supportSsh = HostTypeEnum.SSH.contains(host.getTypes()); + Valid.isTrue(supportSsh, ErrorMessage.PLEASE_CHECK_HOST_SSH, host.getName()); + // 文件名称 + HostOsTypeEnum os = HostOsTypeEnum.of(host.getOsType()); + String agentFileName = Strings.format(AGENT_FILE_FORMAT, + os.name().toLowerCase(), + host.getArchType().toLowerCase(), + os.getBinarySuffix()); + // 安装参数 + AgentInstallParams params = AgentInstallParams.builder() + .hostId(host.getId()) + .osType(host.getOsType()) + .agentKey(host.getAgentKey()) + .agentFilePath(PathUtils.getOrionPath(FileConst.AGENT_RELEASE + Const.SLASH + agentFileName)) + .configFilePath(PathUtils.getOrionPath(FileConst.AGENT_RELEASE + Const.SLASH + FileConst.CONFIG_YAML)) + .startScriptPath(PathUtils.getOrionPath(FileConst.AGENT_RELEASE + Const.SLASH + Const.START + os.getScriptSuffix())) + .build(); + taskParams.add(params); + // 添加待检查文件 + checkFileList.add(params.getAgentFilePath()); + checkFileList.add(params.getStartScriptPath()); + checkFileList.add(params.getConfigFilePath()); + } + + // 检查文件是否存在 + for (String file : checkFileList) { + Valid.isTrue(Files1.isFile(file), ErrorMessage.FILE_ABSENT + Const.SPACE + file); + } + return taskParams; + } + + /** + * 获取替换变量 + * + * @return vars + */ + private Map getReplaceVars() { + Map map = new HashMap<>(); + map.put("SERVER_HOST", IPs.IP); + map.put("SERVER_TOKEN", exposeToken); + return map; + } + + /** + * 获取主机替换变量 + * + * @param vars vars + * @return vars + */ + private Map getHostReplaceVars(Map vars, AgentInstallParams params) { + vars = Maps.newMap(vars); + vars.put("AGENT_KEY", params.getAgentKey()); + return vars; + } + +} diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostConnectServiceImpl.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostConnectServiceImpl.java index 647a5451..c369de68 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostConnectServiceImpl.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostConnectServiceImpl.java @@ -476,6 +476,7 @@ public class HostConnectServiceImpl implements HostConnectService { config.setHostName(host.getName()); config.setHostCode(host.getCode()); config.setHostAddress(host.getAddress()); + config.setAgentKey(host.getAgentKey()); } } diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostExtraServiceImpl.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostExtraServiceImpl.java index 3add637b..0e7cb714 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostExtraServiceImpl.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostExtraServiceImpl.java @@ -23,10 +23,15 @@ package org.dromara.visor.module.asset.service.impl; import cn.orionsec.kit.lang.utils.Strings; +import cn.orionsec.kit.lang.utils.collect.Lists; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; import org.dromara.visor.common.constant.Const; +import org.dromara.visor.common.constant.ErrorMessage; import org.dromara.visor.common.handler.data.model.GenericsDataModel; import org.dromara.visor.common.utils.Valid; import org.dromara.visor.framework.security.core.utils.SecurityUtils; +import org.dromara.visor.module.asset.dao.HostDAO; import org.dromara.visor.module.asset.entity.request.host.HostExtraUpdateRequest; import org.dromara.visor.module.asset.handler.host.extra.HostExtraItemEnum; import org.dromara.visor.module.asset.handler.host.extra.model.HostSpecExtraModel; @@ -53,6 +58,9 @@ import java.util.stream.Collectors; @Service public class HostExtraServiceImpl implements HostExtraService { + @Resource + private HostDAO hostDAO; + @Resource private DataExtraApi dataExtraApi; @@ -146,6 +154,44 @@ public class HostExtraServiceImpl implements HostExtraService { dataExtraApi.addExtraItems(newItems, DataExtraTypeEnum.HOST); } + @Override + public void syncHostSpec(String key, String taskId, JSONObject spec) { + try { + // 查询主机id + Long id = hostDAO.selectIdByAgentKey(key); + Valid.notNull(id, ErrorMessage.HOST_ABSENT); + // 设置已同步标识 + spec.put(Const.SYNCED, true); + // 查询配置信息 + String newSpec; + HostSpecExtraModel beforeSpec = this.getHostSpecMap(Lists.singleton(id)).get(id); + if (beforeSpec == null) { + // 新增 + newSpec = spec.toString(); + } else { + // 合并 + JSONObject beforeSpecValue = JSON.parseObject(beforeSpec.serial()); + spec.forEach((k, v) -> { + if (v != null) { + beforeSpecValue.put(k, v); + } + }); + newSpec = beforeSpecValue.toJSONString(); + } + // 修改规格 + DataExtraSetDTO update = new DataExtraSetDTO(); + update.setUserId(Const.SYSTEM_USER_ID); + update.setRelId(id); + update.setItem(HostExtraItemEnum.SPEC.name()); + update.setValue(newSpec); + dataExtraApi.setExtraItem(update, DataExtraTypeEnum.HOST); + // 回调成功 + } catch (Exception e) { + // 回调失败 + throw e; + } + } + /** * 检查配置项并且转为视图 (不存在则初始化默认值) * diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostServiceImpl.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostServiceImpl.java index b5a2c178..4d10c694 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostServiceImpl.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostServiceImpl.java @@ -23,6 +23,7 @@ package org.dromara.visor.module.asset.service.impl; import cn.orionsec.kit.lang.define.wrapper.DataGrid; +import cn.orionsec.kit.lang.id.UUIds; import cn.orionsec.kit.lang.utils.Booleans; import cn.orionsec.kit.lang.utils.Strings; import cn.orionsec.kit.lang.utils.collect.Lists; @@ -36,6 +37,7 @@ import org.dromara.visor.common.constant.ErrorMessage; import org.dromara.visor.common.enums.EnableStatus; import org.dromara.visor.common.utils.Valid; import org.dromara.visor.framework.biz.operator.log.core.utils.OperatorLogs; +import org.dromara.visor.framework.mybatis.core.query.DataQuery; import org.dromara.visor.framework.redis.core.utils.RedisMaps; import org.dromara.visor.framework.redis.core.utils.barrier.CacheBarriers; import org.dromara.visor.module.asset.convert.HostConvert; @@ -46,6 +48,7 @@ import org.dromara.visor.module.asset.entity.domain.HostDO; import org.dromara.visor.module.asset.entity.dto.HostCacheDTO; import org.dromara.visor.module.asset.entity.request.host.*; import org.dromara.visor.module.asset.entity.vo.HostVO; +import org.dromara.visor.module.asset.enums.AgentInstallStatusEnum; import org.dromara.visor.module.asset.enums.HostStatusEnum; import org.dromara.visor.module.asset.handler.host.extra.HostExtraItemEnum; import org.dromara.visor.module.asset.handler.host.extra.model.HostSpecExtraModel; @@ -63,6 +66,7 @@ import org.dromara.visor.module.infra.enums.DataExtraTypeEnum; import org.dromara.visor.module.infra.enums.DataGroupTypeEnum; import org.dromara.visor.module.infra.enums.FavoriteTypeEnum; import org.dromara.visor.module.infra.enums.TagTypeEnum; +import org.dromara.visor.module.monitor.api.MonitorHostApi; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -116,13 +120,19 @@ public class HostServiceImpl implements HostService { @Resource private DataExtraApi dataExtraApi; + @Resource + private MonitorHostApi monitorHostApi; + @Override @Transactional(rollbackFor = Exception.class) public Long createHost(HostCreateRequest request) { log.info("HostService-createHost request: {}", JSON.toJSONString(request)); // 转换 HostDO record = HostConvert.MAPPER.to(request); + // 设置默认值 record.setStatus(HostStatusEnum.ENABLED.name()); + record.setAgentKey(UUIds.random32()); + record.setAgentInstallStatus(AgentInstallStatusEnum.NOT_INSTALL.getStatus()); // 查询数据是否冲突 this.checkHostNamePresent(record); this.checkHostCodePresent(record); @@ -230,7 +240,13 @@ public class HostServiceImpl implements HostService { @Override @SneakyThrows - public HostVO getHostById(Long id) { + public HostVO getHostById(Long id, Boolean base) { + // 查询主机基础信息 + if (Booleans.isTrue(base)) { + HostDO record = hostDAO.selectById(id); + Valid.notNull(record, ErrorMessage.HOST_ABSENT); + return HostConvert.MAPPER.to(record); + } // 查询 tag 信息 Future> tagFuture = tagRelApi.getRelTagsAsync(TagTypeEnum.HOST, id); // 查询分组信息 @@ -238,10 +254,13 @@ public class HostServiceImpl implements HostService { // 查询主机 HostDO record = hostDAO.selectById(id); Valid.notNull(record, ErrorMessage.HOST_ABSENT); + // 查询规格 + HostSpecExtraModel spec = hostExtraService.getHostExtra(Const.SYSTEM_USER_ID, id, HostExtraItemEnum.SPEC); // 转换 HostVO vo = HostConvert.MAPPER.to(record); vo.setTags(tagFuture.get()); vo.setGroupIdList(groupIdFuture.get()); + vo.setSpec(spec); return vo; } @@ -283,12 +302,20 @@ public class HostServiceImpl implements HostService { if (wrapper == null) { return DataGrid.of(Lists.empty()); } - // 查询 - DataGrid hosts = hostDAO.of() + // 完整条件 + DataQuery query = hostDAO.of() .wrapper(wrapper) - .page(request) - .order(request, HostDO::getId) - .dataGrid(HostConvert.MAPPER::to); + .page(request); + if (Booleans.isTrue(request.getOrderByAgent())) { + // 通过 agentInstallStatus 进行排序 + query.order(false, HostDO::getAgentInstallStatus); + query.order(false, HostDO::getAgentOnlineStatus); + } else { + // 通过 id 进行排序 + query.order(request, HostDO::getId); + } + // 查询数据 + DataGrid hosts = query.dataGrid(HostConvert.MAPPER::to); // 查询拓展信息 this.setExtraInfo(request, hosts.getRows()); return hosts; @@ -343,6 +370,8 @@ public class HostServiceImpl implements HostService { favoriteApi.deleteByRelIdList(FavoriteTypeEnum.HOST, idList); // 删除额外配置 dataExtraApi.deleteByRelIdList(DataExtraTypeEnum.HOST, idList); + // 删除监控主机 + monitorHostApi.deleteByHostIdList(idList); } @Override @@ -396,9 +425,13 @@ public class HostServiceImpl implements HostService { } // 基础条件 wrapper.eq(HostDO::getId, request.getId()) + .in(HostDO::getId, request.getIdList()) .eq(HostDO::getOsType, request.getOsType()) .eq(HostDO::getArchType, request.getArchType()) .eq(HostDO::getStatus, request.getStatus()) + .eq(HostDO::getAgentKey, request.getAgentKey()) + .eq(HostDO::getAgentInstallStatus, request.getAgentInstallStatus()) + .eq(HostDO::getAgentOnlineStatus, request.getAgentOnlineStatus()) .like(HostDO::getName, request.getName()) .like(HostDO::getCode, request.getCode()) .like(HostDO::getAddress, request.getAddress()) diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/task/AgentHeartbeatCheckTask.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/task/AgentHeartbeatCheckTask.java new file mode 100644 index 00000000..dbad8171 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/task/AgentHeartbeatCheckTask.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.asset.task; + +import org.dromara.visor.module.asset.service.HostAgentEndpointService; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 探针心跳检查任务 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/4/15 23:50 + */ +@Component +public class AgentHeartbeatCheckTask { + + @Resource + private HostAgentEndpointService hostAgentEndpointService; + + /** + * 每分钟检测心跳 + */ + @Scheduled(initialDelay = 65000, fixedRate = 60000) + public void checkHeartbeat() { + hostAgentEndpointService.checkHeartbeat(); + } + +} diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/resources/mapper/HostAgentLogMapper.xml b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/resources/mapper/HostAgentLogMapper.xml new file mode 100644 index 00000000..003e5aeb --- /dev/null +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/resources/mapper/HostAgentLogMapper.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + id, host_id, agent_key, type, status, message, create_time, update_time, creator, updater, deleted + + + diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/resources/mapper/HostMapper.xml b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/resources/mapper/HostMapper.xml index 6e205c27..83e65934 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/resources/mapper/HostMapper.xml +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/resources/mapper/HostMapper.xml @@ -12,6 +12,11 @@ + + + + + @@ -22,7 +27,7 @@ - id, types, os_type, arch_type, name, code, address, status, description, create_time, update_time, creator, updater, deleted + id, types, os_type, arch_type, name, code, address, status, agent_key, agent_version, agent_install_status, agent_online_status, agent_online_change_time, description, create_time, update_time, creator, updater, deleted diff --git a/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/controller/UploadTaskController.java b/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/controller/UploadTaskController.java index d189fff1..eb622ca9 100644 --- a/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/controller/UploadTaskController.java +++ b/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/controller/UploadTaskController.java @@ -113,7 +113,8 @@ public class UploadTaskController { @Operation(summary = "查询上传状态") @Parameter(name = "id", description = "id", required = true) @PreAuthorize("@ss.hasPermission('exec:upload-task:query')") - public List getUploadTaskStatus(@RequestParam("idList") List idList, @RequestParam("queryFiles") Boolean queryFiles) { + public List getUploadTaskStatus(@RequestParam("idList") List idList, + @RequestParam("queryFiles") Boolean queryFiles) { return uploadTaskService.getUploadTaskStatus(idList, queryFiles); } diff --git a/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/define/ExecThreadPools.java b/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/define/ExecThreadPools.java index cfa7362b..81e78ae2 100644 --- a/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/define/ExecThreadPools.java +++ b/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/define/ExecThreadPools.java @@ -29,7 +29,7 @@ import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; /** - * 资产线程池 + * 执行线程池 * * @author Jiahang Li * @version 1.0.0 diff --git a/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/handler/exec/command/handler/BaseExecCommandHandler.java b/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/handler/exec/command/handler/BaseExecCommandHandler.java index 93d11313..1ae8fe8d 100644 --- a/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/handler/exec/command/handler/BaseExecCommandHandler.java +++ b/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/handler/exec/command/handler/BaseExecCommandHandler.java @@ -408,6 +408,8 @@ public abstract class BaseExecCommandHandler implements IExecCommandHandler { params.put("hostAddress", connectConfig.getHostAddress()); params.put("hostPort", connectConfig.getHostPort()); params.put("hostUsername", connectConfig.getUsername()); + // TODO 文档 + params.put("agentKey", connectConfig.getAgentKey()); params.put("hostUuid", uuid); params.put("hostUuidShort", uuid.replace("-", Strings.EMPTY)); params.put("osType", connectConfig.getOsType()); diff --git a/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/service/impl/UploadTaskServiceImpl.java b/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/service/impl/UploadTaskServiceImpl.java index 1ef5955f..072bb330 100644 --- a/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/service/impl/UploadTaskServiceImpl.java +++ b/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/service/impl/UploadTaskServiceImpl.java @@ -372,10 +372,9 @@ public class UploadTaskServiceImpl implements UploadTaskService { // 检查主机数量 Valid.eq(hosts.size(), hostIdList.size(), ErrorMessage.HOST_ABSENT); // 检查主机状态 - boolean allEnabled = hosts.stream() - .map(HostDTO::getStatus) - .allMatch(s -> HostStatusEnum.ENABLED.name().equals(s)); - Valid.isTrue(allEnabled, ErrorMessage.HOST_NOT_ENABLED); + for (HostDTO host : hosts) { + Valid.eq(HostStatusEnum.ENABLED.name(), host.getStatus(), ErrorMessage.HOST_NOT_ENABLED, host.getName()); + } return hosts; } diff --git a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-provider/src/main/java/org/dromara/visor/module/infra/api/SystemUserApi.java b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-provider/src/main/java/org/dromara/visor/module/infra/api/SystemUserApi.java index 07bdc5e8..e705fb68 100644 --- a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-provider/src/main/java/org/dromara/visor/module/infra/api/SystemUserApi.java +++ b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-provider/src/main/java/org/dromara/visor/module/infra/api/SystemUserApi.java @@ -33,6 +33,14 @@ import org.dromara.visor.module.infra.entity.dto.user.SystemUserDTO; */ public interface SystemUserApi { + /** + * 通过用户名查询 id + * + * @param username username + * @return id + */ + Long getIdByUsername(String username); + /** * 通过 id 查询用户名 * diff --git a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/api/impl/SystemUserApiImpl.java b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/api/impl/SystemUserApiImpl.java index de95ba5a..2745c662 100644 --- a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/api/impl/SystemUserApiImpl.java +++ b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/api/impl/SystemUserApiImpl.java @@ -44,6 +44,16 @@ public class SystemUserApiImpl implements SystemUserApi { @Resource private SystemUserDAO systemUserDAO; + @Override + public Long getIdByUsername(String username) { + return systemUserDAO.of() + .createWrapper() + .select(SystemUserDO::getId) + .eq(SystemUserDO::getUsername, username) + .then() + .getOne(SystemUserDO::getId); + } + @Override public String getUsernameById(Long id) { return systemUserDAO.of() diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-provider/pom.xml b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-provider/pom.xml new file mode 100644 index 00000000..222fb39f --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-provider/pom.xml @@ -0,0 +1,26 @@ + + + + org.dromara.visor + orion-visor-module-monitor + ${revision} + + + 4.0.0 + orion-visor-module-monitor-provider + jar + + 项目监控模块 + https://github.com/dromara/orion-visor + + + + + org.dromara.visor + orion-visor-common + + + + \ No newline at end of file diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-provider/src/main/java/org/dromara/visor/module/monitor/api/MonitorHostApi.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-provider/src/main/java/org/dromara/visor/module/monitor/api/MonitorHostApi.java new file mode 100644 index 00000000..ecfd6898 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-provider/src/main/java/org/dromara/visor/module/monitor/api/MonitorHostApi.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.api; + +import java.util.List; + +/** + * 监控主机对外服务 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/19 15:48 + */ +public interface MonitorHostApi { + + /** + * 设置探针为下线状态 + * + * @param agentKeyList agentKeyList + */ + void setAgentOffline(List agentKeyList); + + /** + * 删除监控主机 + * + * @param hostIdList hostIdList + * @return effect + */ + Integer deleteByHostIdList(List hostIdList); + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-provider/src/main/java/org/dromara/visor/module/monitor/entity/dto/.gitkeep b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-provider/src/main/java/org/dromara/visor/module/monitor/entity/dto/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/pom.xml b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/pom.xml new file mode 100644 index 00000000..2c112e37 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/pom.xml @@ -0,0 +1,110 @@ + + + + org.dromara.visor + orion-visor-module-monitor + ${revision} + + + 4.0.0 + orion-visor-module-monitor-service + jar + + 项目监控模块 + https://github.com/dromara/orion-visor + + + + + org.dromara.visor + orion-visor-common + + + + + org.dromara.visor + orion-visor-module-common + ${revision} + + + + + org.dromara.visor + orion-visor-module-infra-provider + ${revision} + + + org.dromara.visor + orion-visor-module-asset-provider + ${revision} + + + org.dromara.visor + orion-visor-module-monitor-provider + ${revision} + + + + + org.dromara.visor + orion-visor-spring-boot-starter-web + + + org.dromara.visor + orion-visor-spring-boot-starter-websocket + + + org.dromara.visor + orion-visor-spring-boot-starter-log + + + org.dromara.visor + orion-visor-spring-boot-starter-biz-operator-log + + + org.dromara.visor + orion-visor-spring-boot-starter-desensitize + + + org.dromara.visor + orion-visor-spring-boot-starter-security + + + org.dromara.visor + orion-visor-spring-boot-starter-redis + + + org.dromara.visor + orion-visor-spring-boot-starter-mybatis + + + org.dromara.visor + orion-visor-spring-boot-starter-storage + + + org.dromara.visor + orion-visor-spring-boot-starter-job + + + org.dromara.visor + orion-visor-spring-boot-starter-test + + + org.dromara.visor + orion-visor-spring-boot-starter-influxdb + + + + + + + + com.github.wvengen + proguard-maven-plugin + + + + + \ No newline at end of file diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/api/impl/MonitorHostApiImpl.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/api/impl/MonitorHostApiImpl.java new file mode 100644 index 00000000..29c6a93d --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/api/impl/MonitorHostApiImpl.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.api.impl; + +import cn.orionsec.kit.lang.utils.collect.Lists; +import lombok.extern.slf4j.Slf4j; +import org.dromara.visor.common.constant.Const; +import org.dromara.visor.module.monitor.api.MonitorHostApi; +import org.dromara.visor.module.monitor.dao.MonitorHostDAO; +import org.dromara.visor.module.monitor.define.context.MonitorContext; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 监控主机对外服务实现 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/19 15:48 + */ +@Slf4j +@Service +public class MonitorHostApiImpl implements MonitorHostApi { + + @Resource + private MonitorHostDAO monitorHostDAO; + + @Resource + private MonitorContext monitorContext; + + @Override + public void setAgentOffline(List agentKeyList) { + // 下线后删除指标 + agentKeyList.forEach(s -> monitorContext.setAgentMetrics(s, null)); + } + + @Override + public Integer deleteByHostIdList(List hostIdList) { + log.info("MonitorHostApi.deleteByHostIdList start hostIdList: {}", hostIdList); + if (Lists.isEmpty(hostIdList)) { + return Const.N_0; + } + // 通过 hostId 删除 + int effect = monitorHostDAO.deleteByHostIdList(hostIdList); + log.info("MonitorHostApi.deleteByHostIdList finish effect: {}", effect); + return effect; + } + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/constant/MetricsConst.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/constant/MetricsConst.java new file mode 100644 index 00000000..d4b16946 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/constant/MetricsConst.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.constant; + +/** + * 指标常量 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/15 17:23 + */ +public interface MetricsConst { + + String CPU_USER_SECONDS_TOTAL = "cpu_user_seconds_total"; + String CPU_SYSTEM_SECONDS_TOTAL = "cpu_system_seconds_total"; + String CPU_TOTAL_SECONDS_TOTAL = "cpu_total_seconds_total"; + + String MEM_USED_BYTES_TOTAL = "mem_used_bytes_total"; + String MEM_USED_PERCENT = "mem_used_percent"; + String MEM_SWAP_USED_BYTES_TOTAL = "mem_swap_used_bytes_total"; + String MEM_SWAP_USED_PERCENT = "mem_swap_used_percent"; + + String LOAD1 = "load1"; + String LOAD5 = "load5"; + String LOAD15 = "load15"; + String LOAD1_CORE_RATIO = "load1_core_ratio"; + String LOAD5_CORE_RATIO = "load5_core_ratio"; + String LOAD15_CORE_RATIO = "load15_core_ratio"; + + String DISK_FS_USED_BYTES_TOTAL = "disk_fs_used_bytes_total"; + String DISK_FS_USED_PERCENT = "disk_fs_used_percent"; + String DISK_FS_INODES_USED_PERCENT = "disk_fs_inodes_used_percent"; + + String DISK_IO_READ_BYTES_TOTAL = "disk_io_read_bytes_total"; + String DISK_IO_WRITE_BYTES_TOTAL = "disk_io_write_bytes_total"; + String DISK_IO_READS_TOTAL = "disk_io_reads_total"; + String DISK_IO_WRITES_TOTAL = "disk_io_writes_total"; + String DISK_IO_READ_BYTES_PER_SECOND = "disk_io_read_bytes_per_second"; + String DISK_IO_WRITE_BYTES_PER_SECOND = "disk_io_write_bytes_per_second"; + String DISK_IO_READS_PER_SECOND = "disk_io_reads_per_second"; + String DISK_IO_WRITES_PER_SECOND = "disk_io_writes_per_second"; + + String NET_SENT_BYTES_TOTAL = "net_sent_bytes_total"; + String NET_RECV_BYTES_TOTAL = "net_recv_bytes_total"; + String NET_SENT_PACKETS_TOTAL = "net_sent_packets_total"; + String NET_RECV_PACKETS_TOTAL = "net_recv_packets_total"; + String NET_SENT_BYTES_PER_SECOND = "net_sent_bytes_per_second"; + String NET_RECV_BYTES_PER_SECOND = "net_recv_bytes_per_second"; + String NET_SENT_PACKETS_PER_SECOND = "net_sent_packets_per_second"; + String NET_RECV_PACKETS_PER_SECOND = "net_recv_packets_per_second"; + + String NET_TCP_CONNECTIONS = "net_tcp_connections"; + String NET_UDP_CONNECTIONS = "net_udp_connections"; + String NET_INET_CONNECTIONS = "net_inet_connections"; + String NET_ALL_CONNECTIONS = "net_all_connections"; + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/controller/MonitorAgentEndpointController.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/controller/MonitorAgentEndpointController.java new file mode 100644 index 00000000..b1e5fba2 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/controller/MonitorAgentEndpointController.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.dromara.visor.common.constant.CustomHeaderConst; +import org.dromara.visor.framework.log.core.annotation.IgnoreLog; +import org.dromara.visor.framework.log.core.enums.IgnoreLogMode; +import org.dromara.visor.framework.web.core.annotation.ExposeApi; +import org.dromara.visor.framework.web.core.annotation.RestWrapper; +import org.dromara.visor.module.monitor.entity.dto.HostMetaDTO; +import org.dromara.visor.module.monitor.entity.dto.MetricsDataDTO; +import org.dromara.visor.module.monitor.service.MonitorAgentEndpointService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; + +/** + * 监控探针端点 api + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/22 14:33 + */ +@Tag(name = "monitor - 监控探针端点") +@Slf4j +@Validated +@RestWrapper +@RestController +@RequestMapping("/monitor/agent-endpoint") +public class MonitorAgentEndpointController { + + @Resource + private MonitorAgentEndpointService monitorAgentEndpointService; + + @ExposeApi + @IgnoreLog(IgnoreLogMode.ALL) + @PostMapping("/metrics") + @Operation(summary = "上报指标数据") + public Boolean addMetrics(@RequestHeader(CustomHeaderConst.AGENT_KEY_HEADER) String key, + @RequestBody MetricsDataDTO data) { + monitorAgentEndpointService.addMetrics(key, data); + return true; + } + + @ExposeApi + @PostMapping("/sync-host-meta") + @Operation(summary = "上线时同步主机元数据") + public Boolean syncHostMeta(@RequestHeader(CustomHeaderConst.AGENT_KEY_HEADER) String key, + @RequestBody HostMetaDTO data) { + monitorAgentEndpointService.syncHostMeta(key, data); + return true; + } + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/controller/MonitorHostController.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/controller/MonitorHostController.java new file mode 100644 index 00000000..2e83692f --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/controller/MonitorHostController.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.controller; + +import cn.orionsec.kit.lang.define.wrapper.DataGrid; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.dromara.visor.common.entity.chart.TimeChartSeries; +import org.dromara.visor.common.validator.group.Key; +import org.dromara.visor.common.validator.group.Page; +import org.dromara.visor.framework.biz.operator.log.core.annotation.OperatorLog; +import org.dromara.visor.framework.log.core.annotation.IgnoreLog; +import org.dromara.visor.framework.log.core.enums.IgnoreLogMode; +import org.dromara.visor.framework.web.core.annotation.DemoDisableApi; +import org.dromara.visor.framework.web.core.annotation.RestWrapper; +import org.dromara.visor.module.monitor.define.operator.MonitorHostOperatorType; +import org.dromara.visor.module.monitor.entity.request.host.MonitorHostChartRequest; +import org.dromara.visor.module.monitor.entity.request.host.MonitorHostQueryRequest; +import org.dromara.visor.module.monitor.entity.request.host.MonitorHostSwitchUpdateRequest; +import org.dromara.visor.module.monitor.entity.request.host.MonitorHostUpdateRequest; +import org.dromara.visor.module.monitor.entity.vo.MonitorHostMetricsDataVO; +import org.dromara.visor.module.monitor.entity.vo.MonitorHostVO; +import org.dromara.visor.module.monitor.service.MonitorHostService; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 监控主机 api + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025-8-14 16:27 + */ +@Tag(name = "monitor - 监控主机服务") +@Slf4j +@Validated +@RestWrapper +@RestController +@RequestMapping("/monitor/monitor-host") +public class MonitorHostController { + + @Resource + private MonitorHostService monitorHostService; + + @IgnoreLog(IgnoreLogMode.RET) + @PostMapping("/query") + @Operation(summary = "分页查询监控主机") + @PreAuthorize("@ss.hasPermission('monitor:monitor-host:query')") + public DataGrid getMonitorHostPage(@Validated(Page.class) @RequestBody MonitorHostQueryRequest request) { + return monitorHostService.getMonitorHostPage(request); + } + + @IgnoreLog(IgnoreLogMode.RET) + @PostMapping("/metrics") + @Operation(summary = "查询监控指标") + @PreAuthorize("@ss.hasPermission('monitor:monitor-host:query')") + public List getMonitorHostMetrics(@Validated(Key.class) @RequestBody MonitorHostQueryRequest request) { + return monitorHostService.getMonitorHostMetrics(request.getAgentKeyList()); + } + + @IgnoreLog(IgnoreLogMode.RET) + @PostMapping("/chart") + @Operation(summary = "查询监控指标") + @PreAuthorize("@ss.hasPermission('monitor:monitor-host:query')") + public List getMonitorHostChart(@Validated @RequestBody MonitorHostChartRequest request) { + return monitorHostService.getMonitorHostChart(request); + } + + @DemoDisableApi + @OperatorLog(MonitorHostOperatorType.UPDATE) + @PutMapping("/update") + @Operation(summary = "更新监控主机") + @PreAuthorize("@ss.hasPermission('monitor:monitor-host:update')") + public Integer updateMonitorHost(@Validated @RequestBody MonitorHostUpdateRequest request) { + return monitorHostService.updateMonitorHostById(request); + } + + @DemoDisableApi + @OperatorLog(MonitorHostOperatorType.UPDATE_SWITCH) + @PutMapping("/update-switch") + @Operation(summary = "更新监控主机告警开关") + @PreAuthorize("@ss.hasAnyPermission('monitor:monitor-host:update', 'monitor:monitor-host:update-switch')") + public Integer updateMonitorHostAlarmSwitch(@Validated @RequestBody MonitorHostSwitchUpdateRequest request) { + return monitorHostService.updateMonitorHostAlarmSwitch(request); + } + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/controller/MonitorMetricsController.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/controller/MonitorMetricsController.java new file mode 100644 index 00000000..905c09b0 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/controller/MonitorMetricsController.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.controller; + +import cn.orionsec.kit.lang.define.wrapper.DataGrid; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.dromara.visor.common.validator.group.Page; +import org.dromara.visor.framework.biz.operator.log.core.annotation.OperatorLog; +import org.dromara.visor.framework.log.core.annotation.IgnoreLog; +import org.dromara.visor.framework.log.core.enums.IgnoreLogMode; +import org.dromara.visor.framework.web.core.annotation.DemoDisableApi; +import org.dromara.visor.framework.web.core.annotation.RestWrapper; +import org.dromara.visor.module.monitor.define.operator.MonitorMetricsOperatorType; +import org.dromara.visor.module.monitor.entity.request.metrics.MonitorMetricsCreateRequest; +import org.dromara.visor.module.monitor.entity.request.metrics.MonitorMetricsQueryRequest; +import org.dromara.visor.module.monitor.entity.request.metrics.MonitorMetricsUpdateRequest; +import org.dromara.visor.module.monitor.entity.vo.MonitorMetricsVO; +import org.dromara.visor.module.monitor.service.MonitorMetricsService; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 监控指标 api + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025-8-12 21:31 + */ +@Tag(name = "monitor - 监控指标服务") +@Slf4j +@Validated +@RestWrapper +@RestController +@RequestMapping("/monitor/monitor-metrics") +public class MonitorMetricsController { + + @Resource + private MonitorMetricsService monitorMetricsService; + + @DemoDisableApi + @OperatorLog(MonitorMetricsOperatorType.CREATE) + @PostMapping("/create") + @Operation(summary = "创建监控指标") + @PreAuthorize("@ss.hasPermission('monitor:monitor-metrics:create')") + public Long createMonitorMetrics(@Validated @RequestBody MonitorMetricsCreateRequest request) { + return monitorMetricsService.createMonitorMetrics(request); + } + + @DemoDisableApi + @OperatorLog(MonitorMetricsOperatorType.UPDATE) + @PutMapping("/update") + @Operation(summary = "更新监控指标") + @PreAuthorize("@ss.hasPermission('monitor:monitor-metrics:update')") + public Integer updateMonitorMetrics(@Validated @RequestBody MonitorMetricsUpdateRequest request) { + return monitorMetricsService.updateMonitorMetricsById(request); + } + + @IgnoreLog(IgnoreLogMode.RET) + @GetMapping("/list") + @Operation(summary = "查询全部监控指标") + @PreAuthorize("@ss.hasPermission('monitor:monitor-metrics:query')") + public List getMonitorMetricsList() { + return monitorMetricsService.getMonitorMetricsList(); + } + + @IgnoreLog(IgnoreLogMode.RET) + @PostMapping("/query") + @Operation(summary = "分页查询监控指标") + @PreAuthorize("@ss.hasPermission('monitor:monitor-metrics:query')") + public DataGrid getMonitorMetricsPage(@Validated(Page.class) @RequestBody MonitorMetricsQueryRequest request) { + return monitorMetricsService.getMonitorMetricsPage(request); + } + + @DemoDisableApi + @OperatorLog(MonitorMetricsOperatorType.DELETE) + @DeleteMapping("/delete") + @Operation(summary = "删除监控指标") + @Parameter(name = "id", description = "id", required = true) + @PreAuthorize("@ss.hasPermission('monitor:monitor-metrics:delete')") + public Integer deleteMonitorMetrics(@RequestParam("id") Long id) { + return monitorMetricsService.deleteMonitorMetricsById(id); + } + + @DemoDisableApi + @OperatorLog(MonitorMetricsOperatorType.DELETE) + @DeleteMapping("/batch-delete") + @Operation(summary = "批量删除监控指标") + @Parameter(name = "idList", description = "idList", required = true) + @PreAuthorize("@ss.hasPermission('monitor:monitor-metrics:delete')") + public Integer batchDeleteMonitorMetrics(@RequestParam("idList") List idList) { + return monitorMetricsService.deleteMonitorMetricsByIdList(idList); + } + +} + diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/convert/MonitorHostConvert.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/convert/MonitorHostConvert.java new file mode 100644 index 00000000..719be752 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/convert/MonitorHostConvert.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.convert; + +import org.dromara.visor.module.asset.entity.dto.host.HostDTO; +import org.dromara.visor.module.asset.entity.dto.host.HostQueryDTO; +import org.dromara.visor.module.monitor.entity.domain.MonitorHostDO; +import org.dromara.visor.module.monitor.entity.request.host.MonitorHostQueryRequest; +import org.dromara.visor.module.monitor.entity.request.host.MonitorHostUpdateRequest; +import org.dromara.visor.module.monitor.entity.vo.MonitorHostVO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * 监控主机 内部对象转换器 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025-8-14 16:27 + */ +@Mapper +public interface MonitorHostConvert { + + MonitorHostConvert MAPPER = Mappers.getMapper(MonitorHostConvert.class); + + MonitorHostDO to(MonitorHostUpdateRequest request); + + MonitorHostVO to(MonitorHostDO domain); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "hostId", source = "id") + MonitorHostVO to(HostDTO dto); + + HostQueryDTO toHostQuery(MonitorHostQueryRequest request); + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/convert/MonitorMetricsConvert.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/convert/MonitorMetricsConvert.java new file mode 100644 index 00000000..a3e7f862 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/convert/MonitorMetricsConvert.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.convert; + +import org.dromara.visor.module.monitor.entity.domain.MonitorMetricsDO; +import org.dromara.visor.module.monitor.entity.dto.MonitorMetricsCacheDTO; +import org.dromara.visor.module.monitor.entity.request.metrics.MonitorMetricsCreateRequest; +import org.dromara.visor.module.monitor.entity.request.metrics.MonitorMetricsQueryRequest; +import org.dromara.visor.module.monitor.entity.request.metrics.MonitorMetricsUpdateRequest; +import org.dromara.visor.module.monitor.entity.vo.MonitorMetricsVO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 监控指标 内部对象转换器 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025-8-12 21:31 + */ +@Mapper +public interface MonitorMetricsConvert { + + MonitorMetricsConvert MAPPER = Mappers.getMapper(MonitorMetricsConvert.class); + + MonitorMetricsDO to(MonitorMetricsCreateRequest request); + + MonitorMetricsDO to(MonitorMetricsUpdateRequest request); + + MonitorMetricsDO to(MonitorMetricsQueryRequest request); + + MonitorMetricsVO to(MonitorMetricsDO domain); + + List to(List list); + + MonitorMetricsVO to(MonitorMetricsCacheDTO cache); + + MonitorMetricsCacheDTO toCache(MonitorMetricsDO domain); + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/dao/MonitorHostDAO.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/dao/MonitorHostDAO.java new file mode 100644 index 00000000..bde3f647 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/dao/MonitorHostDAO.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.dao; + +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import org.apache.ibatis.annotations.Mapper; +import org.dromara.visor.framework.mybatis.core.mapper.IMapper; +import org.dromara.visor.framework.mybatis.core.query.Conditions; +import org.dromara.visor.module.monitor.entity.domain.MonitorHostDO; + +import java.util.List; + +/** + * 监控主机 Mapper 接口 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025-8-14 16:27 + */ +@Mapper +public interface MonitorHostDAO extends IMapper { + + /** + * 通过 hostIdList 查询 + * + * @param hostIdList hostIdList + * @return rows + */ + default List selectByHostIdList(List hostIdList) { + return this.of() + .createWrapper() + .in(MonitorHostDO::getHostId, hostIdList) + .then() + .list(); + } + + /** + * 通过 agentKey 查询 + * + * @param agentKey agentKey + * @return row + */ + default MonitorHostDO selectByAgentKey(String agentKey) { + return this.of() + .createWrapper() + .eq(MonitorHostDO::getAgentKey, agentKey) + .then() + .getOne(); + } + + /** + * 通过 hostId 查询 + * + * @param hostId hostId + * @return row + */ + default MonitorHostDO selectByHostId(Long hostId) { + return this.of() + .createWrapper() + .eq(MonitorHostDO::getHostId, hostId) + .then() + .getOne(); + } + + /** + * 通过 hostIdList 删除 + * + * @param hostIdList hostIdList + * @return effect + */ + default int deleteByHostIdList(List hostIdList) { + return this.delete(Conditions.in(MonitorHostDO::getHostId, hostIdList)); + } + + /** + * 设置 policyId 为 null + * + * @param policyId policyId + * @return effect + */ + default int setPolicyIdWithNull(Long policyId) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate() + .set(MonitorHostDO::getPolicyId, null) + .eq(MonitorHostDO::getPolicyId, policyId); + return this.update(null, updateWrapper); + } + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/dao/MonitorMetricsDAO.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/dao/MonitorMetricsDAO.java new file mode 100644 index 00000000..e956c5c4 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/dao/MonitorMetricsDAO.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.dao; + +import org.apache.ibatis.annotations.Mapper; +import org.dromara.visor.framework.mybatis.core.mapper.IMapper; +import org.dromara.visor.module.monitor.entity.domain.MonitorMetricsDO; + +/** + * 监控指标 Mapper 接口 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025-8-12 21:31 + */ +@Mapper +public interface MonitorMetricsDAO extends IMapper { + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/define/cache/MonitorMetricsCacheKeyDefine.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/define/cache/MonitorMetricsCacheKeyDefine.java new file mode 100644 index 00000000..2bc2e58a --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/define/cache/MonitorMetricsCacheKeyDefine.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.define.cache; + +import cn.orionsec.kit.lang.define.cache.key.CacheKeyBuilder; +import cn.orionsec.kit.lang.define.cache.key.CacheKeyDefine; +import cn.orionsec.kit.lang.define.cache.key.struct.RedisCacheStruct; +import org.dromara.visor.module.monitor.entity.dto.MonitorMetricsCacheDTO; + +import java.util.concurrent.TimeUnit; + +/** + * 监控指标缓存 key + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025-8-12 21:31 + */ +public interface MonitorMetricsCacheKeyDefine { + + CacheKeyDefine MONITOR_METRICS = new CacheKeyBuilder() + .key("monitor:metrics:list") + .desc("监控指标") + .type(MonitorMetricsCacheDTO.class) + .struct(RedisCacheStruct.HASH) + .timeout(8, TimeUnit.HOURS) + .build(); + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/define/context/MonitorContext.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/define/context/MonitorContext.java new file mode 100644 index 00000000..fbefa1b6 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/define/context/MonitorContext.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.define.context; + +import cn.orionsec.kit.lang.utils.Strings; +import com.alibaba.fastjson.JSON; +import lombok.extern.slf4j.Slf4j; +import org.dromara.visor.module.monitor.dao.MonitorHostDAO; +import org.dromara.visor.module.monitor.entity.domain.MonitorHostDO; +import org.dromara.visor.module.monitor.entity.dto.MetricsDataDTO; +import org.dromara.visor.module.monitor.entity.dto.MonitorHostConfigDTO; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 监控上下文 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/21 17:26 + */ +@Slf4j +@Component +public class MonitorContext { + + private static final int MAX_CACHE_TIME = 5 * 60 * 1000; // 5min + + private static final int CLEAN_INTERVAL = 60 * 1000; // 1min + + private static final ConcurrentHashMap LATEST_METRICS_CACHE = new ConcurrentHashMap<>(); + + private static final ConcurrentHashMap HOST_CONFIG_CACHE = new ConcurrentHashMap<>(); + + private long lastCleanTime; + + @Resource + private MonitorHostDAO monitorHostDAO; + + /** + * 初始化监控主机配置 + */ + @PostConstruct + public void initMonitorHostConfig() { + List hosts = monitorHostDAO.selectList(null); + for (MonitorHostDO host : hosts) { + String config = host.getMonitorConfig(); + if (Strings.isBlank(config)) { + continue; + } + // 设置配置缓存 + this.setMonitorHostConfig(host.getAgentKey(), JSON.parseObject(config, MonitorHostConfigDTO.class)); + } + } + + /** + * 设置指标信息 + * + * @param key key + * @param metrics metrics + */ + public void setAgentMetrics(String key, MetricsDataDTO metrics) { + if (metrics == null) { + LATEST_METRICS_CACHE.remove(key); + } else { + LATEST_METRICS_CACHE.put(key, metrics); + } + } + + /** + * 获取指标信息 + * + * @param key key + * @return metrics + */ + public MetricsDataDTO getAgentMetrics(String key) { + // 删除过期缓存 + long current = System.currentTimeMillis(); + if (current - lastCleanTime > CLEAN_INTERVAL) { + this.lastCleanTime = current; + LATEST_METRICS_CACHE.forEach((k, v) -> { + if (current - v.getTimestamp() > MAX_CACHE_TIME) { + LATEST_METRICS_CACHE.remove(k, v); + } + }); + } + return LATEST_METRICS_CACHE.get(key); + } + + /** + * 设置监控主机配置 + * + * @param agentKey agentKey + * @param config config + */ + public void setMonitorHostConfig(String agentKey, MonitorHostConfigDTO config) { + HOST_CONFIG_CACHE.put(agentKey, config); + } + + /** + * 获取监控主机配置 + * + * @param agentKey agentKey + * @return config + */ + public MonitorHostConfigDTO getMonitorHostConfig(String agentKey) { + return HOST_CONFIG_CACHE.get(agentKey); + } + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/define/operator/MonitorHostOperatorType.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/define/operator/MonitorHostOperatorType.java new file mode 100644 index 00000000..25850dc4 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/define/operator/MonitorHostOperatorType.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.define.operator; + +import org.dromara.visor.framework.biz.operator.log.core.annotation.Module; +import org.dromara.visor.framework.biz.operator.log.core.factory.InitializingOperatorTypes; +import org.dromara.visor.framework.biz.operator.log.core.model.OperatorType; + +import static org.dromara.visor.framework.biz.operator.log.core.enums.OperatorRiskLevel.M; + +/** + * 监控主机 操作日志类型 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025-8-14 16:27 + */ +@Module("monitor:monitor-host") +public class MonitorHostOperatorType extends InitializingOperatorTypes { + + public static final String UPDATE = "monitor-host:update"; + + public static final String UPDATE_SWITCH = "monitor-host:update-switch"; + + @Override + public OperatorType[] types() { + return new OperatorType[]{ + new OperatorType(M, UPDATE, "更新监控配置 ${name}"), + new OperatorType(M, UPDATE_SWITCH, "更新监控开关 ${name}${switch}"), + }; + } + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/define/operator/MonitorMetricsOperatorType.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/define/operator/MonitorMetricsOperatorType.java new file mode 100644 index 00000000..c50e902b --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/define/operator/MonitorMetricsOperatorType.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.define.operator; + +import org.dromara.visor.framework.biz.operator.log.core.annotation.Module; +import org.dromara.visor.framework.biz.operator.log.core.factory.InitializingOperatorTypes; +import org.dromara.visor.framework.biz.operator.log.core.model.OperatorType; + +import static org.dromara.visor.framework.biz.operator.log.core.enums.OperatorRiskLevel.*; + +/** + * 监控指标 操作日志类型 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025-8-12 21:31 + */ +@Module("monitor:monitor-metrics") +public class MonitorMetricsOperatorType extends InitializingOperatorTypes { + + public static final String CREATE = "monitor-metrics:create"; + + public static final String UPDATE = "monitor-metrics:update"; + + public static final String DELETE = "monitor-metrics:delete"; + + @Override + public OperatorType[] types() { + return new OperatorType[]{ + new OperatorType(L, CREATE, "创建监控指标 ${name}"), + new OperatorType(M, UPDATE, "更新监控指标 ${name}"), + new OperatorType(H, DELETE, "删除监控指标 ${count} 条"), + }; + } + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/domain/MonitorHostDO.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/domain/MonitorHostDO.java new file mode 100644 index 00000000..8216eb9d --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/domain/MonitorHostDO.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.entity.domain; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import org.dromara.visor.framework.mybatis.core.domain.BaseDO; + +/** + * 监控主机 实体对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025-8-14 16:27 + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@TableName(value = "monitor_host", autoResultMap = true) +@Schema(name = "MonitorHostDO", description = "监控主机 实体对象") +public class MonitorHostDO extends BaseDO { + + private static final long serialVersionUID = 1L; + + @Schema(description = "主机id") + @TableField("host_id") + private Long hostId; + + @Schema(description = "策略id") + @TableField("policy_id") + private Long policyId; + + @Schema(description = "agentKey") + @TableField("agent_key") + private String agentKey; + + @Schema(description = "告警开关") + @TableField("alarm_switch") + private Integer alarmSwitch; + + @Schema(description = "负责人id") + @TableField("owner_user_id") + private Long ownerUserId; + + @Schema(description = "负责人用户名") + @TableField("owner_username") + private String ownerUsername; + + @Schema(description = "监控元数据") + @TableField("monitor_meta") + private String monitorMeta; + + @Schema(description = "监控配置") + @TableField("monitor_config") + private String monitorConfig; + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/domain/MonitorMetricsDO.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/domain/MonitorMetricsDO.java new file mode 100644 index 00000000..01b6cd6f --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/domain/MonitorMetricsDO.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.entity.domain; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import org.dromara.visor.framework.mybatis.core.domain.BaseDO; + +/** + * 监控指标 实体对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025-8-12 21:31 + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@TableName(value = "monitor_metrics", autoResultMap = true) +@Schema(name = "MonitorMetricsDO", description = "监控指标 实体对象") +public class MonitorMetricsDO extends BaseDO { + + private static final long serialVersionUID = 1L; + + @Schema(description = "指标名称") + @TableField("name") + private String name; + + @Schema(description = "数据集") + @TableField("measurement") + private String measurement; + + @Schema(description = "指标项") + @TableField("value") + private String value; + + @Schema(description = "单位") + @TableField("unit") + private String unit; + + @Schema(description = "后缀") + @TableField("suffix") + private String suffix; + + @Schema(description = "指标描述") + @TableField("description") + private String description; + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/dto/HostMetaDTO.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/dto/HostMetaDTO.java new file mode 100644 index 00000000..0a40a6e1 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/dto/HostMetaDTO.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.entity.dto; + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * 主机元数据 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/11 22:01 + */ +@Data +public class HostMetaDTO implements Serializable { + + /** + * CPU 列表 + */ + private List cpus; + + /** + * 磁盘名称列表 + */ + private List disks; + + /** + * 网卡名称列表 + */ + private List nets; + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/dto/MetricsDTO.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/dto/MetricsDTO.java new file mode 100644 index 00000000..9419ca0c --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/dto/MetricsDTO.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.entity.dto; + +import com.alibaba.fastjson.JSONObject; +import lombok.Data; + +import java.io.Serializable; +import java.util.Map; + +/** + * 指标信息 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/11 22:00 + */ +@Data +public class MetricsDTO implements Serializable { + + /** + * 指标类型 + */ + private String type; + + /** + * 标签 + */ + private Map tags; + + /** + * 指标值 + */ + private JSONObject values; + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/dto/MetricsDataDTO.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/dto/MetricsDataDTO.java new file mode 100644 index 00000000..3d7a81a9 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/dto/MetricsDataDTO.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.entity.dto; + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * 指标数据 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/11 21:59 + */ +@Data +public class MetricsDataDTO implements Serializable { + + /** + * 时间戳 + */ + private Long timestamp; + + /** + * 指标 + */ + private List metrics; + +} + diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/dto/MonitorHostConfigDTO.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/dto/MonitorHostConfigDTO.java new file mode 100644 index 00000000..b1e4ec16 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/dto/MonitorHostConfigDTO.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.entity.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 监控配置业务对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/13 23:34 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(name = "MonitorHostConfigDTO", description = "监控配置业务对象") +public class MonitorHostConfigDTO implements Serializable { + + @Schema(description = "cpu索引名称") + private String cpuName; + + @Schema(description = "磁盘名称") + private String diskName; + + @Schema(description = "网卡名称") + private String networkName; + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/dto/MonitorHostMetaDTO.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/dto/MonitorHostMetaDTO.java new file mode 100644 index 00000000..a240c225 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/dto/MonitorHostMetaDTO.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.entity.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.List; + +/** + * 监控元数据业务对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/13 23:34 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(name = "MonitorHostMetaDTO", description = "监控元数据业务对象") +public class MonitorHostMetaDTO implements Serializable { + + @Schema(description = "cpu") + private List cpus; + + @Schema(description = "磁盘") + private List disks; + + @Schema(description = "网卡") + private List nets; + + @Schema(description = "内存大小") + private Long memoryBytes; + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/dto/MonitorMetricsCacheDTO.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/dto/MonitorMetricsCacheDTO.java new file mode 100644 index 00000000..acf2aec2 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/dto/MonitorMetricsCacheDTO.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.entity.dto; + +import cn.orionsec.kit.lang.define.cache.key.model.LongCacheIdModel; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.Date; + +/** + * 监控指标 缓存对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025-8-12 21:31 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(name = "MonitorMetricsCacheDTO", description = "监控指标 缓存对象") +public class MonitorMetricsCacheDTO implements LongCacheIdModel, Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "id") + private Long id; + + @Schema(description = "指标名称") + private String name; + + @Schema(description = "数据集") + private String measurement; + + @Schema(description = "指标项") + private String value; + + @Schema(description = "单位") + private String unit; + + @Schema(description = "后缀") + private String suffix; + + @Schema(description = "指标描述") + private String description; + + @Schema(description = "创建时间") + private Date createTime; + + @Schema(description = "修改时间") + private Date updateTime; + + @Schema(description = "创建人") + private String creator; + + @Schema(description = "修改人") + private String updater; + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/host/MonitorHostAgentConfigUpdateRequest.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/host/MonitorHostAgentConfigUpdateRequest.java new file mode 100644 index 00000000..0a5bc583 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/host/MonitorHostAgentConfigUpdateRequest.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.entity.request.host; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 监控主机配置更新请求 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/13 23:34 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(name = "MonitorHostAgentConfigUpdateRequest", description = "监控主机配置更新请求") +public class MonitorHostAgentConfigUpdateRequest implements Serializable { + + @Schema(description = "cpu索引名称") + private String cpuName; + + @Schema(description = "磁盘名称") + private String diskName; + + @Schema(description = "网卡名称") + private String networkName; + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/host/MonitorHostChartRequest.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/host/MonitorHostChartRequest.java new file mode 100644 index 00000000..a16903db --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/host/MonitorHostChartRequest.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.entity.request.host; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.dromara.visor.common.entity.BaseQueryRequest; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import java.util.List; + +/** + * 监控主机图表 查询请求对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025-8-14 16:27 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(name = "MonitorHostChartRequest", description = "监控主机图表 查询请求对象") +public class MonitorHostChartRequest extends BaseQueryRequest { + + @NotEmpty + @Schema(description = "agentKey") + private List agentKeys; + + @NotBlank + @Schema(description = "表") + private String measurement; + + @NotEmpty + @Schema(description = "字段") + private List fields; + + @NotBlank + @Schema(description = "时间窗口") + private String window; + + @NotBlank + @Schema(description = "聚合参数") + private String aggregate; + + @Schema(description = "聚合参数") + private Long start; + + @Schema(description = "聚合参数") + private Long end; + + @Schema(description = "区间") + private String range; + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/host/MonitorHostMetaSyncRequest.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/host/MonitorHostMetaSyncRequest.java new file mode 100644 index 00000000..542baf81 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/host/MonitorHostMetaSyncRequest.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.entity.request.host; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +/** + * 监控主机 同步元数据请求对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025-8-14 16:27 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(name = "MonitorHostMetaSyncRequest", description = "监控主机 同步元数据请求对象") +public class MonitorHostMetaSyncRequest implements Serializable { + + private static final long serialVersionUID = 1L; + + @NotNull + @Schema(description = "hostId") + private Long hostId; + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/host/MonitorHostQueryRequest.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/host/MonitorHostQueryRequest.java new file mode 100644 index 00000000..d920e405 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/host/MonitorHostQueryRequest.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.entity.request.host; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.dromara.visor.common.entity.BaseQueryRequest; +import org.dromara.visor.common.validator.group.Key; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.Size; +import java.util.List; + +/** + * 监控主机 查询请求对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025-8-14 16:27 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(name = "MonitorHostQueryRequest", description = "监控主机 查询请求对象") +public class MonitorHostQueryRequest extends BaseQueryRequest { + + @NotEmpty(groups = Key.class) + @Schema(description = "agentKey") + private List agentKeyList; + + @Schema(description = "搜索") + private String searchValue; + + @Schema(description = "告警开关") + private Integer alarmSwitch; + + @Schema(description = "策略id") + private Long policyId; + + @Schema(description = "负责人id") + private Long ownerUserId; + + @Size(max = 64) + @Schema(description = "主机名称") + private String name; + + @Size(max = 64) + @Schema(description = "主机编码") + private String code; + + @Size(max = 128) + @Schema(description = "主机地址") + private String address; + + @Schema(description = "探针安装状态") + private Integer agentInstallStatus; + + @Schema(description = "探针在线状态") + private Integer agentOnlineStatus; + + @Size(max = 255) + @Schema(description = "描述") + private String description; + + @Schema(description = "tag") + private List tags; + + @Schema(description = "是否查询分组信息") + private Boolean queryGroup; + + @Schema(description = "是否查询 tag 信息") + private Boolean queryTag; + + @Schema(description = "是否查询规格信息") + private Boolean querySpec; + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/host/MonitorHostSwitchUpdateRequest.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/host/MonitorHostSwitchUpdateRequest.java new file mode 100644 index 00000000..1a4b2453 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/host/MonitorHostSwitchUpdateRequest.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.entity.request.host; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +/** + * 监控主机 更新告警开关请求对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025-8-14 16:27 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(name = "MonitorHostSwitchUpdateRequest", description = "监控主机 更新告警开关请求对象") +public class MonitorHostSwitchUpdateRequest implements Serializable { + + private static final long serialVersionUID = 1L; + + @NotNull + @Schema(description = "id") + private Long id; + + @NotNull + @Schema(description = "告警开关") + private Integer alarmSwitch; + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/host/MonitorHostUpdatePolicyRequest.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/host/MonitorHostUpdatePolicyRequest.java new file mode 100644 index 00000000..8586f0c7 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/host/MonitorHostUpdatePolicyRequest.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.entity.request.host; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +/** + * 监控主机 更新策略请求对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025-8-14 16:27 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(name = "MonitorHostUpdatePolicyRequest", description = "监控主机 更新策略请求对象") +public class MonitorHostUpdatePolicyRequest implements Serializable { + + private static final long serialVersionUID = 1L; + + @NotNull + @Schema(description = "id") + private Long id; + + @NotNull + @Schema(description = "策略id") + private Long policyId; + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/host/MonitorHostUpdateRequest.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/host/MonitorHostUpdateRequest.java new file mode 100644 index 00000000..af90a9eb --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/host/MonitorHostUpdateRequest.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.entity.request.host; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +/** + * 监控主机 更新请求对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025-8-14 16:27 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(name = "MonitorHostUpdateRequest", description = "监控主机 更新请求对象") +public class MonitorHostUpdateRequest implements Serializable { + + private static final long serialVersionUID = 1L; + + @NotNull + @Schema(description = "id") + private Long id; + + @Schema(description = "策略id") + private Long policyId; + + @Schema(description = "告警开关") + private Integer alarmSwitch; + + @Schema(description = "负责人id") + private Long ownerUserId; + + @Schema(description = "负责人用户名") + private String ownerUsername; + + @Schema(description = "元数据-cpu索引名称") + private String cpuName; + + @Schema(description = "元数据-磁盘名称") + private String diskName; + + @Schema(description = "元数据-网卡名称") + private String networkName; + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/metrics/MonitorMetricsCreateRequest.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/metrics/MonitorMetricsCreateRequest.java new file mode 100644 index 00000000..27ab3300 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/metrics/MonitorMetricsCreateRequest.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.entity.request.metrics; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import java.io.Serializable; + +/** + * 监控指标 创建请求对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025-8-12 21:31 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(name = "MonitorMetricsCreateRequest", description = "监控指标 创建请求对象") +public class MonitorMetricsCreateRequest implements Serializable { + + private static final long serialVersionUID = 1L; + + @NotBlank + @Size(max = 64) + @Schema(description = "指标名称") + private String name; + + @NotBlank + @Size(max = 64) + @Schema(description = "数据集") + private String measurement; + + @NotBlank + @Size(max = 128) + @Schema(description = "指标项") + private String value; + + @NotBlank + @Size(max = 8) + @Schema(description = "单位") + private String unit; + + @Size(max = 32) + @Schema(description = "后缀") + private String suffix; + + @Size(max = 128) + @Schema(description = "指标描述") + private String description; + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/metrics/MonitorMetricsQueryRequest.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/metrics/MonitorMetricsQueryRequest.java new file mode 100644 index 00000000..10c6296c --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/metrics/MonitorMetricsQueryRequest.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.entity.request.metrics; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import org.dromara.visor.common.entity.BaseQueryRequest; + +import javax.validation.constraints.Size; + +/** + * 监控指标 查询请求对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025-8-12 21:31 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@Schema(name = "MonitorMetricsQueryRequest", description = "监控指标 查询请求对象") +public class MonitorMetricsQueryRequest extends BaseQueryRequest { + + @Size(max = 64) + @Schema(description = "指标名称") + private String name; + + @Size(max = 64) + @Schema(description = "数据集") + private String measurement; + + @Size(max = 128) + @Schema(description = "指标项") + private String value; + + @Size(max = 8) + @Schema(description = "单位") + private String unit; + + @Size(max = 32) + @Schema(description = "后缀") + private String suffix; + + @Size(max = 128) + @Schema(description = "指标描述") + private String description; + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/metrics/MonitorMetricsUpdateRequest.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/metrics/MonitorMetricsUpdateRequest.java new file mode 100644 index 00000000..d81a0bff --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/request/metrics/MonitorMetricsUpdateRequest.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.entity.request.metrics; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.io.Serializable; + +/** + * 监控指标 更新请求对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025-8-12 21:31 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(name = "MonitorMetricsUpdateRequest", description = "监控指标 更新请求对象") +public class MonitorMetricsUpdateRequest implements Serializable { + + private static final long serialVersionUID = 1L; + + @NotNull + @Schema(description = "id") + private Long id; + + @NotBlank + @Size(max = 64) + @Schema(description = "指标名称") + private String name; + + @NotBlank + @Size(max = 64) + @Schema(description = "数据集") + private String measurement; + + @NotBlank + @Size(max = 128) + @Schema(description = "指标项") + private String value; + + @NotBlank + @Size(max = 8) + @Schema(description = "单位") + private String unit; + + @Size(max = 32) + @Schema(description = "后缀") + private String suffix; + + @Size(max = 128) + @Schema(description = "指标描述") + private String description; + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/vo/MonitorHostMetricsDataVO.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/vo/MonitorHostMetricsDataVO.java new file mode 100644 index 00000000..965313de --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/vo/MonitorHostMetricsDataVO.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.entity.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 监控主机指标数据 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/15 16:34 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(name = "MonitorHostMetricsDataVO", description = "监控主机指标数据 视图响应对象") +public class MonitorHostMetricsDataVO { + + @Schema(description = "agentKey") + private String agentKey; + + @Schema(description = "是否无数据") + private Boolean noData; + + @Schema(description = "采集时间") + private Long timestamp; + + @Schema(description = "cpu名称") + private String cpuName; + + @Schema(description = "磁盘名称") + private String diskName; + + @Schema(description = "网卡名称") + private String networkName; + + @Schema(description = "cpu 使用率") + private Double cpuUsagePercent; + + @Schema(description = "内存使用率") + private Double memoryUsagePercent; + + @Schema(description = "内存使用量") + private Long memoryUsageBytes; + + @Schema(description = "load1") + private Double load1; + + @Schema(description = "load5") + private Double load5; + + @Schema(description = "load15") + private Double load15; + + @Schema(description = "磁盘使用率") + private Double diskUsagePercent; + + @Schema(description = "磁盘使用量") + private Long diskUsageBytes; + + @Schema(description = "网卡上行带宽速度") + private Double networkSentPreBytes; + + @Schema(description = "网卡下行带宽速度") + private Double networkRecvPreBytes; + + public static MonitorHostMetricsDataVO noData(String agentKey) { + return MonitorHostMetricsDataVO.builder() + .noData(true) + .agentKey(agentKey) + .build(); + } + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/vo/MonitorHostVO.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/vo/MonitorHostVO.java new file mode 100644 index 00000000..320bd34b --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/vo/MonitorHostVO.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.entity.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.dromara.visor.module.asset.entity.dto.host.HostAgentLogDTO; +import org.dromara.visor.module.infra.entity.dto.tag.TagDTO; +import org.dromara.visor.module.monitor.entity.dto.MonitorHostConfigDTO; +import org.dromara.visor.module.monitor.entity.dto.MonitorHostMetaDTO; + +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +/** + * 监控主机 视图响应对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025-8-14 16:27 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(name = "MonitorHostVO", description = "监控主机 视图响应对象") +public class MonitorHostVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "id") + private Long id; + + @Schema(description = "主机id") + private Long hostId; + + @Schema(description = "策略id") + private Long policyId; + + @Schema(description = "策略名称") + private String policyName; + + @Schema(description = "系统类型") + private String osType; + + @Schema(description = "主机名称") + private String name; + + @Schema(description = "主机编码") + private String code; + + @Schema(description = "主机地址") + private String address; + + @Schema(description = "主机状态") + private String status; + + @Schema(description = "agentKey") + private String agentKey; + + @Schema(description = "探针版本") + private String agentVersion; + + @Schema(description = "最新版本") + private String latestVersion; + + @Schema(description = "探针安装状态") + private Integer agentInstallStatus; + + @Schema(description = "探针在线状态") + private Integer agentOnlineStatus; + + @Schema(description = "上次切换在线状态时间") + private Date lastChangeOnlineTime; + + @Schema(description = "告警开关") + private Integer alarmSwitch; + + @Schema(description = "负责人id") + private Long ownerUserId; + + @Schema(description = "负责人用户名") + private String ownerUsername; + + @Schema(description = "tags") + private List tags; + + @Schema(description = "监控元数据") + private MonitorHostMetaDTO meta; + + @Schema(description = "监控配置") + private MonitorHostConfigDTO config; + + @Schema(description = "监控数据") + private MonitorHostMetricsDataVO metricsData; + + @Schema(description = "安装日志") + private HostAgentLogDTO installLog; + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/vo/MonitorMetricsVO.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/vo/MonitorMetricsVO.java new file mode 100644 index 00000000..be2b0dbb --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/entity/vo/MonitorMetricsVO.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.entity.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.Date; + +/** + * 监控指标 视图响应对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025-8-12 21:31 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(name = "MonitorMetricsVO", description = "监控指标 视图响应对象") +public class MonitorMetricsVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "id") + private Long id; + + @Schema(description = "指标名称") + private String name; + + @Schema(description = "数据集") + private String measurement; + + @Schema(description = "指标项") + private String value; + + @Schema(description = "单位") + private String unit; + + @Schema(description = "后缀") + private String suffix; + + @Schema(description = "指标描述") + private String description; + + @Schema(description = "创建时间") + private Date createTime; + + @Schema(description = "修改时间") + private Date updateTime; + + @Schema(description = "创建人") + private String creator; + + @Schema(description = "修改人") + private String updater; + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/enums/MeasurementFieldEnum.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/enums/MeasurementFieldEnum.java new file mode 100644 index 00000000..c8936655 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/enums/MeasurementFieldEnum.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.enums; + +import lombok.Getter; +import org.apache.commons.collections4.map.HashedMap; +import org.dromara.visor.module.monitor.constant.MetricsConst; + +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * 指标度量类型 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/14 10:27 + */ +@Getter +public enum MeasurementFieldEnum { + + /** + * cpu + */ + CPU("cpu", (s) -> { + s.accept(MetricsConst.CPU_USER_SECONDS_TOTAL, double.class); + s.accept(MetricsConst.CPU_SYSTEM_SECONDS_TOTAL, double.class); + s.accept(MetricsConst.CPU_TOTAL_SECONDS_TOTAL, double.class); + }), + + /** + * 内存 + */ + MEMORY("memory", s -> { + s.accept(MetricsConst.MEM_USED_BYTES_TOTAL, long.class); + s.accept(MetricsConst.MEM_USED_PERCENT, double.class); + s.accept(MetricsConst.MEM_SWAP_USED_BYTES_TOTAL, long.class); + s.accept(MetricsConst.MEM_SWAP_USED_PERCENT, double.class); + }), + + /** + * 负载 + */ + LOAD("load", s -> { + s.accept(MetricsConst.LOAD1, double.class); + s.accept(MetricsConst.LOAD5, double.class); + s.accept(MetricsConst.LOAD15, double.class); + s.accept(MetricsConst.LOAD1_CORE_RATIO, double.class); + s.accept(MetricsConst.LOAD5_CORE_RATIO, double.class); + s.accept(MetricsConst.LOAD15_CORE_RATIO, double.class); + }), + + /** + * 磁盘 + */ + DISK("disk", s -> { + s.accept(MetricsConst.DISK_FS_USED_BYTES_TOTAL, long.class); + s.accept(MetricsConst.DISK_FS_USED_PERCENT, double.class); + s.accept(MetricsConst.DISK_FS_INODES_USED_PERCENT, double.class); + }), + + /** + * io + */ + IO("io", s -> { + s.accept(MetricsConst.DISK_IO_READ_BYTES_TOTAL, long.class); + s.accept(MetricsConst.DISK_IO_WRITE_BYTES_TOTAL, long.class); + s.accept(MetricsConst.DISK_IO_READS_TOTAL, long.class); + s.accept(MetricsConst.DISK_IO_WRITES_TOTAL, long.class); + s.accept(MetricsConst.DISK_IO_READ_BYTES_PER_SECOND, double.class); + s.accept(MetricsConst.DISK_IO_WRITE_BYTES_PER_SECOND, double.class); + s.accept(MetricsConst.DISK_IO_READS_PER_SECOND, double.class); + s.accept(MetricsConst.DISK_IO_WRITES_PER_SECOND, double.class); + }), + + /** + * 网络 + */ + NETWORK("network", s -> { + s.accept(MetricsConst.NET_SENT_BYTES_TOTAL, long.class); + s.accept(MetricsConst.NET_RECV_BYTES_TOTAL, long.class); + s.accept(MetricsConst.NET_SENT_PACKETS_TOTAL, long.class); + s.accept(MetricsConst.NET_RECV_PACKETS_TOTAL, long.class); + s.accept(MetricsConst.NET_SENT_BYTES_PER_SECOND, double.class); + s.accept(MetricsConst.NET_RECV_BYTES_PER_SECOND, double.class); + s.accept(MetricsConst.NET_SENT_PACKETS_PER_SECOND, double.class); + s.accept(MetricsConst.NET_RECV_PACKETS_PER_SECOND, double.class); + }), + + /** + * 连接数 + */ + CONNECTIONS("connections", s -> { + s.accept(MetricsConst.NET_TCP_CONNECTIONS, int.class); + s.accept(MetricsConst.NET_UDP_CONNECTIONS, int.class); + s.accept(MetricsConst.NET_INET_CONNECTIONS, int.class); + s.accept(MetricsConst.NET_ALL_CONNECTIONS, int.class); + }), + + ; + + private final String measurement; + private final Map> fields; + + MeasurementFieldEnum(String measurement, Consumer>> register) { + this.measurement = measurement; + this.fields = new HashedMap<>(); + register.accept(this.fields::put); + } + + public static MeasurementFieldEnum of(String measurement) { + if (measurement == null) { + return null; + } + for (MeasurementFieldEnum e : values()) { + if (e.measurement.equals(measurement)) { + return e; + } + } + return null; + } + + /** + * 获取度量值类型 + * + * @param measurement measurement + * @param field field + * @return type + */ + public static Class getMetricsValueType(String measurement, String field) { + MeasurementFieldEnum m = of(measurement); + if (m == null) { + return null; + } + return m.getFields().get(field); + } + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/enums/MetricsAggregateEnum.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/enums/MetricsAggregateEnum.java new file mode 100644 index 00000000..edc58079 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/enums/MetricsAggregateEnum.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 监控指标聚合函数 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/14 10:15 + */ +@Getter +@AllArgsConstructor +public enum MetricsAggregateEnum { + + /** + * 平均值 + */ + MEAN("mean"), + + /** + * 最大值 + */ + MAX("max"), + + /** + * 最小值 + */ + MIN("min"), + + /** + * 总和 + */ + SUM("sum"), + + ; + + private final String fn; + + public static MetricsAggregateEnum of(String fn) { + if (fn == null) { + return MEAN; + } + for (MetricsAggregateEnum e : values()) { + if (e.fn.equals(fn)) { + return e; + } + } + return MEAN; + } + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/enums/MetricsUnitEnum.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/enums/MetricsUnitEnum.java new file mode 100644 index 00000000..dd6fed4a --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/enums/MetricsUnitEnum.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.enums; + +/** + * 指标单位枚举 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/14 10:15 + */ +public enum MetricsUnitEnum { + + /** + * 字节 + */ + BYTES, + + /** + * 比特 + */ + BITS, + + /** + * 次 + */ + COUNT, + + /** + * 秒 + */ + SECONDS, + + /** + * 百分比 + */ + PER, + + /** + * 字节/秒 + */ + BYTES_S, + + /** + * 比特/秒 + */ + BITS_S, + + /** + * 次/秒 + */ + COUNT_S, + + /** + * 文本 + */ + TEXT, + + /** + * 无 + */ + NONE, + + ; + + public static MetricsUnitEnum of(String name) { + if (name == null) { + return NONE; + } + for (MetricsUnitEnum unit : values()) { + if (unit.name().equals(name)) { + return unit; + } + } + return NONE; + } + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/enums/MonitorAlarmSwitchEnum.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/enums/MonitorAlarmSwitchEnum.java new file mode 100644 index 00000000..6d6f50e4 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/enums/MonitorAlarmSwitchEnum.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 监控告警开关枚举 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/14 11:23 + */ +@Getter +@AllArgsConstructor +public enum MonitorAlarmSwitchEnum { + + /** + * 关闭 + */ + OFF(0), + + /** + * 开启 + */ + ON(1), + + ; + + private final Integer value; + + public static MonitorAlarmSwitchEnum of(Integer value) { + if (value == null) { + return OFF; + } + for (MonitorAlarmSwitchEnum e : MonitorAlarmSwitchEnum.values()) { + if (value.equals(e.value)) { + return e; + } + } + return OFF; + } + + /** + * 是否开启 + * + * @param value value + * @return on + */ + public static boolean isOn(Integer value) { + return ON.value.equals(value); + } + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/service/MonitorAgentEndpointService.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/service/MonitorAgentEndpointService.java new file mode 100644 index 00000000..8bfe4dc5 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/service/MonitorAgentEndpointService.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.service; + +import org.dromara.visor.module.monitor.entity.dto.HostMetaDTO; +import org.dromara.visor.module.monitor.entity.dto.MetricsDataDTO; + +/** + * 监控探针端点 服务类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/22 14:42 + */ +public interface MonitorAgentEndpointService { + + /** + * 添加监控指标 + * + * @param agentKey agentKey + * @param data data + */ + void addMetrics(String agentKey, MetricsDataDTO data); + + /** + * 上线时同步元数据 + * + * @param agentKey agentKey + * @param meta meta + */ + void syncHostMeta(String agentKey, HostMetaDTO meta); + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/service/MonitorHostService.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/service/MonitorHostService.java new file mode 100644 index 00000000..f998bd7b --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/service/MonitorHostService.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.service; + +import cn.orionsec.kit.lang.define.wrapper.DataGrid; +import org.dromara.visor.common.entity.chart.TimeChartSeries; +import org.dromara.visor.module.monitor.entity.request.host.MonitorHostChartRequest; +import org.dromara.visor.module.monitor.entity.request.host.MonitorHostQueryRequest; +import org.dromara.visor.module.monitor.entity.request.host.MonitorHostSwitchUpdateRequest; +import org.dromara.visor.module.monitor.entity.request.host.MonitorHostUpdateRequest; +import org.dromara.visor.module.monitor.entity.vo.MonitorHostMetricsDataVO; +import org.dromara.visor.module.monitor.entity.vo.MonitorHostVO; + +import java.util.List; + +/** + * 监控主机 服务类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025-8-14 16:27 + */ +public interface MonitorHostService { + + /** + * 分页查询监控主机 + * + * @param request request + * @return rows + */ + DataGrid getMonitorHostPage(MonitorHostQueryRequest request); + + /** + * 获取监控主机指标数据 + * + * @param agentKeyList agentKeyList + * @return metrics + */ + List getMonitorHostMetrics(List agentKeyList); + + /** + * 获取监控主机图表数据 + * + * @param request request + * @return series + */ + List getMonitorHostChart(MonitorHostChartRequest request); + + /** + * 更新监控主机 + * + * @param request request + * @return effect + */ + Integer updateMonitorHostById(MonitorHostUpdateRequest request); + + /** + * 更新监控主机告警开关 + * + * @param request request + * @return effect + */ + Integer updateMonitorHostAlarmSwitch(MonitorHostSwitchUpdateRequest request); + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/service/MonitorMetricsService.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/service/MonitorMetricsService.java new file mode 100644 index 00000000..6d57af05 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/service/MonitorMetricsService.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.service; + +import cn.orionsec.kit.lang.define.wrapper.DataGrid; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.dromara.visor.module.monitor.entity.domain.MonitorMetricsDO; +import org.dromara.visor.module.monitor.entity.request.metrics.MonitorMetricsCreateRequest; +import org.dromara.visor.module.monitor.entity.request.metrics.MonitorMetricsQueryRequest; +import org.dromara.visor.module.monitor.entity.request.metrics.MonitorMetricsUpdateRequest; +import org.dromara.visor.module.monitor.entity.vo.MonitorMetricsVO; + +import java.util.List; + +/** + * 监控指标 服务类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025-8-12 21:31 + */ +public interface MonitorMetricsService { + + /** + * 创建监控指标 + * + * @param request request + * @return id + */ + Long createMonitorMetrics(MonitorMetricsCreateRequest request); + + /** + * 更新监控指标 + * + * @param request request + * @return effect + */ + Integer updateMonitorMetricsById(MonitorMetricsUpdateRequest request); + + /** + * 通过缓存查询监控指标 + * + * @return rows + */ + List getMonitorMetricsList(); + + /** + * 分页查询监控指标 + * + * @param request request + * @return rows + */ + DataGrid getMonitorMetricsPage(MonitorMetricsQueryRequest request); + + /** + * 通过值获取监控指标名称 + * + * @param value value + * @return name + */ + String getMetricName(String value); + + /** + * 删除监控指标 + * + * @param id id + * @return effect + */ + Integer deleteMonitorMetricsById(Long id); + + /** + * 批量删除监控指标 + * + * @param idList idList + * @return effect + */ + Integer deleteMonitorMetricsByIdList(List idList); + + /** + * 构建查询 wrapper + * + * @param request request + * @return wrapper + */ + LambdaQueryWrapper buildQueryWrapper(MonitorMetricsQueryRequest request); + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/service/impl/MonitorAgentEndpointServiceImpl.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/service/impl/MonitorAgentEndpointServiceImpl.java new file mode 100644 index 00000000..bb50261d --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/service/impl/MonitorAgentEndpointServiceImpl.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.service.impl; + +import cn.orionsec.kit.lang.able.Executable; +import cn.orionsec.kit.lang.utils.Strings; +import cn.orionsec.kit.lang.utils.collect.Lists; +import cn.orionsec.kit.lang.utils.collect.Maps; +import com.alibaba.fastjson.JSON; +import com.influxdb.client.domain.WritePrecision; +import com.influxdb.client.write.Point; +import lombok.extern.slf4j.Slf4j; +import org.dromara.visor.common.constant.Const; +import org.dromara.visor.common.constant.ErrorMessage; +import org.dromara.visor.common.utils.LockerUtils; +import org.dromara.visor.common.utils.Valid; +import org.dromara.visor.framework.influxdb.core.utils.InfluxdbUtils; +import org.dromara.visor.module.asset.api.HostApi; +import org.dromara.visor.module.asset.entity.dto.host.HostDTO; +import org.dromara.visor.module.infra.api.SystemUserApi; +import org.dromara.visor.module.monitor.dao.MonitorHostDAO; +import org.dromara.visor.module.monitor.define.context.MonitorContext; +import org.dromara.visor.module.monitor.entity.domain.MonitorHostDO; +import org.dromara.visor.module.monitor.entity.dto.HostMetaDTO; +import org.dromara.visor.module.monitor.entity.dto.MetricsDataDTO; +import org.dromara.visor.module.monitor.entity.dto.MonitorHostConfigDTO; +import org.dromara.visor.module.monitor.enums.MonitorAlarmSwitchEnum; +import org.dromara.visor.module.monitor.service.MonitorAgentEndpointService; +import org.dromara.visor.module.monitor.utils.MetricsUtils; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 监控探针端点 服务实现类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/22 14:42 + */ +@Slf4j +@Service +public class MonitorAgentEndpointServiceImpl implements MonitorAgentEndpointService { + + private static final String LOCK_KEY_PREFIX = "monitor:online:"; + + @Resource + private MonitorHostDAO monitorHostDAO; + + @Resource + private HostApi hostApi; + + @Resource + private SystemUserApi systemUserApi; + + @Resource + private MonitorContext monitorContext; + + @Override + @Async("metricsExecutor") + public void addMetrics(String agentKey, MetricsDataDTO data) { + log.info("MonitorAgentEndpointService.addMetrics start agentKey: {}", agentKey); + // 设置数据缓存 + monitorContext.setAgentMetrics(agentKey, data); + // 数据点 + List points = data.getMetrics() + .stream() + .map(s -> MetricsUtils.createPoint(s.getType(), s.getValues()) + .addTag(Const.KEY, agentKey) + .addTags(Maps.def(s.getTags(), Maps.empty())) + .time(data.getTimestamp(), WritePrecision.MS)) + .collect(Collectors.toList()); + // 写入数据点 + InfluxdbUtils.writePoints(points); + // TODO 告警 + + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void syncHostMeta(String agentKey, HostMetaDTO meta) { + // 同步逻辑 + Executable exec = () -> { + // 查询主机是否存在 + HostDTO host = hostApi.selectByAgentKey(agentKey); + Valid.notNull(host, ErrorMessage.HOST_ABSENT); + // 查询数据 + MonitorHostDO monitorHost = monitorHostDAO.selectByAgentKey(agentKey); + MonitorHostConfigDTO newConfig = null; + if (monitorHost == null) { + // 不存在则新增 + newConfig = this.getDefaultMonitorConfig(meta); + monitorHost = MonitorHostDO.builder() + .hostId(host.getId()) + .agentKey(agentKey) + .alarmSwitch(MonitorAlarmSwitchEnum.OFF.getValue()) + .monitorMeta(JSON.toJSONString(meta)) + .monitorConfig(JSON.toJSONString(newConfig)) + .creator(host.getCreator()) + .updater(host.getCreator()) + .build(); + // 设置负责人信息 + Long userId = systemUserApi.getIdByUsername(host.getCreator()); + if (userId != null) { + monitorHost.setOwnerUserId(userId); + monitorHost.setOwnerUsername(host.getCreator()); + } + monitorHostDAO.insert(monitorHost); + } else { + // 更新数据 + MonitorHostDO update = new MonitorHostDO(); + update.setId(monitorHost.getId()); + update.setMonitorMeta(JSON.toJSONString(meta)); + // 设置默认配置 + if (Strings.isBlank(monitorHost.getMonitorConfig())) { + newConfig = this.getDefaultMonitorConfig(meta); + update.setMonitorConfig(JSON.toJSONString(newConfig)); + } + monitorHostDAO.updateById(update); + } + // 设置配置缓存 + if (newConfig != null) { + monitorContext.setMonitorHostConfig(agentKey, newConfig); + } + }; + // 获取锁并执行同步逻辑 + LockerUtils.lockExecute(LOCK_KEY_PREFIX + agentKey, Const.MS_S_10, exec); + } + + /** + * 获取默认监控配置 + * + * @param meta meta + * @return config + */ + private MonitorHostConfigDTO getDefaultMonitorConfig(HostMetaDTO meta) { + return MonitorHostConfigDTO.builder() + .cpuName(Lists.first(meta.getCpus())) + .diskName(Lists.first(meta.getDisks())) + .networkName(Lists.first(meta.getNets())) + .build(); + } + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/service/impl/MonitorHostServiceImpl.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/service/impl/MonitorHostServiceImpl.java new file mode 100644 index 00000000..a87abc60 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/service/impl/MonitorHostServiceImpl.java @@ -0,0 +1,472 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.service.impl; + +import cn.orionsec.kit.lang.define.wrapper.DataGrid; +import cn.orionsec.kit.lang.function.Functions; +import cn.orionsec.kit.lang.utils.Objects1; +import cn.orionsec.kit.lang.utils.Strings; +import cn.orionsec.kit.lang.utils.collect.Lists; +import cn.orionsec.kit.lang.utils.collect.Maps; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import lombok.extern.slf4j.Slf4j; +import org.dromara.visor.common.constant.Const; +import org.dromara.visor.common.constant.ErrorMessage; +import org.dromara.visor.common.constant.ExtraFieldConst; +import org.dromara.visor.common.entity.chart.TimeChartSeries; +import org.dromara.visor.common.utils.Valid; +import org.dromara.visor.framework.biz.operator.log.core.utils.OperatorLogs; +import org.dromara.visor.framework.influxdb.core.query.FluxQueryBuilder; +import org.dromara.visor.framework.influxdb.core.utils.InfluxdbUtils; +import org.dromara.visor.module.asset.api.HostAgentApi; +import org.dromara.visor.module.asset.api.HostApi; +import org.dromara.visor.module.asset.entity.dto.host.HostAgentLogDTO; +import org.dromara.visor.module.asset.entity.dto.host.HostDTO; +import org.dromara.visor.module.asset.entity.dto.host.HostQueryDTO; +import org.dromara.visor.module.asset.enums.AgentOnlineStatusEnum; +import org.dromara.visor.module.infra.api.SystemUserApi; +import org.dromara.visor.module.monitor.constant.MetricsConst; +import org.dromara.visor.module.monitor.convert.MonitorHostConvert; +import org.dromara.visor.module.monitor.dao.MonitorHostDAO; +import org.dromara.visor.module.monitor.define.context.MonitorContext; +import org.dromara.visor.module.monitor.entity.domain.MonitorHostDO; +import org.dromara.visor.module.monitor.entity.dto.MetricsDTO; +import org.dromara.visor.module.monitor.entity.dto.MetricsDataDTO; +import org.dromara.visor.module.monitor.entity.dto.MonitorHostConfigDTO; +import org.dromara.visor.module.monitor.entity.dto.MonitorHostMetaDTO; +import org.dromara.visor.module.monitor.entity.request.host.MonitorHostChartRequest; +import org.dromara.visor.module.monitor.entity.request.host.MonitorHostQueryRequest; +import org.dromara.visor.module.monitor.entity.request.host.MonitorHostSwitchUpdateRequest; +import org.dromara.visor.module.monitor.entity.request.host.MonitorHostUpdateRequest; +import org.dromara.visor.module.monitor.entity.vo.MonitorHostMetricsDataVO; +import org.dromara.visor.module.monitor.entity.vo.MonitorHostVO; +import org.dromara.visor.module.monitor.enums.MeasurementFieldEnum; +import org.dromara.visor.module.monitor.enums.MonitorAlarmSwitchEnum; +import org.dromara.visor.module.monitor.service.MonitorHostService; +import org.dromara.visor.module.monitor.service.MonitorMetricsService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 监控主机 服务实现类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025-8-14 16:27 + */ +@Slf4j +@Service +public class MonitorHostServiceImpl implements MonitorHostService { + + @Resource + private MonitorHostDAO monitorHostDAO; + + @Resource + private HostApi hostApi; + + @Resource + private HostAgentApi hostAgentApi; + + @Resource + private SystemUserApi systemUserApi; + + @Resource + private MonitorMetricsService monitorMetricsService; + + @Resource + private MonitorContext monitorContext; + + @Override + public DataGrid getMonitorHostPage(MonitorHostQueryRequest request) { + // 转换查询条件 + HostQueryDTO hostQuery = MonitorHostConvert.MAPPER.toHostQuery(request); + hostQuery.setQueryTag(true); + hostQuery.setOrderByAgent(true); + List monitorHosts = null; + // 查询监控主机数据 + Integer alarmSwitch = request.getAlarmSwitch(); + Long ownerUserId = request.getOwnerUserId(); + Long policyId = request.getPolicyId(); + if (!Objects1.isAllNull(alarmSwitch, ownerUserId, policyId)) { + monitorHosts = monitorHostDAO.of() + .createValidateWrapper() + .eq(MonitorHostDO::getAlarmSwitch, alarmSwitch) + .eq(MonitorHostDO::getOwnerUserId, ownerUserId) + .eq(MonitorHostDO::getPolicyId, policyId) + .then() + .list(); + if (monitorHosts.isEmpty()) { + return new DataGrid<>(); + } + hostQuery.setIdList(Lists.map(monitorHosts, MonitorHostDO::getHostId)); + } + // 查询主机 + DataGrid hosts = hostApi.getHostPage(hostQuery); + if (hosts.isEmpty()) { + return new DataGrid<>(Lists.empty(), hosts.getTotal()); + } + List hostIdList = hosts.stream() + .map(HostDTO::getId) + .collect(Collectors.toList()); + // 若未查询过监控主机表则查询 + if (monitorHosts == null) { + monitorHosts = monitorHostDAO.selectByHostIdList(hostIdList); + } + Map monitorHostMap = monitorHosts.stream() + .collect(Collectors.toMap(MonitorHostDO::getHostId, + Function.identity(), + Functions.right())); + // TODO 查询策略名称 + + // 查询安装日志 + Map agentInstallLogMap = hostAgentApi.selectAgentInstallLog(hostIdList) + .stream() + .collect(Collectors.toMap(HostAgentLogDTO::getHostId, + Function.identity(), + Functions.right())); + String latestVersion = hostAgentApi.getAgentVersion(); + // 给主机进行赋值 + return hosts.map(s -> { + MonitorHostVO vo = MonitorHostConvert.MAPPER.to(s); + // 设置监控信息 + MonitorHostDO monitorHost = monitorHostMap.get(s.getId()); + if (monitorHost != null) { + vo.setId(monitorHost.getId()); + vo.setPolicyId(monitorHost.getPolicyId()); + vo.setAlarmSwitch(monitorHost.getAlarmSwitch()); + vo.setOwnerUserId(monitorHost.getOwnerUserId()); + vo.setOwnerUsername(monitorHost.getOwnerUsername()); + // 反序列化元数据 + vo.setMeta(JSON.parseObject(monitorHost.getMonitorMeta(), MonitorHostMetaDTO.class)); + // 反序列化配置 + vo.setConfig(JSON.parseObject(monitorHost.getMonitorConfig(), MonitorHostConfigDTO.class)); + } + // 设置安装日志 + vo.setInstallLog(agentInstallLogMap.get(s.getId())); + // 设置最新版本 + vo.setLatestVersion(latestVersion); + // 设置指标信息 + if (AgentOnlineStatusEnum.ONLINE.getValue().equals(vo.getAgentOnlineStatus())) { + vo.setMetricsData(this.getHostMetricsData(vo.getAgentKey(), vo.getConfig())); + } else { + vo.setMetricsData(MonitorHostMetricsDataVO.noData(vo.getAgentKey())); + } + return vo; + }); + } + + @Override + public List getMonitorHostMetrics(List agentKeyList) { + return agentKeyList.stream() + .map(s -> this.getHostMetricsData(s, null)) + .collect(Collectors.toList()); + } + + @Override + public List getMonitorHostChart(MonitorHostChartRequest request) { + List agentKeys = request.getAgentKeys(); + List fields = request.getFields(); + List seriesList = this.getChartSeries(request); + // 查询 agentKey 对应的名称 + Map cacheNameByAgentKey = hostAgentApi.getCacheNameByAgentKey(agentKeys); + // 封装数据 + for (TimeChartSeries series : seriesList) { + Map tags = series.getTags(); + Map sortedTags = new LinkedHashMap<>(); + String key = (String) tags.get(Const.KEY); + String field = monitorMetricsService.getMetricName((String) tags.get(Const.FIELD)); + tags.remove(Const.KEY); + tags.remove(Const.FIELD); + // 设置主机名称 + if (agentKeys.size() > 1) { + sortedTags.put(ExtraFieldConst.HOST_NAME, cacheNameByAgentKey.get(key)); + } + // 设置字段 + if (fields.size() > 1) { + sortedTags.put(ExtraFieldConst.FIELD, field); + } + sortedTags.putAll(tags); + // 为空需要添加 field (计算名称) + if (sortedTags.isEmpty()) { + sortedTags.put(ExtraFieldConst.FIELD, field); + } + series.setTags(sortedTags); + // 设置名称 + String name = sortedTags.values() + .stream() + .map(Objects::toString) + .collect(Collectors.joining("-")); + series.setName(name); + } + // 排序指标 + seriesList.sort(Comparator.comparing(TimeChartSeries::getName)); + return seriesList; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Integer updateMonitorHostById(MonitorHostUpdateRequest request) { + Long id = Valid.notNull(request.getId(), ErrorMessage.ID_MISSING); + Long policyId = request.getPolicyId(); + log.info("MonitorHostService-updateMonitorHostById id: {}, request: {}", id, JSON.toJSONString(request)); + // 查询数据 + MonitorHostDO monitorHost = monitorHostDAO.selectById(id); + Valid.notNull(monitorHost, ErrorMessage.DATA_ABSENT); + // 查询主机信息 + HostDTO host = hostApi.selectById(monitorHost.getHostId()); + Valid.notNull(host, ErrorMessage.HOST_ABSENT); + // 查询用户信息 + Optional.ofNullable(request.getOwnerUserId()) + .map(systemUserApi::getUsernameById) + .ifPresent(request::setOwnerUsername); + // 设置日志参数 + OperatorLogs.add(OperatorLogs.NAME, host.getName()); + // 查询策略是否存在 TODO + if (policyId != null) { + + } + // 转换 + MonitorHostDO updateRecord = MonitorHostConvert.MAPPER.to(request); + // 配置信息 + MonitorHostConfigDTO config = MonitorHostConfigDTO.builder() + .cpuName(request.getCpuName()) + .diskName(request.getDiskName()) + .networkName(request.getNetworkName()) + .build(); + updateRecord.setMonitorConfig(JSON.toJSONString(config)); + // 更新 + int effect = monitorHostDAO.updateById(updateRecord); + // 更新缓存 + monitorContext.setMonitorHostConfig(host.getAgentKey(), config); + log.info("MonitorHostService-updateMonitorHostById effect: {}", effect); + return effect; + } + + @Override + public Integer updateMonitorHostAlarmSwitch(MonitorHostSwitchUpdateRequest request) { + Long id = request.getId(); + MonitorAlarmSwitchEnum alarmSwitch = MonitorAlarmSwitchEnum.of(request.getAlarmSwitch()); + // 查询数据 + MonitorHostDO monitorHost = monitorHostDAO.selectById(id); + Valid.notNull(monitorHost, ErrorMessage.DATA_ABSENT); + // 查询主机信息 + HostDTO host = hostApi.selectById(monitorHost.getHostId()); + Valid.notNull(host, ErrorMessage.HOST_ABSENT); + // 设置日志参数 + OperatorLogs.add(OperatorLogs.NAME, host.getName()); + OperatorLogs.add(OperatorLogs.SWITCH, alarmSwitch.name()); + // 修改数据 + MonitorHostDO update = new MonitorHostDO(); + update.setId(id); + update.setAlarmSwitch(alarmSwitch.getValue()); + int effect = monitorHostDAO.updateById(update); + log.info("MonitorHostService-updateMonitorHostAlarmSwitch effect: {}", effect); + // todo 咋更新缓存 + return effect; + } + + /** + * 查询数据图表 + * + * @param request request + * @return values + */ + private List getChartSeries(MonitorHostChartRequest request) { + String measurement = request.getMeasurement(); + List agentKeys = request.getAgentKeys(); + List fields = request.getFields(); + // 获取配置信息 + List configList = agentKeys.stream() + .map(monitorContext::getMonitorHostConfig) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + // 查询主机信息 + FluxQueryBuilder query = InfluxdbUtils.query(); + // 设置时间 + String range = request.getRange(); + if (range != null) { + query.range(range); + } else { + Valid.notNull(request.getStart(), ErrorMessage.PARAM_MISSING); + Valid.notNull(request.getEnd(), ErrorMessage.PARAM_MISSING); + } + // 设置名称 + Set names = null; + if (MeasurementFieldEnum.CPU.getMeasurement().equals(measurement)) { + names = configList.stream() + .map(MonitorHostConfigDTO::getCpuName) + .collect(Collectors.toSet()); + } else if (MeasurementFieldEnum.DISK.getMeasurement().equals(measurement)) { + names = configList.stream() + .map(MonitorHostConfigDTO::getDiskName) + .collect(Collectors.toSet()); + } else if (MeasurementFieldEnum.NETWORK.getMeasurement().equals(measurement)) { + names = configList.stream() + .map(MonitorHostConfigDTO::getNetworkName) + .collect(Collectors.toSet()); + } + if (!Lists.isEmpty(names)) { + query.name(names); + } + // 设置其他查询条件 + String flux = query.measurement(measurement) + .key(agentKeys) + .fields(fields) + .aggregateWindow(request.getWindow(), request.getAggregate(), true) + .pretty() + .build(); + // 查询数据 + return InfluxdbUtils.querySeries(flux); + } + + /** + * 获取主机指标数据 + * + * @param agentKey agentKey + * @param config config + * @return data + */ + public MonitorHostMetricsDataVO getHostMetricsData(String agentKey, MonitorHostConfigDTO config) { + MetricsDataDTO metrics = monitorContext.getAgentMetrics(agentKey); + // 无数据 + if (metrics == null) { + return MonitorHostMetricsDataVO.noData(agentKey); + } + // 从缓存中获取配置 + if (config == null) { + config = monitorContext.getMonitorHostConfig(agentKey); + } + // 获取名称 + LinkedHashMap metricDefaultNameMap = metrics.getMetrics() + .stream() + .collect(Collectors.toMap( + MetricsDTO::getType, + s -> Strings.def(Maps.get(s.getTags(), Const.NAME)), + Functions.left(), + LinkedHashMap::new)); + String cpuName = Optional.ofNullable(config) + .map(MonitorHostConfigDTO::getCpuName) + .orElse(metricDefaultNameMap.get(MeasurementFieldEnum.CPU.getMeasurement())); + String diskName = Optional.ofNullable(config) + .map(MonitorHostConfigDTO::getDiskName) + .orElse(metricDefaultNameMap.get(MeasurementFieldEnum.DISK.getMeasurement())); + String networkName = Optional.ofNullable(config) + .map(MonitorHostConfigDTO::getNetworkName) + .orElse(metricDefaultNameMap.get(MeasurementFieldEnum.NETWORK.getMeasurement())); + // 指标缓存 + Map metricTypeMap = metrics.getMetrics() + .stream() + .collect(Collectors.toMap( + s -> s.getType() + "_" + Strings.def(Maps.get(s.getTags(), Const.NAME)), + MetricsDTO::getValues, + Functions.right(), + LinkedHashMap::new)); + MonitorHostMetricsDataVO data = MonitorHostMetricsDataVO.builder() + .noData(false) + .agentKey(agentKey) + .cpuName(cpuName) + .diskName(diskName) + .networkName(networkName) + .timestamp(metrics.getTimestamp()) + .build(); + // 组装指标 + this.setNamedMetricValue(metricTypeMap, MeasurementFieldEnum.CPU, + cpuName, MetricsConst.CPU_TOTAL_SECONDS_TOTAL, 0D, + JSONObject::getDouble, + data::setCpuUsagePercent); + this.setNamedMetricValue(metricTypeMap, MeasurementFieldEnum.MEMORY, + null, MetricsConst.MEM_USED_BYTES_TOTAL, 0L, + JSONObject::getLong, + data::setMemoryUsageBytes); + this.setNamedMetricValue(metricTypeMap, MeasurementFieldEnum.MEMORY, + null, MetricsConst.MEM_USED_PERCENT, 0D, + JSONObject::getDouble, + data::setMemoryUsagePercent); + this.setNamedMetricValue(metricTypeMap, MeasurementFieldEnum.LOAD, + null, MetricsConst.LOAD1, 0D, + JSONObject::getDouble, + data::setLoad1); + this.setNamedMetricValue(metricTypeMap, MeasurementFieldEnum.LOAD, + null, MetricsConst.LOAD5, 0D, + JSONObject::getDouble, + data::setLoad5); + this.setNamedMetricValue(metricTypeMap, MeasurementFieldEnum.LOAD, + null, MetricsConst.LOAD15, 0D, + JSONObject::getDouble, + data::setLoad15); + this.setNamedMetricValue(metricTypeMap, MeasurementFieldEnum.DISK, + diskName, MetricsConst.DISK_FS_USED_PERCENT, 0D, + JSONObject::getDouble, + data::setDiskUsagePercent); + this.setNamedMetricValue(metricTypeMap, MeasurementFieldEnum.DISK, + diskName, MetricsConst.DISK_FS_USED_BYTES_TOTAL, 0L, + JSONObject::getLong, + data::setDiskUsageBytes); + this.setNamedMetricValue(metricTypeMap, MeasurementFieldEnum.NETWORK, + networkName, MetricsConst.NET_SENT_BYTES_PER_SECOND, 0D, + JSONObject::getDouble, + data::setNetworkSentPreBytes); + this.setNamedMetricValue(metricTypeMap, MeasurementFieldEnum.NETWORK, + networkName, MetricsConst.NET_RECV_BYTES_PER_SECOND, 0D, + JSONObject::getDouble, + data::setNetworkRecvPreBytes); + return data; + } + + /** + * 设置指标值 + * + * @param metricTypeMap metricTypeMap + * @param type type + * @param name name + * @param field field + * @param defaultValue defaultValue + * @param getter getter + * @param setter setter + * @param T + */ + private void setNamedMetricValue(Map metricTypeMap, + MeasurementFieldEnum type, + String name, + String field, + T defaultValue, + BiFunction getter, + Consumer setter) { + // 获取值 + T value = Optional.of(type.getMeasurement() + "_" + Strings.def(name)) + .map(metricTypeMap::get) + .map(s -> getter.apply(s, field)) + .orElse(defaultValue); + // 设置值 + setter.accept(value); + } + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/service/impl/MonitorMetricsServiceImpl.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/service/impl/MonitorMetricsServiceImpl.java new file mode 100644 index 00000000..afc9de26 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/service/impl/MonitorMetricsServiceImpl.java @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.service.impl; + +import cn.orionsec.kit.lang.define.wrapper.DataGrid; +import cn.orionsec.kit.lang.utils.collect.Lists; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import lombok.extern.slf4j.Slf4j; +import org.dromara.visor.common.constant.Const; +import org.dromara.visor.common.constant.ErrorMessage; +import org.dromara.visor.common.utils.Valid; +import org.dromara.visor.framework.biz.operator.log.core.utils.OperatorLogs; +import org.dromara.visor.framework.redis.core.utils.RedisMaps; +import org.dromara.visor.framework.redis.core.utils.barrier.CacheBarriers; +import org.dromara.visor.module.monitor.convert.MonitorMetricsConvert; +import org.dromara.visor.module.monitor.dao.MonitorMetricsDAO; +import org.dromara.visor.module.monitor.define.cache.MonitorMetricsCacheKeyDefine; +import org.dromara.visor.module.monitor.entity.domain.MonitorMetricsDO; +import org.dromara.visor.module.monitor.entity.dto.MonitorMetricsCacheDTO; +import org.dromara.visor.module.monitor.entity.request.metrics.MonitorMetricsCreateRequest; +import org.dromara.visor.module.monitor.entity.request.metrics.MonitorMetricsQueryRequest; +import org.dromara.visor.module.monitor.entity.request.metrics.MonitorMetricsUpdateRequest; +import org.dromara.visor.module.monitor.entity.vo.MonitorMetricsVO; +import org.dromara.visor.module.monitor.service.MonitorMetricsService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 监控指标 服务实现类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025-8-12 21:31 + */ +@Slf4j +@Service +public class MonitorMetricsServiceImpl implements MonitorMetricsService { + + private final Map metricsNameMap = new HashMap<>(); + + @Resource + private MonitorMetricsDAO monitorMetricsDAO; + + @PostConstruct + public void init() { + // 加载指标名称关联 + List metricsList = monitorMetricsDAO.selectList(null); + for (MonitorMetricsDO metrics : metricsList) { + metricsNameMap.put(metrics.getValue(), metrics.getName()); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createMonitorMetrics(MonitorMetricsCreateRequest request) { + log.info("MonitorMetricsService-createMonitorMetrics request: {}", JSON.toJSONString(request)); + // 转换 + MonitorMetricsDO record = MonitorMetricsConvert.MAPPER.to(request); + // 查询数据是否冲突 + this.checkMonitorMetricsPresent(record); + // 插入 + int effect = monitorMetricsDAO.insert(record); + Long id = record.getId(); + // 删除缓存 + RedisMaps.delete(MonitorMetricsCacheKeyDefine.MONITOR_METRICS); + // 设置日志参数 + OperatorLogs.add(OperatorLogs.ID, id); + // 修改本地缓存 + metricsNameMap.remove(record.getValue()); + metricsNameMap.put(request.getValue(), request.getName()); + log.info("MonitorMetricsService-createMonitorMetrics id: {}, effect: {}", id, effect); + return id; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Integer updateMonitorMetricsById(MonitorMetricsUpdateRequest request) { + Long id = Valid.notNull(request.getId(), ErrorMessage.ID_MISSING); + log.info("MonitorMetricsService-updateMonitorMetricsById id: {}, request: {}", id, JSON.toJSONString(request)); + // 查询 + MonitorMetricsDO record = monitorMetricsDAO.selectById(id); + Valid.notNull(record, ErrorMessage.DATA_ABSENT); + // 转换 + MonitorMetricsDO updateRecord = MonitorMetricsConvert.MAPPER.to(request); + // 查询数据是否冲突 + this.checkMonitorMetricsPresent(updateRecord); + // 更新 + int effect = monitorMetricsDAO.updateById(updateRecord); + log.info("MonitorMetricsService-updateMonitorMetricsById effect: {}", effect); + // 删除缓存 + RedisMaps.delete(MonitorMetricsCacheKeyDefine.MONITOR_METRICS); + // 修改本地缓存 + metricsNameMap.put(request.getValue(), request.getName()); + return effect; + } + + @Override + public List getMonitorMetricsList() { + // 查询缓存 + List list = RedisMaps.valuesJson(MonitorMetricsCacheKeyDefine.MONITOR_METRICS); + if (list.isEmpty()) { + // 查询数据库 + list = monitorMetricsDAO.of().list(MonitorMetricsConvert.MAPPER::toCache); + // 设置屏障 防止穿透 + CacheBarriers.checkBarrier(list, MonitorMetricsCacheDTO::new); + // 设置缓存 + RedisMaps.putAllJson(MonitorMetricsCacheKeyDefine.MONITOR_METRICS, s -> s.getId().toString(), list); + } + // 删除屏障 + CacheBarriers.removeBarrier(list); + // 转换 + return list.stream() + .map(MonitorMetricsConvert.MAPPER::to) + .sorted(Comparator.comparing(MonitorMetricsVO::getId).reversed()) + .collect(Collectors.toList()); + } + + @Override + public DataGrid getMonitorMetricsPage(MonitorMetricsQueryRequest request) { + // 条件 + LambdaQueryWrapper wrapper = this.buildQueryWrapper(request); + // 查询 + return monitorMetricsDAO.of() + .wrapper(wrapper) + .page(request) + .order(request, MonitorMetricsDO::getId) + .dataGrid(MonitorMetricsConvert.MAPPER::to); + } + + @Override + public String getMetricName(String value) { + return metricsNameMap.getOrDefault(value, value); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Integer deleteMonitorMetricsById(Long id) { + log.info("MonitorMetricsService-deleteMonitorMetricsById id: {}", id); + // 检查数据是否存在 + MonitorMetricsDO record = monitorMetricsDAO.selectById(id); + Valid.notNull(record, ErrorMessage.DATA_ABSENT); + // 删除 + int effect = monitorMetricsDAO.deleteById(id); + // 删除缓存 + RedisMaps.delete(MonitorMetricsCacheKeyDefine.MONITOR_METRICS, id); + // 设置日志参数 + OperatorLogs.add(OperatorLogs.COUNT, effect); + log.info("MonitorMetricsService-deleteMonitorMetricsById id: {}, effect: {}", id, effect); + return effect; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Integer deleteMonitorMetricsByIdList(List idList) { + if (Lists.isEmpty(idList)) { + OperatorLogs.add(OperatorLogs.COUNT, Const.N_0); + return Const.N_0; + } + log.info("MonitorMetricsService-deleteMonitorMetricsByIdList idList: {}", idList); + int effect = monitorMetricsDAO.deleteBatchIds(idList); + // 删除缓存 + RedisMaps.delete(MonitorMetricsCacheKeyDefine.MONITOR_METRICS, idList); + // 设置日志参数 + OperatorLogs.add(OperatorLogs.COUNT, effect); + log.info("MonitorMetricsService-deleteMonitorMetricsByIdList effect: {}", effect); + return effect; + } + + @Override + public LambdaQueryWrapper buildQueryWrapper(MonitorMetricsQueryRequest request) { + return monitorMetricsDAO.wrapper() + .eq(MonitorMetricsDO::getName, request.getName()) + .eq(MonitorMetricsDO::getMeasurement, request.getMeasurement()) + .eq(MonitorMetricsDO::getValue, request.getValue()) + .eq(MonitorMetricsDO::getDescription, request.getDescription()); + } + + /** + * 检查对象是否存在 + * + * @param domain domain + */ + private void checkMonitorMetricsPresent(MonitorMetricsDO domain) { + // 构造条件 + LambdaQueryWrapper wrapper = monitorMetricsDAO.wrapper() + // 更新时忽略当前记录 + .ne(MonitorMetricsDO::getId, domain.getId()) + // 用其他字段做重复校验 + .eq(MonitorMetricsDO::getName, domain.getName()) + .eq(MonitorMetricsDO::getMeasurement, domain.getMeasurement()) + .eq(MonitorMetricsDO::getValue, domain.getValue()); + // 检查是否存在 + boolean present = monitorMetricsDAO.of(wrapper).present(); + Valid.isFalse(present, ErrorMessage.DATA_PRESENT); + } + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/utils/MetricsUtils.java b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/utils/MetricsUtils.java new file mode 100644 index 00000000..419b3286 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/utils/MetricsUtils.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.monitor.utils; + +import cn.orionsec.kit.lang.utils.Strings; +import com.alibaba.fastjson.JSONObject; +import com.influxdb.client.write.Point; +import org.dromara.visor.module.monitor.enums.MeasurementFieldEnum; + +/** + * 指标值工具类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/8/11 22:46 + */ +public class MetricsUtils { + + private MetricsUtils() { + } + + /** + * 设置值 + * + * @param type type + * @param values values + * @return point + */ + public static Point createPoint(String type, JSONObject values) { + // 创建数据点 + Point point = Point.measurement(type); + // 设置数据值 + for (String field : values.keySet()) { + // 数据类型 + Class dataType = MeasurementFieldEnum.getMetricsValueType(type, field); + if (dataType == null) { + continue; + } + // 过滤数据 + String str = values.getString(field); + if (Strings.isBlank(str)) { + continue; + } + // 转换数据类型 + if (dataType == byte.class || dataType == short.class || dataType == int.class || dataType == long.class) { + point.addField(field, values.getLongValue(field)); + } else if (dataType == float.class || dataType == double.class) { + point.addField(field, values.getDoubleValue(field)); + } else if (dataType == boolean.class) { + point.addField(field, values.getBoolean(field)); + } else if (dataType == String.class) { + point.addField(field, values.getString(field)); + } + } + return point; + } + +} diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/resources/mapper/MonitorHostMapper.xml b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/resources/mapper/MonitorHostMapper.xml new file mode 100644 index 00000000..eb7c0d19 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/resources/mapper/MonitorHostMapper.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + id, host_id, policy_id, agent_key, alarm_switch, owner_user_id, owner_username, monitor_meta, monitor_config, create_time, update_time, creator, updater, deleted + + + diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/resources/mapper/MonitorMetricsMapper.xml b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/resources/mapper/MonitorMetricsMapper.xml new file mode 100644 index 00000000..84e4f020 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/resources/mapper/MonitorMetricsMapper.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + id, name, measurement, value, unit, suffix, description, id, create_time, update_time, creator, updater, deleted + + + diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/test/java/org/dromara/visor/module/monitor/api/impl/.gitkeep b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/test/java/org/dromara/visor/module/monitor/api/impl/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/test/java/org/dromara/visor/module/monitor/service/impl/.gitkeep b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/test/java/org/dromara/visor/module/monitor/service/impl/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/test/resources/application-unit-test.yaml b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/test/resources/application-unit-test.yaml new file mode 100644 index 00000000..f349f31e --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/test/resources/application-unit-test.yaml @@ -0,0 +1,30 @@ +spring: + main: + lazy-initialization: true + banner-mode: OFF + datasource: + druid: + name: orion_visor + url: jdbc:h2:mem:memdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; + driver-class-name: org.h2.Driver + username: sa + password: + max-active: 1 + async-init: true + initial-size: 1 + test-while-idle: false + sql: + init: + schema-locations: + - classpath:/sql/create-table-h2-*.sql + redis: + host: 127.0.0.1 + port: 16379 + database: 0 + redisson: + threads: 2 + netty-threads: 2 + minimum-idle-size: 2 + +mybatis-plus: + lazy-initialization: true diff --git a/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/test/resources/sql/.gitkeep b/orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/test/resources/sql/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/orion-visor-modules/orion-visor-module-monitor/pom.xml b/orion-visor-modules/orion-visor-module-monitor/pom.xml new file mode 100644 index 00000000..d0effa82 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-monitor/pom.xml @@ -0,0 +1,23 @@ + + + + org.dromara.visor + orion-visor-modules + ${revision} + + + 4.0.0 + orion-visor-module-monitor + pom + + 项目监控模块 + https://github.com/dromara/orion-visor + + + orion-visor-module-monitor-provider + orion-visor-module-monitor-service + + + \ No newline at end of file diff --git a/orion-visor-modules/pom.xml b/orion-visor-modules/pom.xml index a9e26967..191903fe 100644 --- a/orion-visor-modules/pom.xml +++ b/orion-visor-modules/pom.xml @@ -21,6 +21,7 @@ orion-visor-module-asset orion-visor-module-exec orion-visor-module-terminal + orion-visor-module-monitor \ No newline at end of file diff --git a/orion-visor-ui/src/api/asset/host-agent.ts b/orion-visor-ui/src/api/asset/host-agent.ts new file mode 100644 index 00000000..d07c658d --- /dev/null +++ b/orion-visor-ui/src/api/asset/host-agent.ts @@ -0,0 +1,102 @@ +import axios from 'axios'; +import qs from 'query-string'; + +/** + * 主机探针状态 + */ +export interface HostAgentStatusResponse { + id: number; + agentVersion: string; + latestVersion: string; + agentInstallStatus: number; + agentOnlineStatus: number; +} + +/** + * 探针日志 + */ +export interface HostAgentLogResponse { + id: number; + hostId: number; + agentKey: string; + type: string; + status: string; + message: string; + createTime: number; + updateTime: number; + creator: string; + updater: string; + agentStatus: HostAgentStatusResponse; +} + +/** + * 安装探针请求 + */ +export interface HostInstallAgentRequest { + idList?: Array; +} + +/** + * 主机安装探针更新状态请求 + */ +export interface HostAgentInstallStatusUpdateRequest { + id?: number; + status?: string; + message?: string; +} + +/** + * 安装主机探针 + */ +export function installHostAgent(request: HostInstallAgentRequest) { + return axios.post('/asset/host-agent/install', request); +} + +/** + * 修改探针安装状态 + */ +export function updateAgentInstallStatus(request: HostAgentInstallStatusUpdateRequest) { + return axios.put('/asset/host-agent/update-install-status', request); +} + +/** + * 查询主机探针状态 + */ +export function getHostAgentStatus(idList: Array) { + return axios.get>('/asset/host-agent/status', { + params: { idList }, + promptBizErrorMessage: false, + promptRequestErrorMessage: false, + paramsSerializer: params => { + return qs.stringify(params, { arrayFormat: 'comma' }); + } + }); +} + +/** + * 查询探针安装状态 + */ +export function getAgentInstallLogStatus(idList: Array) { + return axios.get>('/asset/host-agent/install-status', { + params: { idList }, + promptBizErrorMessage: false, + promptRequestErrorMessage: false, + paramsSerializer: params => { + return qs.stringify(params, { arrayFormat: 'comma' }); + } + }); +} + +/** + * 上传探针发布包 + */ +export function uploadAgentRelease(file: File) { + const formData = new FormData(); + formData.append('file', file); + return axios.post('/asset/host-agent/upload-agent-release', formData, { + timeout: 120000, + headers: { + 'Content-Type': 'multipart/form-data' + }, + }); +} diff --git a/orion-visor-ui/src/api/asset/host-config.ts b/orion-visor-ui/src/api/asset/host-config.ts index 281d34a0..1de7a426 100644 --- a/orion-visor-ui/src/api/asset/host-config.ts +++ b/orion-visor-ui/src/api/asset/host-config.ts @@ -56,7 +56,6 @@ export interface HostVncConfig extends HostBaseConfig { identityId?: number; noUsername?: boolean; noPassword?: boolean; - portForwardId?: number; timezone?: string; clipboardEncoding?: string; } diff --git a/orion-visor-ui/src/api/asset/host-extra.ts b/orion-visor-ui/src/api/asset/host-extra.ts index ca235c83..c77a90d0 100644 --- a/orion-visor-ui/src/api/asset/host-extra.ts +++ b/orion-visor-ui/src/api/asset/host-extra.ts @@ -61,7 +61,7 @@ export interface HostSpecExtraModel { outBandwidth: number; publicIpAddresses: Array; privateIpAddresses: Array; - chargePerson: string; + ownerPerson: string; createdTime: number; expiredTime: number; items: Array<{ diff --git a/orion-visor-ui/src/api/asset/host.ts b/orion-visor-ui/src/api/asset/host.ts index a01dd48e..4da9acfb 100644 --- a/orion-visor-ui/src/api/asset/host.ts +++ b/orion-visor-ui/src/api/asset/host.ts @@ -69,6 +69,11 @@ export interface HostQueryBaseResponse { code: string; address: string; status: string; + agentKey: string; + agentVersion: string; + agentInstallStatus: number; + agentOnlineStatus: number; + agentOnlineChangeTime: number; description: string; createTime: number; updateTime: number; @@ -143,8 +148,8 @@ export function updateHostSpec(request: Partial) { /** * 查询主机 */ -export function getHost(id: number) { - return axios.get('/asset/host/get', { params: { id } }); +export function getHost(id: number, base = false) { + return axios.get('/asset/host/get', { params: { id, base } }); } /** diff --git a/orion-visor-ui/src/api/interceptor.ts b/orion-visor-ui/src/api/interceptor.ts index 2bf14d34..a2b6b146 100644 --- a/orion-visor-ui/src/api/interceptor.ts +++ b/orion-visor-ui/src/api/interceptor.ts @@ -6,7 +6,7 @@ import { getToken } from '@/utils/auth'; import { httpBaseUrl } from '@/utils/env'; import { reLoginTipsKey } from '@/types/symbol'; -axios.defaults.timeout = 10000; +axios.defaults.timeout = 15000; axios.defaults.setAuthorization = true; axios.defaults.promptBizErrorMessage = true; axios.defaults.promptRequestErrorMessage = true; diff --git a/orion-visor-ui/src/api/monitor/metrics.ts b/orion-visor-ui/src/api/monitor/metrics.ts new file mode 100644 index 00000000..1d5437d3 --- /dev/null +++ b/orion-visor-ui/src/api/monitor/metrics.ts @@ -0,0 +1,95 @@ +import type { TableData } from '@arco-design/web-vue'; +import type { DataGrid, OrderDirection, Pagination } from '@/types/global'; +import axios from 'axios'; + +/** + * 监控指标创建请求 + */ +export interface MetricsCreateRequest { + name?: string; + measurement?: string; + value?: string; + unit?: string; + suffix?: string; + description?: string; +} + +/** + * 监控指标更新请求 + */ +export interface MetricsUpdateRequest extends MetricsCreateRequest { + id?: number; +} + +/** + * 监控指标查询请求 + */ +export interface MetricsQueryRequest extends Pagination, OrderDirection { + searchValue?: string; + id?: number; + name?: string; + measurement?: string; + value?: string; + unit?: string; + suffix?: string; + description?: string; +} + +/** + * 监控指标查询响应 + */ +export interface MetricsQueryResponse extends TableData { + id: number; + name: string; + measurement: string; + value: string; + unit: string; + suffix: string; + description: string; + createTime: number; + updateTime: number; + creator: string; + updater: string; +} + +/** + * 创建监控指标 + */ +export function createMetrics(request: MetricsCreateRequest) { + return axios.post('/monitor/monitor-metrics/create', request); +} + +/** + * 更新监控指标 + */ +export function updateMetrics(request: MetricsUpdateRequest) { + return axios.put('/monitor/monitor-metrics/update', request); +} + +/** + * 查询监控指标 + */ +export function getMetrics(id: number) { + return axios.get('/monitor/monitor-metrics/get', { params: { id } }); +} + +/** + * 查询全部监控指标 + */ +export function getMetricsList() { + return axios.get>('/monitor/monitor-metrics/list'); +} + +/** + * 分页查询监控指标 + */ +export function getMetricsPage(request: MetricsQueryRequest) { + return axios.post>('/monitor/monitor-metrics/query', request); +} + +/** + * 删除监控指标 + */ +export function deleteMetrics(id: number) { + return axios.delete('/monitor/monitor-metrics/delete', { params: { id } }); +} diff --git a/orion-visor-ui/src/api/monitor/monitor-host.ts b/orion-visor-ui/src/api/monitor/monitor-host.ts new file mode 100644 index 00000000..34d62e3f --- /dev/null +++ b/orion-visor-ui/src/api/monitor/monitor-host.ts @@ -0,0 +1,168 @@ +import type { TableData } from '@arco-design/web-vue'; +import type { DataGrid, Pagination, TimeChartSeries } from '@/types/global'; +import type { HostAgentLogResponse } from '@/api/asset/host-agent'; +import axios from 'axios'; + +/** + * 监控主机更新请求 + */ +export interface MonitorHostUpdateRequest { + id?: number; + policyId?: number; + alarmSwitch?: number; + ownerUserId?: number; + cpuName?: string; + diskName?: string; + networkName?: string; +} + +/** + * 监控主机更新请求 + */ +export interface MonitorHostSwitchUpdateRequest { + id?: number; + alarmSwitch?: number; +} + +/** + * 监控主机查询请求 + */ +export interface MonitorHostQueryRequest extends Pagination { + agentKeyList?: Array; + searchValue?: string; + alarmSwitch?: number; + policyId?: number; + ownerUserId?: number; + name?: string; + code?: string; + address?: string; + agentKey?: string; + agentInstallStatus?: number; + agentOnlineStatus?: number; + description?: string; + tags?: Array; +} + +/** + * 监控主机图表查询请求 + */ +export interface MonitorHostChartRequest { + agentKeys?: Array; + measurement?: string; + fields?: Array; + window?: string; + aggregate?: string; + range?: string; + start?: string; + end?: string; +} + +/** + * 监控主机查询响应 + */ +export interface MonitorHostQueryResponse extends TableData { + id: number; + hostId: number; + policyId: number; + policyName: string; + osType: string; + name: string; + code: string; + address: string; + status: string; + agentKey: string; + agentVersion: string; + latestVersion: string; + agentInstallStatus: number; + agentOnlineStatus: number; + agentOnlineChangeTime: number; + alarmSwitch: number; + ownerUserId: number; + ownerUsername: string; + tags: Array<{ id: number, name: string }>; + meta: MonitorHostMeta; + config: MonitorHostConfig; + metricsData: MonitorHostMetricsData; + installLog: HostAgentLogResponse; +} + +/** + * 监控元数据 + */ +export interface MonitorHostMeta { + cpus: Array; + disks: Array; + nets: Array; + memoryBytes: number; +} + +/** + * 监控配置 + */ +export interface MonitorHostConfig { + cpuName: string; + diskName: string; + networkName: string; +} + +/** + * 监控数据 + */ +export interface MonitorHostMetricsData { + agentKey: string; + noData: boolean; + timestamp: number; + cpuName: string; + diskName: string; + networkName: string; + cpuUsagePercent: number; + memoryUsagePercent: number; + memoryUsageBytes: number; + load1: number; + load5: number; + load15: number; + diskUsagePercent: number; + diskUsageBytes: number; + networkSentPreBytes: number; + networkRecvPreBytes: number; +} + +/** + * 查询监控主机指标 + */ +export function getMonitorHostMetrics(agentKeyList: Array) { + return axios.post>('/monitor/monitor-host/metrics', { + agentKeyList + }, { + promptBizErrorMessage: false, + promptRequestErrorMessage: false, + }); +} + +/** + * 查询监控主机图表 + */ +export function getMonitorHostChart(request: MonitorHostChartRequest) { + return axios.post>('/monitor/monitor-host/chart', request); +} + +/** + * 分页查询监控主机 + */ +export function getMonitorHostPage(request: MonitorHostQueryRequest) { + return axios.post>('/monitor/monitor-host/query', request); +} + +/** + * 更新监控主机 + */ +export function updateMonitorHost(request: MonitorHostUpdateRequest) { + return axios.put('/monitor/monitor-host/update', request); +} + +/** + * 更新监控主机告警开关 + */ +export function updateMonitorHostAlarmSwitch(request: MonitorHostSwitchUpdateRequest) { + return axios.put('/monitor/monitor-host/update-switch', request); +} diff --git a/orion-visor-ui/src/api/terminal/terminal-sftp.ts b/orion-visor-ui/src/api/terminal/terminal-sftp.ts index 43f837fd..252de82e 100644 --- a/orion-visor-ui/src/api/terminal/terminal-sftp.ts +++ b/orion-visor-ui/src/api/terminal/terminal-sftp.ts @@ -20,7 +20,7 @@ export function setSftpFileContent(token: string, content: string) { formData.append('token', token); formData.append('file', new File([content], Date.now() + '', { type: 'text/plain' })); return axios.post('/terminal/terminal-sftp/set-content', formData, { - timeout: 60000, + timeout: 120000, headers: { 'Content-Type': 'multipart/form-data' } diff --git a/orion-visor-ui/src/assets/style/arco-extends.less b/orion-visor-ui/src/assets/style/arco-extends.less index 386470a9..2bdecfc7 100644 --- a/orion-visor-ui/src/assets/style/arco-extends.less +++ b/orion-visor-ui/src/assets/style/arco-extends.less @@ -31,19 +31,19 @@ background-color: rgb(var(--blue-6)); &.normal { - color: rgb(var(--arcoblue-6)); + background-color: rgb(var(--arcoblue-6)); } &.pass { - color: rgb(var(--green-6)); + background-color: rgb(var(--green-6)); } &.warn { - color: rgb(var(--orange-6)); + background-color: rgb(var(--orange-6)); } &.error { - color: rgb(var(--red-6)); + background-color: rgb(var(--red-6)); } } } diff --git a/orion-visor-ui/src/assets/style/chart.less b/orion-visor-ui/src/assets/style/chart.less new file mode 100644 index 00000000..844607a5 --- /dev/null +++ b/orion-visor-ui/src/assets/style/chart.less @@ -0,0 +1,49 @@ +// tooltip +.chart-tooltip-wrapper { + // 时间 + .chart-tooltip-time { + font-size: 13px; + } + + // 表头 + .chart-tooltip-header { + margin-top: 5px; + font-size: 12px; + line-height: 1.3; + + &-grid { + display: grid; + gap: 0 8px; + align-items: center; + } + + &-item { + font-weight: bold; + padding: 2px 0; + white-space: nowrap; + border-bottom: 1px solid #ddd; + } + } + + // 圆点 + .chart-tooltip-dot { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + margin-right: 5px; + } + + // 数据行 + .chart-tooltip-col { + font-weight: 500; + color: #000; + padding: 3px 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: flex; + align-items: center; + } + +} diff --git a/orion-visor-ui/src/assets/style/global.less b/orion-visor-ui/src/assets/style/global.less index 105f2ba8..3d82980c 100644 --- a/orion-visor-ui/src/assets/style/global.less +++ b/orion-visor-ui/src/assets/style/global.less @@ -282,10 +282,18 @@ body { margin-left: 4px; } +.mr2 { + margin-right: 2px; +} + .mr4 { margin-right: 4px; } +.mt2 { + margin-top: 2px; +} + .mt4 { margin-top: 4px; } diff --git a/orion-visor-ui/src/assets/style/layout.less b/orion-visor-ui/src/assets/style/layout.less index e7bbf85a..1930ceeb 100644 --- a/orion-visor-ui/src/assets/style/layout.less +++ b/orion-visor-ui/src/assets/style/layout.less @@ -172,7 +172,7 @@ } & > .arco-card-body { - padding: 0 16px 16px 16px; + padding: 0 16px 12px 16px; flex-grow: 1; } } @@ -230,6 +230,10 @@ } // -- doption +.arco-dropdown-option-disabled .arco-dropdown-option-content .more-doption { + color: var(--color-text-4); +} + .more-doption { min-width: 42px; padding: 0 4px; diff --git a/orion-visor-ui/src/components/app/setting/index.vue b/orion-visor-ui/src/components/app/setting/index.vue index dd3ac34e..b2c937a0 100644 --- a/orion-visor-ui/src/components/app/setting/index.vue +++ b/orion-visor-ui/src/components/app/setting/index.vue @@ -152,6 +152,15 @@ defaultVal: appStore.hostIdentityView, options: cardOptions, }, + { + name: '主机监控', + key: 'monitorHostView', + type: 'radio-group', + margin: '0 0 4px 0', + permission: ['monitor:monitor-host:query'], + defaultVal: appStore.monitorHostView, + options: cardOptions, + }, ]); // 是否展示创建 PWA 应用 diff --git a/orion-visor-ui/src/components/app/tab-bar/index.vue b/orion-visor-ui/src/components/app/tab-bar/index.vue index 2911802f..03e86b5a 100644 --- a/orion-visor-ui/src/components/app/tab-bar/index.vue +++ b/orion-visor-ui/src/components/app/tab-bar/index.vue @@ -6,6 +6,7 @@

@@ -18,7 +19,6 @@ + + + + diff --git a/orion-visor-ui/src/views/monitor/metrics/components/metrics-table.vue b/orion-visor-ui/src/views/monitor/metrics/components/metrics-table.vue new file mode 100644 index 00000000..61eca0d0 --- /dev/null +++ b/orion-visor-ui/src/views/monitor/metrics/components/metrics-table.vue @@ -0,0 +1,221 @@ + + + + + + + diff --git a/orion-visor-ui/src/views/monitor/metrics/index.vue b/orion-visor-ui/src/views/monitor/metrics/index.vue new file mode 100644 index 00000000..8876f0ee --- /dev/null +++ b/orion-visor-ui/src/views/monitor/metrics/index.vue @@ -0,0 +1,46 @@ + + + + + + + diff --git a/orion-visor-ui/src/views/monitor/metrics/types/const.ts b/orion-visor-ui/src/views/monitor/metrics/types/const.ts new file mode 100644 index 00000000..d14d05b0 --- /dev/null +++ b/orion-visor-ui/src/views/monitor/metrics/types/const.ts @@ -0,0 +1,10 @@ +export const TableName = 'monitor_metrics'; + +// 监控指标类型 字典项 +export const MeasurementKey = 'metricsMeasurement'; + +// 监控指标单位 字典项 +export const MetricsUnitKey = 'metricsUnit'; + +// 加载的字典值 +export const dictKeys = [MeasurementKey, MetricsUnitKey]; diff --git a/orion-visor-ui/src/views/monitor/metrics/types/form.rules.ts b/orion-visor-ui/src/views/monitor/metrics/types/form.rules.ts new file mode 100644 index 00000000..9bfc9f58 --- /dev/null +++ b/orion-visor-ui/src/views/monitor/metrics/types/form.rules.ts @@ -0,0 +1,52 @@ +import type { FieldRule } from '@arco-design/web-vue'; + +export const name = [{ + required: true, + message: '请输入指标名称' +}, { + maxLength: 64, + message: '指标名称长度不能大于64位' +}] as FieldRule[]; + +export const measurement = [{ + required: true, + message: '请输入数据集' +}, { + maxLength: 64, + message: '数据集长度不能大于64位' +}] as FieldRule[]; + +export const value = [{ + required: true, + message: '请输入指标项' +}, { + maxLength: 128, + message: '指标项长度不能大于128位' +}] as FieldRule[]; + +export const unit = [{ + required: true, + message: '请选择单位' +}] as FieldRule[]; + +export const suffix = [{ + required: true, + message: '请输入后缀文本' +}, { + maxLength: 32, + message: '后缀文本长度不能大于32位' +}] as FieldRule[]; + +export const description = [{ + maxLength: 128, + message: '指标描述长度不能大于128位' +}] as FieldRule[]; + +export default { + name, + measurement, + value, + unit, + suffix, + description, +} as Record; diff --git a/orion-visor-ui/src/views/monitor/metrics/types/table.columns.ts b/orion-visor-ui/src/views/monitor/metrics/types/table.columns.ts new file mode 100644 index 00000000..bfbdfce6 --- /dev/null +++ b/orion-visor-ui/src/views/monitor/metrics/types/table.columns.ts @@ -0,0 +1,98 @@ +import type { TableColumnData } from '@arco-design/web-vue'; +import { dateFormat } from '@/utils'; + +const columns = [ + { + title: 'id', + dataIndex: 'id', + slotName: 'id', + width: 68, + align: 'left', + fixed: 'left', + default: true, + }, { + title: '指标名称', + dataIndex: 'name', + slotName: 'name', + align: 'left', + width: 238, + ellipsis: true, + tooltip: true, + default: true, + }, { + title: '数据集', + dataIndex: 'measurement', + slotName: 'measurement', + align: 'left', + width: 148, + ellipsis: true, + tooltip: true, + default: true, + }, { + title: '指标项', + dataIndex: 'value', + slotName: 'value', + align: 'left', + minWidth: 288, + ellipsis: true, + tooltip: true, + default: true, + }, { + title: '指标单位', + dataIndex: 'unit', + slotName: 'unit', + align: 'left', + width: 168, + default: true, + }, { + title: '指标描述', + dataIndex: 'description', + slotName: 'description', + align: 'left', + ellipsis: true, + tooltip: true, + default: true, + }, { + title: '创建时间', + dataIndex: 'createTime', + slotName: 'createTime', + align: 'center', + width: 180, + render: ({ record }) => { + return dateFormat(new Date(record.createTime)); + }, + }, { + title: '修改时间', + dataIndex: 'updateTime', + slotName: 'updateTime', + align: 'center', + width: 180, + render: ({ record }) => { + return dateFormat(new Date(record.updateTime)); + }, + default: true, + }, { + title: '创建人', + dataIndex: 'creator', + slotName: 'creator', + width: 148, + ellipsis: true, + tooltip: true, + }, { + title: '修改人', + dataIndex: 'updater', + slotName: 'updater', + width: 148, + ellipsis: true, + tooltip: true, + }, { + title: '操作', + slotName: 'handle', + width: 130, + align: 'center', + fixed: 'right', + default: true, + }, +] as TableColumnData[]; + +export default columns; diff --git a/orion-visor-ui/src/views/monitor/monitor-detail/compoments/detail-header.vue b/orion-visor-ui/src/views/monitor/monitor-detail/compoments/detail-header.vue new file mode 100644 index 00000000..59e84fa5 --- /dev/null +++ b/orion-visor-ui/src/views/monitor/monitor-detail/compoments/detail-header.vue @@ -0,0 +1,207 @@ + + + + + + + diff --git a/orion-visor-ui/src/views/monitor/monitor-detail/compoments/metrics-chart-tab.vue b/orion-visor-ui/src/views/monitor/monitor-detail/compoments/metrics-chart-tab.vue new file mode 100644 index 00000000..bcc8608f --- /dev/null +++ b/orion-visor-ui/src/views/monitor/monitor-detail/compoments/metrics-chart-tab.vue @@ -0,0 +1,179 @@ + + + + + + + diff --git a/orion-visor-ui/src/views/monitor/monitor-detail/compoments/metrics-chart.vue b/orion-visor-ui/src/views/monitor/monitor-detail/compoments/metrics-chart.vue new file mode 100644 index 00000000..ef9f8fb4 --- /dev/null +++ b/orion-visor-ui/src/views/monitor/monitor-detail/compoments/metrics-chart.vue @@ -0,0 +1,241 @@ + + + + + + + diff --git a/orion-visor-ui/src/views/monitor/monitor-detail/index.vue b/orion-visor-ui/src/views/monitor/monitor-detail/index.vue new file mode 100644 index 00000000..fc2b8402 --- /dev/null +++ b/orion-visor-ui/src/views/monitor/monitor-detail/index.vue @@ -0,0 +1,112 @@ + + + + + + + diff --git a/orion-visor-ui/src/views/monitor/monitor-detail/types/const.ts b/orion-visor-ui/src/views/monitor/monitor-detail/types/const.ts new file mode 100644 index 00000000..b83e96af --- /dev/null +++ b/orion-visor-ui/src/views/monitor/monitor-detail/types/const.ts @@ -0,0 +1,45 @@ +import type { WindowUnit, MetricUnitType, MetricUnitFormatOptions } from '@/utils/metrics'; + +// 图表组件配置 +export interface MetricsChartProps { + agentKeys: Array; + range: string; + windowValue: number; + windowUnit: WindowUnit; + option: MetricsChartOption; +} + +// 图表显示配置 +export interface MetricsChartOption { + name: string; + type?: 'line' | 'bar'; + measurement: string; + fields: Array; + span?: number; + legend?: boolean; + background?: boolean; + colors: Array<[string, string]>; + aggregate: string; + unit: MetricUnitType; + unitOption: MetricUnitFormatOptions; +} + +// tab +export const TabKeys = { + CHART: 'chart' +}; + +// 探针在线状态 字典项 +export const OnlineStatusKey = 'agentOnlineStatus'; + +// 监控告警开关 字典项 +export const AlarmSwitchKey = 'monitorAlarmSwitch'; + +// 指标图表区间 字典项 +export const ChartRangeKey = 'metricsChartRange'; + +// 指标聚合函数 字典项 +export const MetricsAggregateKey = 'metricsAggregate'; + +// 加载的字典值 +export const dictKeys = [AlarmSwitchKey, OnlineStatusKey, ChartRangeKey, MetricsAggregateKey]; diff --git a/orion-visor-ui/src/views/monitor/monitor-host/components/monitor-cell.vue b/orion-visor-ui/src/views/monitor/monitor-host/components/monitor-cell.vue new file mode 100644 index 00000000..b2939e74 --- /dev/null +++ b/orion-visor-ui/src/views/monitor/monitor-host/components/monitor-cell.vue @@ -0,0 +1,68 @@ + + + + + + + diff --git a/orion-visor-ui/src/views/monitor/monitor-host/components/monitor-host-card-list.vue b/orion-visor-ui/src/views/monitor/monitor-host/components/monitor-host-card-list.vue new file mode 100644 index 00000000..0b6fdd94 --- /dev/null +++ b/orion-visor-ui/src/views/monitor/monitor-host/components/monitor-host-card-list.vue @@ -0,0 +1,418 @@ + + + + + + + diff --git a/orion-visor-ui/src/views/monitor/monitor-host/components/monitor-host-form-drawer.vue b/orion-visor-ui/src/views/monitor/monitor-host/components/monitor-host-form-drawer.vue new file mode 100644 index 00000000..7a3f8b1b --- /dev/null +++ b/orion-visor-ui/src/views/monitor/monitor-host/components/monitor-host-form-drawer.vue @@ -0,0 +1,150 @@ + + + + + + + diff --git a/orion-visor-ui/src/views/monitor/monitor-host/components/monitor-host-table.vue b/orion-visor-ui/src/views/monitor/monitor-host/components/monitor-host-table.vue new file mode 100644 index 00000000..fe224806 --- /dev/null +++ b/orion-visor-ui/src/views/monitor/monitor-host/components/monitor-host-table.vue @@ -0,0 +1,484 @@ + + + + + + + diff --git a/orion-visor-ui/src/views/monitor/monitor-host/components/release-upload-modal.vue b/orion-visor-ui/src/views/monitor/monitor-host/components/release-upload-modal.vue new file mode 100644 index 00000000..83fcf6a2 --- /dev/null +++ b/orion-visor-ui/src/views/monitor/monitor-host/components/release-upload-modal.vue @@ -0,0 +1,112 @@ + + + + + + + diff --git a/orion-visor-ui/src/views/monitor/monitor-host/index.vue b/orion-visor-ui/src/views/monitor/monitor-host/index.vue new file mode 100644 index 00000000..1dc9a151 --- /dev/null +++ b/orion-visor-ui/src/views/monitor/monitor-host/index.vue @@ -0,0 +1,65 @@ + + + + + + + diff --git a/orion-visor-ui/src/views/monitor/monitor-host/types/card.fields.ts b/orion-visor-ui/src/views/monitor/monitor-host/types/card.fields.ts new file mode 100644 index 00000000..c6b3c21a --- /dev/null +++ b/orion-visor-ui/src/views/monitor/monitor-host/types/card.fields.ts @@ -0,0 +1,81 @@ +import type { CardField, CardFieldConfig } from '@/types/card'; + +const fieldConfig = { + rowGap: '10px', + labelSpan: 6, + minHeight: '22px', + fields: [ + { + label: '主机ID', + dataIndex: 'hostId', + slotName: 'hostId', + default: true, + }, { + label: '主机地址', + dataIndex: 'address', + slotName: 'address', + default: true, + }, { + label: '主机编码', + dataIndex: 'code', + slotName: 'code', + default: false, + }, { + label: '负责人', + dataIndex: 'ownerUsername', + slotName: 'ownerUsername', + ellipsis: true, + default: true, + }, { + label: '设备状态', + dataIndex: 'agentOnlineStatus', + slotName: 'agentOnlineStatus', + default: true, + }, { + label: 'CPU', + dataIndex: 'cpuUsage', + slotName: 'cpuUsage', + default: true, + }, { + label: '内存', + dataIndex: 'memoryUsage', + slotName: 'memoryUsage', + default: true, + }, { + label: '磁盘', + dataIndex: 'diskUsage', + slotName: 'diskUsage', + default: true, + }, { + label: '网络', + dataIndex: 'network', + slotName: 'network', + default: true, + }, { + label: '负载', + dataIndex: 'load', + slotName: 'load', + default: true, + }, { + label: '标签', + dataIndex: 'tags', + slotName: 'tags', + default: false, + }, { + label: 'agentKey', + dataIndex: 'agentKey', + slotName: 'agentKey', + ellipsis: true, + default: false, + }, { + label: '探针版本', + dataIndex: 'agentVersion', + slotName: 'agentVersion', + ellipsis: true, + tooltip: true, + default: true, + } + ] as CardField[] +} as CardFieldConfig; + +export default fieldConfig; diff --git a/orion-visor-ui/src/views/monitor/monitor-host/types/const.ts b/orion-visor-ui/src/views/monitor/monitor-host/types/const.ts new file mode 100644 index 00000000..9b0d221b --- /dev/null +++ b/orion-visor-ui/src/views/monitor/monitor-host/types/const.ts @@ -0,0 +1,32 @@ +export const TableName = 'monitor_host'; + +// 监控告警开关 +export const AlarmSwitch = { + OFF: 0, + ON: 1, +}; + +// 探针日志状态 +export const AgentLogStatus = { + WAIT: 'WAIT', + RUNNING: 'RUNNING', + SUCCESS: 'SUCCESS', + FAILED: 'FAILED', +}; + +export const NODATA_TIPS = '暂无数据'; + +// 探针安装状态 字典项 +export const InstallStatusKey = 'agentInstallStatus'; + +// 探针在线状态 字典项 +export const OnlineStatusKey = 'agentOnlineStatus'; + +// 监控告警开关 字典项 +export const AlarmSwitchKey = 'monitorAlarmSwitch'; + +// 探针日志状态 字典项 +export const AgentLogStatusKey = 'agentLogStatus'; + +// 加载的字典值 +export const dictKeys = [InstallStatusKey, AlarmSwitchKey, OnlineStatusKey, AgentLogStatusKey]; diff --git a/orion-visor-ui/src/views/monitor/monitor-host/types/form.rules.ts b/orion-visor-ui/src/views/monitor/monitor-host/types/form.rules.ts new file mode 100644 index 00000000..00b6821b --- /dev/null +++ b/orion-visor-ui/src/views/monitor/monitor-host/types/form.rules.ts @@ -0,0 +1,16 @@ +import type { FieldRule } from '@arco-design/web-vue'; + +export const policyId = [{ + required: false, + message: '请输入策略id' +}] as FieldRule[]; + +export const alarmSwitch = [{ + required: true, + message: '请输入告警开关' +}] as FieldRule[]; + +export default { + policyId, + alarmSwitch, +} as Record; diff --git a/orion-visor-ui/src/views/monitor/monitor-host/types/table.columns.ts b/orion-visor-ui/src/views/monitor/monitor-host/types/table.columns.ts new file mode 100644 index 00000000..3328df4f --- /dev/null +++ b/orion-visor-ui/src/views/monitor/monitor-host/types/table.columns.ts @@ -0,0 +1,112 @@ +import type { TableColumnData } from '@arco-design/web-vue'; + +const columns = [ + { + title: '主机ID', + dataIndex: 'hostId', + slotName: 'hostId', + width: 80, + align: 'left', + fixed: 'left', + default: true, + }, { + title: '主机信息', + dataIndex: 'hostInfo', + slotName: 'hostInfo', + width: 248, + align: 'left', + fixed: 'left', + default: true, + }, { + title: '设备状态', + dataIndex: 'agentOnlineStatus', + slotName: 'agentOnlineStatus', + align: 'center', + width: 120, + default: true, + }, { + title: 'CPU', + dataIndex: 'cpuUsage', + slotName: 'cpuUsage', + align: 'left', + width: 198, + default: true, + }, { + title: '内存', + dataIndex: 'memoryUsage', + slotName: 'memoryUsage', + align: 'left', + width: 198, + default: true, + }, { + title: '磁盘', + dataIndex: 'diskUsage', + slotName: 'diskUsage', + align: 'left', + width: 198, + default: true, + }, { + title: '网络', + dataIndex: 'network', + slotName: 'network', + align: 'left', + width: 148, + default: true, + }, { + title: '负载', + dataIndex: 'load', + slotName: 'load', + align: 'left', + width: 148, + default: true, + }, { + title: '标签', + dataIndex: 'tags', + slotName: 'tags', + align: 'left', + minWidth: 148, + default: false, + }, { + title: 'agentKey', + dataIndex: 'agentKey', + slotName: 'agentKey', + align: 'left', + width: 288, + default: false, + }, { + // TODO + // title: '告警策略', + // dataIndex: 'alarmPolicy', + // slotName: 'alarmPolicy', + // align: 'left', + // width: 120, + // default: true, + // }, { + title: '负责人', + dataIndex: 'ownerUsername', + slotName: 'ownerUsername', + align: 'left', + width: 108, + ellipsis: true, + tooltip: true, + default: true, + }, { + title: '探针版本', + dataIndex: 'agentVersion', + slotName: 'agentVersion', + align: 'left', + width: 118, + ellipsis: true, + tooltip: true, + default: true, + }, { + title: '操作', + slotName: 'handle', + width: 128, + align: 'center', + fixed: 'right', + default: true, + }, +] as TableColumnData[]; + +export default columns; diff --git a/orion-visor-ui/src/views/monitor/monitor-host/types/use-monitor-host-list.ts b/orion-visor-ui/src/views/monitor/monitor-host/types/use-monitor-host-list.ts new file mode 100644 index 00000000..6e072b3e --- /dev/null +++ b/orion-visor-ui/src/views/monitor/monitor-host/types/use-monitor-host-list.ts @@ -0,0 +1,273 @@ +import type { MonitorHostQueryResponse, } from '@/api/monitor/monitor-host'; +import { getMonitorHostMetrics, updateMonitorHostAlarmSwitch } from '@/api/monitor/monitor-host'; +import type { HostAgentLogResponse } from '@/api/asset/host-agent'; +import { getAgentInstallLogStatus, getHostAgentStatus, installHostAgent, updateAgentInstallStatus } from '@/api/asset/host-agent'; +import type { Ref } from 'vue'; +import { onActivated, onDeactivated, onMounted, onUnmounted, ref } from 'vue'; +import { useRouter } from 'vue-router'; +import { useDictStore } from '@/store'; +import { Message, Modal } from '@arco-design/web-vue'; +import { AgentInstallStatus, HostOsType } from '@/views/asset/host-list/types/const'; +import { AgentLogStatus, AlarmSwitchKey } from '@/views/monitor/monitor-host/types/const'; + +// 监控主机列表配置 +export interface UseMonitorHostListOptions { + // 主机信息 + hosts: Ref>; + // 设置加载中 + setLoading: (loading: boolean) => void; + // 重新加载 + reload: () => void; +} + +// 使用监控主机列表 +export default function useMonitorHostList(options: UseMonitorHostListOptions) { + const autoRefresh = ref(true); + const autoRefreshId = ref(); + const fetchInstallStatusId = ref(); + const lastRefreshTime = ref(0); + + const router = useRouter(); + const { toggleDict } = useDictStore(); + const { hosts, setLoading, reload } = options; + + // 打开详情 + const openDetail = (hostId: number, name: string) => { + router.push({ name: 'monitorDetail', query: { hostId, name } }); + }; + + // 安装探针 + const installAgent = async (hostIdList: Array) => { + try { + setLoading(true); + // 获取全部数据 + const installHosts = hosts.value.filter(s => hostIdList.includes(s.hostId)); + let hasWindows = false; + // 安装前检查 + for (let host of installHosts) { + // 检查状态 + if (host?.installLog?.status === AgentLogStatus.WAIT || host?.installLog?.status === AgentLogStatus.RUNNING) { + Message.error('主机' + host.name + '正在安装中, 请勿重复操作'); + return; + } + // 检查系统类型 + if (host.osType === HostOsType.WINDOWS.value) { + hasWindows = true; + } + } + // 二次确认 + Modal.confirm({ + title: '安装提示', + titleAlign: 'start', + bodyStyle: { 'white-space': 'pre-wrap' }, + content: `请确保探针已关闭\n请确认文件夹是否有权限${hasWindows ? '\nWindows 系统仅支持探针上传, 请手动进行安装' : ''}`, + okText: '确定', + onOk: async () => { + try { + // 调用安装 + await installHostAgent({ idList: hostIdList }); + Message.success('开始安装'); + // 重新加载 + reload(); + } catch (e) { + } finally { + setLoading(false); + } + } + }); + } catch (e) { + } finally { + setLoading(false); + } + }; + + // 手动安装成功 + const setInstallSuccess = (log: HostAgentLogResponse) => { + Modal.confirm({ + title: '修正状态', + titleAlign: 'start', + content: `确定要手动将安装记录修正为完成吗?`, + okText: '确定', + onOk: async () => { + try { + setLoading(true); + // 调用修改接口 + await updateAgentInstallStatus({ + id: log.id, + status: AgentLogStatus.SUCCESS, + message: '手动修正', + }); + log.status = AgentLogStatus.SUCCESS; + Message.success('状态已修正'); + } catch (e) { + } finally { + setLoading(false); + } + } + }); + }; + + // 更新报警开关 + const toggleAlarmSwitch = async (record: MonitorHostQueryResponse) => { + const dict = toggleDict(AlarmSwitchKey, record.alarmSwitch); + Modal.confirm({ + title: `${dict.label}确认`, + titleAlign: 'start', + content: `确定要${dict.label}报警功能吗?`, + okText: '确定', + onOk: async () => { + try { + setLoading(true); + const newSwitch = dict.value as number; + // 调用修改接口 + await updateMonitorHostAlarmSwitch({ + id: record.id, + alarmSwitch: newSwitch, + }); + record.alarmSwitch = newSwitch; + Message.success(`已${dict.label}`); + } catch (e) { + } finally { + setLoading(false); + } + } + }); + }; + + // 获取探针安装状态 + const pullInstallLogStatus = async () => { + // 获取安装中的记录 + const runningIdList = hosts.value.filter(s => s.installLog?.status === AgentLogStatus.WAIT || s.installLog?.status === AgentLogStatus.RUNNING) + .map(s => s.installLog?.id) + .filter(Boolean); + if (!runningIdList.length) { + return; + } + // 查询状态 + const { data } = await getAgentInstallLogStatus(runningIdList); + data.forEach(item => { + hosts.value.filter(s => s.installLog?.id === item.id).forEach(s => { + s.installLog.status = item.status; + s.installLog.message = item.message; + // 若安装成功则修改探针信息 + if (item.status === AgentLogStatus.SUCCESS && item.agentStatus) { + s.agentVersion = item.agentStatus.agentVersion; + s.latestVersion = item.agentStatus.latestVersion; + s.agentInstallStatus = item.agentStatus.agentInstallStatus; + s.agentOnlineStatus = item.agentStatus.agentOnlineStatus; + } + }); + }); + }; + + // 获取探针状态 + const getAgentStatus = async () => { + // 获取全部 hostId + const hostIds = hosts.value.map(s => s.hostId); + if (hostIds.length) { + try { + // 查询状态 + const { data } = await getHostAgentStatus(hostIds); + data.forEach(item => { + hosts.value.filter(s => s.hostId === item.id).forEach(s => { + s.agentVersion = item.agentVersion; + s.latestVersion = item.latestVersion; + s.agentInstallStatus = item.agentInstallStatus; + s.agentOnlineStatus = item.agentOnlineStatus; + }); + }); + } catch (e) { + } + } + }; + + // 获取指标信息 + const getHostMetrics = async () => { + // 获取全部已安装 agentKey + const agentKeys = hosts.value + .filter(s => s.agentInstallStatus === AgentInstallStatus.INSTALLED) + .map(s => s.agentKey) + .filter(Boolean); + if (agentKeys.length) { + try { + // 查询指标 + const { data } = await getMonitorHostMetrics(agentKeys); + data.forEach(item => { + hosts.value.filter(s => s.agentKey === item.agentKey).forEach(s => { + s.metricsData = item; + }); + }); + } catch (e) { + } + } + }; + + // 刷新指标 + const refreshMetrics = async () => { + // 加载状态信息 + await getAgentStatus(); + // 加载指标数据 + await getHostMetrics(); + // 设置刷新时间 + lastRefreshTime.value = Date.now(); + }; + + // 切换自动刷新 + const toggleAutoRefresh = async () => { + autoRefresh.value = !autoRefresh.value; + if (autoRefresh.value) { + // 开启自动刷新 + await openAutoRefresh(); + } else { + // 关闭自动刷新 + closeAutoRefresh(); + } + }; + + // 开启自动刷新 + const openAutoRefresh = async () => { + window.clearInterval(autoRefreshId.value); + if (!autoRefresh.value) { + return; + } + if (lastRefreshTime.value === 0) { + // 防止首次就刷新 + lastRefreshTime.value = 1; + } else if (Date.now() - lastRefreshTime.value > 60000) { + // 超过刷新的时间 + await refreshMetrics(); + } + // 设置自动刷新 + autoRefreshId.value = window.setInterval(refreshMetrics, 60000); + }; + + // 关闭自动刷新 + const closeAutoRefresh = () => { + window.clearInterval(autoRefreshId.value); + autoRefreshId.value = undefined; + }; + + onMounted(openAutoRefresh); + onActivated(openAutoRefresh); + onDeactivated(closeAutoRefresh); + onUnmounted(closeAutoRefresh); + + onMounted(() => { + window.clearInterval(fetchInstallStatusId.value); + fetchInstallStatusId.value = window.setInterval(pullInstallLogStatus, 5000); + }); + + onUnmounted(() => { + window.clearInterval(fetchInstallStatusId.value); + }); + + return { + autoRefresh, + openDetail, + installAgent, + setInstallSuccess, + toggleAlarmSwitch, + toggleAutoRefresh, + }; + +}; diff --git a/orion-visor-ui/src/views/terminal/components/view/rdp/rdp-action-bar.vue b/orion-visor-ui/src/views/terminal/components/view/rdp/rdp-action-bar.vue index 7d96f1fc..ba3a311e 100644 --- a/orion-visor-ui/src/views/terminal/components/view/rdp/rdp-action-bar.vue +++ b/orion-visor-ui/src/views/terminal/components/view/rdp/rdp-action-bar.vue @@ -206,7 +206,11 @@ // 选择文件回调 const onSelectFile = (files: Array) => { - fileList.value = [files[files.length - 1]]; + if (files.length) { + fileList.value = [files[files.length - 1]]; + } else { + fileList.value = []; + } }; // 上传文件 From 919e8383bf0d6852b6490db489187eeec8ac81d3 Mon Sep 17 00:00:00 2001 From: lijiahangmax Date: Tue, 9 Sep 2025 22:27:51 +0800 Subject: [PATCH 09/17] =?UTF-8?q?:hammer:=20=E4=BF=AE=E6=94=B9=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E5=8C=85=E5=90=8D.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/service/Dockerfile | 2 +- docker/service/entrypoint.sh | 4 ++-- .../dromara/visor/common/constant/FileConst.java | 10 ++++++---- .../agent/intstall/AbstractAgentInstaller.java | 4 ++-- .../asset/service/impl/HostAgentServiceImpl.java | 14 +++++++------- 5 files changed, 18 insertions(+), 16 deletions(-) diff --git a/docker/service/Dockerfile b/docker/service/Dockerfile index 1a2bc6d4..6d8a5a2a 100644 --- a/docker/service/Dockerfile +++ b/docker/service/Dockerfile @@ -24,7 +24,7 @@ RUN chmod +x /app/entrypoint.sh # 复制 jar 包 COPY ./service/orion-visor-launch.jar /app/app.jar # 复制探针包 -ADD ./service/agent-release.tar.gz /app/agent-release +ADD ./service./instant-agent-release.tar.gz /app/instant-agent-release # 启动检测 HEALTHCHECK --interval=15s --timeout=5s --retries=5 --start-period=10s \ diff --git a/docker/service/entrypoint.sh b/docker/service/entrypoint.sh index 12ee8bf0..a19be15d 100644 --- a/docker/service/entrypoint.sh +++ b/docker/service/entrypoint.sh @@ -1,7 +1,7 @@ #!/bin/sh -AGENT_RELEASE_DIR="/root/orion/orion-visor/agent-release" -DEFAULT_AGENT_DIR="/app/agent-release" +AGENT_RELEASE_DIR="/root/orion/visor/instant-agent-release" +DEFAULT_AGENT_DIR="/app/instant-agent-release" # 确保父目录存在 mkdir -p "$(dirname "$AGENT_RELEASE_DIR")" diff --git a/orion-visor-common/src/main/java/org/dromara/visor/common/constant/FileConst.java b/orion-visor-common/src/main/java/org/dromara/visor/common/constant/FileConst.java index 8995c4f3..1bc002fb 100644 --- a/orion-visor-common/src/main/java/org/dromara/visor/common/constant/FileConst.java +++ b/orion-visor-common/src/main/java/org/dromara/visor/common/constant/FileConst.java @@ -37,13 +37,15 @@ public interface FileConst { String SCRIPT = "script"; - String AGENT = "agent"; + String INSTANT_AGENT_PATH = "instant-agent"; - String AGENT_RELEASE = "agent-release"; + String INSTANT_AGENT_NAME = "instant_agent"; - String AGENT_RELEASE_TEMP = "agent-release-temp"; + String INSTANT_AGENT_RELEASE = "instant-agent-release"; - String AGENT_RELEASE_TAR_GZ = "agent-release.tar.gz"; + String INSTANT_AGENT_RELEASE_TEMP = "instant-agent-release-temp"; + + String INSTANT_AGENT_RELEASE_TAR_GZ = "instant-agent-release.tar.gz"; String VERSION = ".version"; diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/agent/intstall/AbstractAgentInstaller.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/agent/intstall/AbstractAgentInstaller.java index 6d1684c8..d4a3ef64 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/agent/intstall/AbstractAgentInstaller.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/agent/intstall/AbstractAgentInstaller.java @@ -86,7 +86,7 @@ public abstract class AbstractAgentInstaller implements AgentInstaller { this.params = params; this.logId = params.getLogId(); this.startScriptName = Const.START + HostOsTypeEnum.of(params.getOsType()).getScriptSuffix(); - this.uploadAgentName = FileConst.AGENT + HostOsTypeEnum.of(params.getOsType()).getBinarySuffix(); + this.uploadAgentName = FileConst.INSTANT_AGENT_NAME + HostOsTypeEnum.of(params.getOsType()).getBinarySuffix(); } @Override @@ -154,7 +154,7 @@ public abstract class AbstractAgentInstaller implements AgentInstaller { protected String getAgentHomePath() { return PathUtils.buildAppPath(HostOsTypeEnum.WINDOWS.name().equals(params.getOsType()), sshConfig.getUsername(), - FileConst.AGENT) + Const.SLASH; + FileConst.INSTANT_AGENT_PATH) + Const.SLASH; } /** diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostAgentServiceImpl.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostAgentServiceImpl.java index e4278506..d22d15d4 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostAgentServiceImpl.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostAgentServiceImpl.java @@ -95,7 +95,7 @@ public class HostAgentServiceImpl implements HostAgentService { public void readLocalAgentVersion() { log.info("HostAgentService-readLocalAgentVersion start"); // 文件路径 - String path = PathUtils.getOrionPath(FileConst.AGENT_RELEASE + Const.SLASH + FileConst.VERSION); + String path = PathUtils.getOrionPath(FileConst.INSTANT_AGENT_RELEASE + Const.SLASH + FileConst.VERSION); log.info("HostAgentService-readLocalAgentVersion path: {}", path); try { if (!Files1.isFile(path)) { @@ -191,9 +191,9 @@ public class HostAgentServiceImpl implements HostAgentService { Valid.notBlank(fileName, ErrorMessage.FILE_EXTENSION_TYPE); Valid.isTrue(fileName.endsWith(Const.SUFFIX_TAR_GZ), ErrorMessage.FILE_EXTENSION_TYPE); // 保存文件 - String releaseDir = PathUtils.getOrionPath(FileConst.AGENT_RELEASE); - String releaseTempDir = PathUtils.getOrionPath(FileConst.AGENT_RELEASE_TEMP); - File releaseTempFile = new File(releaseTempDir + Const.SLASH + FileConst.AGENT_RELEASE_TAR_GZ); + String releaseDir = PathUtils.getOrionPath(FileConst.INSTANT_AGENT_RELEASE); + String releaseTempDir = PathUtils.getOrionPath(FileConst.INSTANT_AGENT_RELEASE_TEMP); + File releaseTempFile = new File(releaseTempDir + Const.SLASH + FileConst.INSTANT_AGENT_RELEASE_TAR_GZ); log.info("HostAgentService.installAgent start releaseTempDir: {}, releaseTempFile: {}", releaseTempDir, releaseTempFile.getAbsolutePath()); try { // 创建目录 @@ -278,9 +278,9 @@ public class HostAgentServiceImpl implements HostAgentService { .hostId(host.getId()) .osType(host.getOsType()) .agentKey(host.getAgentKey()) - .agentFilePath(PathUtils.getOrionPath(FileConst.AGENT_RELEASE + Const.SLASH + agentFileName)) - .configFilePath(PathUtils.getOrionPath(FileConst.AGENT_RELEASE + Const.SLASH + FileConst.CONFIG_YAML)) - .startScriptPath(PathUtils.getOrionPath(FileConst.AGENT_RELEASE + Const.SLASH + Const.START + os.getScriptSuffix())) + .agentFilePath(PathUtils.getOrionPath(FileConst.INSTANT_AGENT_RELEASE + Const.SLASH + agentFileName)) + .configFilePath(PathUtils.getOrionPath(FileConst.INSTANT_AGENT_RELEASE + Const.SLASH + FileConst.CONFIG_YAML)) + .startScriptPath(PathUtils.getOrionPath(FileConst.INSTANT_AGENT_RELEASE + Const.SLASH + Const.START + os.getScriptSuffix())) .build(); taskParams.add(params); // 添加待检查文件 From 6b44e193f535e0f0d73d9766155db72b45b5def0 Mon Sep 17 00:00:00 2001 From: lijiahangmax Date: Tue, 9 Sep 2025 22:45:23 +0800 Subject: [PATCH 10/17] =?UTF-8?q?:hammer:=20=E4=BF=AE=E6=94=B9=E6=96=87?= =?UTF-8?q?=E6=A1=A3.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitee/ISSUE_TEMPLATE.zh-CN.md | 10 ++++ .gitee/PULL_REQUEST_TEMPLATE.zh-CN.md | 8 +++ .github/ISSUE_TEMPLATE/bug_report.yml | 59 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 5 ++ .github/ISSUE_TEMPLATE/feature_request.yml | 35 +++++++++++++ 5 files changed, 117 insertions(+) create mode 100644 .gitee/ISSUE_TEMPLATE.zh-CN.md create mode 100644 .gitee/PULL_REQUEST_TEMPLATE.zh-CN.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml diff --git a/.gitee/ISSUE_TEMPLATE.zh-CN.md b/.gitee/ISSUE_TEMPLATE.zh-CN.md new file mode 100644 index 00000000..15c3bf47 --- /dev/null +++ b/.gitee/ISSUE_TEMPLATE.zh-CN.md @@ -0,0 +1,10 @@ +### *当前使用版本 (必填) + +### 问题描述 + +### 该问题是如何引起的 + +### 重现步骤 + +### 报错信息 + diff --git a/.gitee/PULL_REQUEST_TEMPLATE.zh-CN.md b/.gitee/PULL_REQUEST_TEMPLATE.zh-CN.md new file mode 100644 index 00000000..cd72b47b --- /dev/null +++ b/.gitee/PULL_REQUEST_TEMPLATE.zh-CN.md @@ -0,0 +1,8 @@ +### 修改描述 + +### 关联的 Issue + +### 测试用例 + +### 修复效果的截屏 + diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..8653f407 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,59 @@ +name: 错误报告 +description: File a bug report. +title: "[错误报告]: " +labels: [""] +body: + - type: markdown + attributes: + value: | + 在提交前请确认: + - 使用的是[最新版本](https://github.com/dromara/orion-visor/releases) + - 参考了[安装文档](https://visor.orionsec.cn/quickstart/docker.html) + - 查阅了[常见问题](https://visor.orionsec.cn/support/faq.html) + - 搜索了[已有 issue](https://github.com/dromara/orion-visor/issues) + - type: checkboxes + id: confirm + attributes: + label: 确认 + description: 在提交 issue 之前, 请确认你已经阅读并确认以下内容 + options: + - label: 我使用的是最新版本 [最新版](https://github.com/dromara/orion-visor/releases) + required: true + - label: 我使用官方文档进行部署 [安装文档](https://visor.orionsec.cn/quickstart/docker.html) + required: true + - label: 我已检查了 [常见问题](https://visor.orionsec.cn/support/faq.html) 并没有找到解决方法 + required: true + - label: 我已搜索 [issue](https://github.com/dromara/orion-visor/issues) 并没有找到相关问题 + required: true + - type: input + id: version + attributes: + label: 当前程序版本 + description: 遇到问题时程序所在的版本号 + validations: + required: true + - type: dropdown + id: install + attributes: + label: 安装方式 + options: + - Docker + - 普通安装 + - 其他 + validations: + required: true + + - type: textarea + id: what-happened + attributes: + label: 问题描述 + description: 请详细描述你碰到的问题 + placeholder: "问题描述" + validations: + required: true + - type: textarea + id: logs + attributes: + label: 详细日志 + description: 问题出现时的程序日志 + render: bash diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..ce4ad1df --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: 官网 + url: https://visor.orionsec.cn/ + about: document. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..9d9892be --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,35 @@ +name: 功能改进 +description: 提出新功能建议 (请提交到需求收集帖) +title: "[功能建议]: " +labels: [""] +body: + - type: markdown + attributes: + value: | + 所有功能建议请统一提交到需求收集帖: 🔗 [#83 需求收集](https://github.com/dromara/orion-visor/issues/83) + + 在提交前请确认: + - ✅ 使用的是[最新版本](https://github.com/dromara/orion-visor/releases) + - ✅ 已搜索[已有 issue](https://github.com/dromara/orion-visor/issues) 和 需求收集帖避免重复 + - ✅ 定制化需求请联系作者 + + --- + + ### 如何提交高质量建议? + 1. **功能描述**: 你希望增加什么? + 2. **使用场景**: 你在什么情况下需要它? + 4. **参考实现**: 开源项目中的类似功能(**禁止引用商业闭源软件**) + - type: textarea + id: feature + attributes: + label: 功能改进 + description: 请详细描述需要改进或者添加的功能。 + placeholder: "功能改进" + validations: + required: true + - type: textarea + id: references + attributes: + label: 参考资料 + description: 可以列举一些参考资料, 但是不要引用同类但商业化软件的任何内容。 + placeholder: "参考资料" \ No newline at end of file From 5e03810295a473f384973966d2e7196e6d8cca3a Mon Sep 17 00:00:00 2001 From: lijiahangmax Date: Tue, 9 Sep 2025 23:10:03 +0800 Subject: [PATCH 11/17] =?UTF-8?q?:hammer:=20sql=20=E8=84=9A=E6=9C=AC.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handler/BaseExecCommandHandler.java | 1 - orion-visor-ui/src/router/routes/base.ts | 16 +- sql/init-2-schema-tables.sql | 210 +++++++++++++----- sql/init-4-data.sql | 117 ++++++++++ 4 files changed, 275 insertions(+), 69 deletions(-) diff --git a/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/handler/exec/command/handler/BaseExecCommandHandler.java b/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/handler/exec/command/handler/BaseExecCommandHandler.java index 1ae8fe8d..bcb81f31 100644 --- a/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/handler/exec/command/handler/BaseExecCommandHandler.java +++ b/orion-visor-modules/orion-visor-module-exec/orion-visor-module-exec-service/src/main/java/org/dromara/visor/module/exec/handler/exec/command/handler/BaseExecCommandHandler.java @@ -408,7 +408,6 @@ public abstract class BaseExecCommandHandler implements IExecCommandHandler { params.put("hostAddress", connectConfig.getHostAddress()); params.put("hostPort", connectConfig.getHostPort()); params.put("hostUsername", connectConfig.getUsername()); - // TODO 文档 params.put("agentKey", connectConfig.getAgentKey()); params.put("hostUuid", uuid); params.put("hostUuidShort", uuid.replace("-", Strings.EMPTY)); diff --git a/orion-visor-ui/src/router/routes/base.ts b/orion-visor-ui/src/router/routes/base.ts index 5669ace3..d2b8b97a 100644 --- a/orion-visor-ui/src/router/routes/base.ts +++ b/orion-visor-ui/src/router/routes/base.ts @@ -25,7 +25,8 @@ export const LOGIN_ROUTE: RouteRecordRaw = { path: '/login', name: LOGIN_ROUTE_NAME, meta: { - locale: '登录' + locale: '登录', + noAffix: true, }, component: () => import('@/views/authentication/login/index.vue'), }; @@ -38,6 +39,7 @@ export const REDIRECT_ROUTE: RouteRecordRaw = { meta: { locale: '重定向', hideInMenu: true, + noAffix: true }, children: [ { @@ -59,8 +61,8 @@ export const UPDATE_PASSWORD_ROUTE: RouteRecordRaw = { name: UPDATE_PASSWORD_ROUTE_NAME, component: () => import('@/views/base/update-password/index.vue'), meta: { - noAffix: true, - locale: '修改密码' + locale: '修改密码', + noAffix: true }, }; @@ -70,8 +72,8 @@ export const FORBIDDEN_ROUTE: RouteRecordRaw = { name: FORBIDDEN_ROUTER_NAME, component: () => import('@/views/base/status/forbidden/index.vue'), meta: { - noAffix: true, - locale: '403' + locale: '403', + noAffix: true }, }; @@ -82,8 +84,8 @@ export const NOT_FOUND_ROUTE: RouteRecordRaw = { name: NOT_FOUND_ROUTER_NAME, component: () => import('@/views/base/status/not-found/index.vue'), meta: { - noAffix: true, - locale: '404' + locale: '404', + noAffix: true }, }; diff --git a/sql/init-2-schema-tables.sql b/sql/init-2-schema-tables.sql index bad4420f..158bc5af 100644 --- a/sql/init-2-schema-tables.sql +++ b/sql/init-2-schema-tables.sql @@ -12,7 +12,7 @@ CREATE TABLE `_copy` `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 @@ -35,7 +35,7 @@ CREATE TABLE `command_snippet` `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_user` (`user_id`) USING BTREE ) ENGINE = InnoDB @@ -60,7 +60,7 @@ CREATE TABLE `data_extra` `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_user_id` (`user_id`) USING BTREE, INDEX `idx_type_rel_id` (`type`, `rel_id`) USING BTREE @@ -86,7 +86,7 @@ CREATE TABLE `data_group` `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_type_user` (`type`, `user_id`) USING BTREE ) ENGINE = InnoDB @@ -110,7 +110,7 @@ CREATE TABLE `data_group_rel` `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_group_rel` (`group_id`, `rel_id`) USING BTREE, INDEX `idx_type_user` (`type`, `user_id`) USING BTREE @@ -135,7 +135,7 @@ CREATE TABLE `data_permission` `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_user_id` (`user_id`) USING BTREE, INDEX `idx_role_id` (`role_id`) USING BTREE, @@ -161,7 +161,7 @@ CREATE TABLE `dict_key` `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_key` (`key_name`) USING BTREE ) ENGINE = InnoDB @@ -187,7 +187,7 @@ CREATE TABLE `dict_value` `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_key_id` (`key_id`) USING BTREE ) ENGINE = InnoDB @@ -214,13 +214,13 @@ CREATE TABLE `exec_host_log` `log_path` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '日志路径', `script_path` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '脚本路径', `error_message` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '错误信息', - `start_time` datetime(3) NULL DEFAULT NULL COMMENT '执行开始时间', - `finish_time` datetime(3) NULL DEFAULT NULL COMMENT '执行结束时间', + `start_time` datetime(6) NULL DEFAULT NULL COMMENT '执行开始时间', + `finish_time` datetime(6) NULL DEFAULT NULL COMMENT '执行结束时间', `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_log_id` (`log_id`) USING BTREE ) ENGINE = InnoDB @@ -251,7 +251,7 @@ CREATE TABLE `exec_job` `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_exec_user_id` (`exec_user_id`) USING BTREE ) ENGINE = InnoDB @@ -273,7 +273,7 @@ CREATE TABLE `exec_job_host` `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_job_id` (`job_id`) USING BTREE ) ENGINE = InnoDB @@ -301,13 +301,13 @@ CREATE TABLE `exec_log` `timeout` int(0) NULL DEFAULT NULL COMMENT '超时时间', `script_exec` tinyint(0) NULL DEFAULT 0 COMMENT '是否使用脚本执行', `status` char(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '执行状态', - `start_time` datetime(3) NULL DEFAULT NULL COMMENT '执行开始时间', - `finish_time` datetime(3) NULL DEFAULT NULL COMMENT '执行完成时间', + `start_time` datetime(6) NULL DEFAULT NULL COMMENT '执行开始时间', + `finish_time` datetime(6) NULL DEFAULT NULL COMMENT '执行完成时间', `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_user_id` (`user_id`) USING BTREE, INDEX `idx_source` (`source`, `source_id`) USING BTREE @@ -333,7 +333,7 @@ CREATE TABLE `exec_template` `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 @@ -354,7 +354,7 @@ CREATE TABLE `exec_template_host` `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE, INDEX `template_id` (`template_id`) USING BTREE ) ENGINE = InnoDB @@ -377,7 +377,7 @@ CREATE TABLE `favorite` `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_type_user` (`type`, `user_id`) USING BTREE ) ENGINE = InnoDB @@ -399,7 +399,7 @@ CREATE TABLE `history_value` `after_value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '修改后', `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 @@ -413,28 +413,60 @@ CREATE TABLE `history_value` DROP TABLE IF EXISTS `host`; CREATE TABLE `host` ( - `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT 'id', - `types` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '主机类型', - `os_type` char(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '系统类型', - `arch_type` char(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '系统架构', - `name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '主机名称', - `code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '主机编码', - `address` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '主机地址', - `status` char(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '主机状态', - `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '主机描述', - `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', - `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', - `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT 'id', + `types` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '主机类型', + `os_type` char(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '系统类型', + `arch_type` char(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '系统架构', + `name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '主机名称', + `code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '主机编码', + `address` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '主机地址', + `status` char(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '主机状态', + `agent_key` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'agentKey', + `agent_version` char(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '探针版本', + `agent_install_status` tinyint(0) NULL DEFAULT NULL COMMENT '探针安装状态', + `agent_online_status` tinyint(0) NULL DEFAULT NULL COMMENT '探针在线状态', + `agent_online_change_time` datetime(0) NULL DEFAULT NULL COMMENT '探针切换在线状态时间', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '主机描述', + `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE, - INDEX `idx_name` (`name`) USING BTREE + INDEX `idx_name` (`name`) USING BTREE, + INDEX `idx_agent_key` (`agent_key`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '主机' ROW_FORMAT = Dynamic; +-- ---------------------------- +-- Table structure for host_agent_log +-- ---------------------------- +DROP TABLE IF EXISTS `host_agent_log`; +CREATE TABLE `host_agent_log` +( + `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT 'id', + `host_id` bigint(0) NULL DEFAULT NULL COMMENT '主机id', + `agent_key` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'agentKey', + `type` char(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '类型', + `status` char(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '状态', + `message` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '消息', + `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_agent_key` (`agent_key`) USING BTREE, + INDEX `idx_host_id` (`host_id`) USING BTREE +) ENGINE = InnoDB + AUTO_INCREMENT = 1 + CHARACTER SET = utf8mb4 + COLLATE = utf8mb4_unicode_ci COMMENT = '主机探针日志' + ROW_FORMAT = Dynamic; + -- ---------------------------- -- Table structure for host_config -- ---------------------------- @@ -450,7 +482,7 @@ CREATE TABLE `host_config` `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE, INDEX `host_type_idx` (`host_id`, `type`) USING BTREE ) ENGINE = InnoDB @@ -476,7 +508,7 @@ CREATE TABLE `host_identity` `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 @@ -500,7 +532,7 @@ CREATE TABLE `host_key` `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 @@ -508,6 +540,62 @@ CREATE TABLE `host_key` COLLATE = utf8mb4_unicode_ci COMMENT = '主机密钥' ROW_FORMAT = Dynamic; +-- ---------------------------- +-- Table structure for monitor_host +-- ---------------------------- +DROP TABLE IF EXISTS `monitor_host`; +CREATE TABLE `monitor_host` +( + `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT 'id', + `host_id` bigint(0) NULL DEFAULT NULL COMMENT '主机id', + `policy_id` bigint(0) NULL DEFAULT NULL COMMENT '策略id', + `agent_key` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'agent key', + `alarm_switch` tinyint(0) NULL DEFAULT 0 COMMENT '告警开关', + `owner_user_id` bigint(0) NULL DEFAULT NULL COMMENT '负责人id', + `owner_username` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '负责人用户名', + `monitor_meta` json NULL COMMENT '监控元数据', + `monitor_config` json NULL COMMENT '监控配置', + `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_host_id` (`host_id`) USING BTREE, + INDEX `idx_policy_id` (`policy_id`) USING BTREE, + INDEX `idx_agent_key` (`agent_key`) USING BTREE +) ENGINE = InnoDB + AUTO_INCREMENT = 1 + CHARACTER SET = utf8mb4 + COLLATE = utf8mb4_unicode_ci COMMENT = '监控主机' + ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for monitor_metrics +-- ---------------------------- +DROP TABLE IF EXISTS `monitor_metrics`; +CREATE TABLE `monitor_metrics` +( + `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT 'id', + `name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '指标名称', + `measurement` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '数据集', + `value` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '指标项', + `unit` char(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'NONE' COMMENT '单位', + `suffix` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '后缀', + `description` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '指标描述', + `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_value` (`value`) USING BTREE +) ENGINE = InnoDB + AUTO_INCREMENT = 1 + CHARACTER SET = utf8mb4 + COLLATE = utf8mb4_unicode_ci COMMENT = '监控指标' + ROW_FORMAT = Dynamic; + -- ---------------------------- -- Table structure for operator_log -- ---------------------------- @@ -530,10 +618,10 @@ CREATE TABLE `operator_log` `error_message` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '错误信息', `return_value` json NULL COMMENT '返回值', `duration` int(0) NULL DEFAULT NULL COMMENT '操作时间', - `start_time` datetime(3) NULL DEFAULT NULL COMMENT '开始时间', - `end_time` datetime(3) NULL DEFAULT NULL COMMENT '结束时间', + `start_time` datetime(6) NULL DEFAULT NULL COMMENT '开始时间', + `end_time` datetime(6) NULL DEFAULT NULL COMMENT '结束时间', `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_user_id` (`user_id`) USING BTREE, INDEX `idx_type` (`type`) USING BTREE @@ -559,7 +647,7 @@ CREATE TABLE `path_bookmark` `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_user` (`user_id`) USING BTREE ) ENGINE = InnoDB @@ -583,7 +671,7 @@ CREATE TABLE `preference` `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_user_type` (`user_id`, `type`) USING BTREE ) ENGINE = InnoDB @@ -615,7 +703,7 @@ CREATE TABLE `system_menu` `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 @@ -640,7 +728,7 @@ CREATE TABLE `system_message` `receiver_username` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '接收人用户名', `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_receiver_classify` (`receiver_id`, `classify`) USING BTREE ) ENGINE = InnoDB @@ -664,7 +752,7 @@ CREATE TABLE `system_role` `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 @@ -685,7 +773,7 @@ CREATE TABLE `system_role_menu` `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_role` (`role_id`) USING BTREE ) ENGINE = InnoDB @@ -708,7 +796,7 @@ CREATE TABLE `system_setting` `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_key` (`config_key`) USING BTREE ) ENGINE = InnoDB @@ -740,7 +828,7 @@ CREATE TABLE `system_user` `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_username` (`username`) USING BTREE ) ENGINE = InnoDB @@ -762,7 +850,7 @@ CREATE TABLE `system_user_role` `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_user` (`user_id`) USING BTREE ) ENGINE = InnoDB @@ -784,7 +872,7 @@ CREATE TABLE `tag` `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_type` (`type`) USING BTREE ) ENGINE = InnoDB @@ -808,7 +896,7 @@ CREATE TABLE `tag_rel` `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_tag` (`tag_id`) USING BTREE, INDEX `idx_type_rel` (`tag_type`, `rel_id`) USING BTREE @@ -833,12 +921,12 @@ CREATE TABLE `terminal_connect_log` `type` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '类型', `status` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '状态', `session_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'sessionId', - `start_time` datetime(3) NULL DEFAULT NULL COMMENT '开始时间', - `end_time` datetime(3) NULL DEFAULT NULL COMMENT '结束时间', + `start_time` datetime(6) NULL DEFAULT NULL COMMENT '开始时间', + `end_time` datetime(6) NULL DEFAULT NULL COMMENT '结束时间', `extra_info` json NULL COMMENT '额外信息', `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_user_id` (`user_id`) USING BTREE, INDEX `idx_host_type` (`host_id`, `type`) USING BTREE, @@ -864,13 +952,13 @@ CREATE TABLE `upload_task` `file_count` int(0) NULL DEFAULT NULL COMMENT '文件数量', `host_count` int(0) NULL DEFAULT NULL COMMENT '主机数量', `extra_info` json NULL COMMENT '额外信息', - `start_time` datetime(3) NULL DEFAULT NULL COMMENT '开始时间', - `end_time` datetime(3) NULL DEFAULT NULL COMMENT '结束时间', + `start_time` datetime(6) NULL DEFAULT NULL COMMENT '开始时间', + `end_time` datetime(6) NULL DEFAULT NULL COMMENT '结束时间', `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 @@ -892,14 +980,14 @@ CREATE TABLE `upload_task_file` `real_file_path` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '实际文件路径', `file_size` bigint(0) NULL DEFAULT NULL COMMENT '文件大小', `status` char(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '状态', - `start_time` datetime(3) NULL DEFAULT NULL COMMENT '开始时间', - `end_time` datetime(3) NULL DEFAULT NULL COMMENT '结束时间', + `start_time` datetime(6) NULL DEFAULT NULL COMMENT '开始时间', + `end_time` datetime(6) NULL DEFAULT NULL COMMENT '结束时间', `error_message` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '错误信息', `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_task_id` (`task_id`) USING BTREE ) ENGINE = InnoDB diff --git a/sql/init-4-data.sql b/sql/init-4-data.sql index 68c34c13..18bf65d3 100644 --- a/sql/init-4-data.sql +++ b/sql/init-4-data.sql @@ -33,6 +33,44 @@ INSERT INTO `system_setting` VALUES (28, 'autoClear', 'autoClear_terminalLogKeep INSERT INTO `system_setting` VALUES (29, 'autoClear', 'autoClear_execLogEnabled', 'true', '2025-02-10 22:22:00', '2025-02-10 22:22:00', 'admin', 'admin', 0); INSERT INTO `system_setting` VALUES (30, 'autoClear', 'autoClear_terminalLogEnabled', 'true', '2025-02-10 22:22:00', '2025-02-10 22:22:00', 'admin', 'admin', 0); +-- 监控指标 +INSERT INTO `monitor_metrics` VALUES (1, 'CPU用户利用率', 'cpu', 'cpu_user_seconds_total', 'PER', NULL, '用户态使用时间', '2025-08-12 23:51:16', '2025-09-06 17:09:02', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (2, 'CPU系统利用率', 'cpu', 'cpu_system_seconds_total', 'PER', NULL, '内核态使用时间', '2025-08-13 00:23:56', '2025-09-06 17:09:02', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (3, 'CPU总利用率', 'cpu', 'cpu_total_seconds_total', 'PER', NULL, '用户态使用时间+内核态使用时间', '2025-08-13 00:24:30', '2025-09-06 17:09:02', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (4, '内存使用量', 'memory', 'mem_used_bytes_total', 'BYTES', NULL, '内存使用字节数', '2025-08-13 00:28:33', '2025-09-07 11:56:25', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (5, '内存使用率', 'memory', 'mem_used_percent', 'PER', NULL, '内存使用率', '2025-08-13 00:30:02', '2025-09-06 17:09:03', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (6, '交换内存使用量', 'memory', 'mem_swap_used_bytes_total', 'BYTES', NULL, '交换区使用字节数', '2025-08-13 00:31:14', '2025-09-07 11:56:21', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (7, '交换内存使用率', 'memory', 'mem_swap_used_percent', 'PER', NULL, '交换区使用率', '2025-08-13 00:31:48', '2025-09-06 17:09:03', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (8, '系统负载-1', 'load', 'load1', 'NONE', NULL, 'load1', '2025-08-13 00:33:01', '2025-08-13 00:33:01', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (9, '系统负载-5', 'load', 'load5', 'NONE', NULL, 'load5', '2025-08-13 00:33:24', '2025-08-13 00:33:24', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (10, '系统负载-15', 'load', 'load15', 'NONE', NULL, 'load15', '2025-08-13 00:33:45', '2025-08-13 00:33:45', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (11, '系统负载比-1', 'load', 'load1_core_ratio', 'NONE', NULL, '每个核心的平均load1 ', '2025-08-13 00:34:41', '2025-08-13 10:50:05', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (12, '系统负载比-5', 'load', 'load5_core_ratio', 'NONE', NULL, '每个核心的平均load5', '2025-08-13 00:35:12', '2025-08-13 10:50:05', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (13, '系统负载比-15', 'load', 'load15_core_ratio', 'NONE', NULL, '每个核心的平均load15', '2025-08-13 00:35:38', '2025-08-13 10:50:05', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (14, '磁盘使用量', 'disk', 'disk_fs_used_bytes', 'BYTES', NULL, '磁盘使用量', '2025-08-13 10:04:18', '2025-08-13 10:04:18', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (15, '磁盘使用率', 'disk', 'disk_fs_used_percent', 'PER', NULL, '磁盘使用率', '2025-08-13 10:04:18', '2025-09-06 17:09:03', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (16, 'inode使用率', 'disk', 'disk_fs_inodes_used_percent', 'PER', NULL, '磁盘inode使用率', '2025-08-13 10:04:18', '2025-09-06 17:09:03', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (17, 'IO总读取字节大小', 'io', 'disk_io_read_bytes_total', 'BYTES', NULL, '磁盘IO总读取字节大小', '2025-08-13 10:10:29', '2025-08-13 10:10:29', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (18, 'IO总写入字节大小', 'io', 'disk_io_write_bytes_total', 'BYTES', NULL, '磁盘IO总写入字节大小', '2025-08-13 10:10:29', '2025-08-13 10:10:29', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (19, 'IO总读取次数', 'io', 'disk_io_reads_total', 'COUNT', NULL, '磁盘IO总读取次数', '2025-08-13 10:10:29', '2025-08-13 10:10:29', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (20, 'IO总写入次数', 'io', 'disk_io_writes_total', 'COUNT', NULL, '磁盘IO总写入次数', '2025-08-13 10:10:29', '2025-08-13 10:10:29', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (21, 'IO每秒读取字节大小', 'io', 'disk_io_read_bytes_per_second', 'BYTES_S', NULL, '磁盘IO每秒读取字节大小', '2025-08-13 10:10:29', '2025-08-13 10:10:29', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (22, 'IO每秒写入字节大小', 'io', 'disk_io_write_bytes_per_second', 'BYTES_S', NULL, '磁盘IO每秒写入字节大小', '2025-08-13 10:10:29', '2025-08-13 10:10:29', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (23, 'IO每秒读取次数', 'io', 'disk_io_reads_per_second', 'COUNT_S', NULL, '磁盘IO每秒读取次数', '2025-08-13 10:10:29', '2025-08-13 10:10:29', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (24, 'IO每秒写入次数', 'io', 'disk_io_writes_per_second', 'COUNT_S', NULL, '磁盘IO每秒写入次数', '2025-08-13 10:10:29', '2025-08-13 10:10:29', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (25, '网卡总发送字节大小', 'network', 'net_recv_bytes_total', 'BITS', NULL, '网卡总发送字节大小', '2025-08-13 10:36:16', '2025-08-13 10:36:16', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (26, '网卡总接收字节大小', 'network', 'net_recv_bytes_total', 'BITS', NULL, '网卡总接收字节大小', '2025-08-13 10:36:16', '2025-08-13 10:36:16', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (27, '网卡总发送包数量', 'network', 'net_sent_packets_total', 'COUNT', NULL, '网卡总发送包数量', '2025-08-13 10:36:16', '2025-08-13 10:36:16', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (28, '网卡总接收包数量', 'network', 'net_recv_packets_total', 'COUNT', NULL, '网卡总接收包数量', '2025-08-13 10:36:16', '2025-08-13 10:36:16', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (29, '网卡每秒发送字节大小', 'network', 'net_sent_bytes_per_second', 'BITS_S', NULL, '网卡每秒发送字节大小', '2025-08-13 10:36:16', '2025-08-13 10:36:16', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (30, '网卡每秒接收字节大小', 'network', 'net_recv_bytes_per_second', 'BITS_S', NULL, '网卡每秒接收字节大小', '2025-08-13 10:36:16', '2025-08-13 10:36:16', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (31, '网卡每秒发送包数量', 'network', 'net_sent_packets_per_second', 'COUNT_S', NULL, '网卡每秒发送包数量', '2025-08-13 10:36:16', '2025-08-13 10:36:16', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (32, '网卡每秒接收包数量', 'network', 'net_recv_packets_per_second', 'COUNT_S', NULL, '网卡每秒接收包数量', '2025-08-13 10:36:16', '2025-08-13 10:36:16', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (33, 'TCP连接数', 'connections', 'net_tcp_connections', 'NONE', NULL, 'TCP连接数', '2025-08-13 10:36:16', '2025-08-13 10:36:16', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (34, 'UDP连接数', 'connections', 'net_udp_connections', 'NONE', NULL, 'UDP连接数', '2025-08-13 10:36:16', '2025-08-13 10:36:16', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (35, 'INET连接数', 'connections', 'net_inet_connections', 'NONE', NULL, 'INET连接数', '2025-08-13 10:36:17', '2025-08-13 10:36:17', 'admin', 'admin', 0); +INSERT INTO `monitor_metrics` VALUES (36, '总连接数', 'connections', 'net_all_connections', 'NONE', NULL, 'TCP + UDP + INET 连接数量', '2025-08-13 10:36:17', '2025-08-13 10:36:17', 'admin', 'admin', 0); + -- 字典项 INSERT INTO `dict_key` VALUES (1, 'operatorLogModule', 'STRING', '[]', '操作日志模块', '2023-10-21 02:04:22', '2023-10-30 14:11:38', 'admin', 'admin', 0); INSERT INTO `dict_key` VALUES (2, 'operatorLogType', 'STRING', '[]', '操作日志类型', '2023-10-21 02:06:04', '2023-10-21 02:06:04', 'admin', 'admin', 0); @@ -86,6 +124,15 @@ INSERT INTO `dict_key` VALUES (73, 'clipboardNormalize', 'STRING', '[]', '剪切 INSERT INTO `dict_key` VALUES (74, 'clipboardEncoding', 'STRING', '[]', '剪切板编码', '2025-06-19 01:22:44', '2025-06-19 01:22:44', 'admin', 'admin', 0); INSERT INTO `dict_key` VALUES (75, 'vcnCursor', 'STRING', '[]', 'vnc光标', '2025-06-19 01:22:54', '2025-06-19 01:22:54', 'admin', 'admin', 0); INSERT INTO `dict_key` VALUES (76, 'driveMountMode', 'STRING', '[{\"name\": \"help\", \"type\": \"STRING\"}]', '驱动挂载模式', '2025-06-28 22:53:20', '2025-06-28 22:53:20', 'admin', 'admin', 0); +INSERT INTO `dict_key` VALUES (77, 'metricsMeasurement', 'STRING', '[]', '监控指标类型', '2025-08-12 23:31:00', '2025-08-12 23:31:00', 'admin', 'admin', 0); +INSERT INTO `dict_key` VALUES (78, 'metricsUnit', 'STRING', '[]', '监控指标单位', '2025-08-12 23:31:00', '2025-08-12 23:31:00', 'admin', 'admin', 0); +INSERT INTO `dict_key` VALUES (79, 'monitorAlarmSwitch', 'INTEGER', '[]', '监控告警开关', '2025-08-23 17:07:21', '2025-09-01 23:32:34', 'admin', 'admin', 0); +INSERT INTO `dict_key` VALUES (80, 'agentOnlineStatus', 'INTEGER', '[{\"name\": \"status\", \"type\": \"STRING\"}, {\"name\": \"color\", \"type\": \"COLOR\"}, {\"name\": \"icon\", \"type\": \"STRING\"}]', '探针在线状态', '2025-08-23 17:07:21', '2025-08-27 07:48:53', 'admin', 'admin', 0); +INSERT INTO `dict_key` VALUES (81, 'agentInstallStatus', 'INTEGER', '[]', '探针安装状态', '2025-08-24 22:48:54', '2025-08-27 07:48:17', 'admin', 'admin', 0); +INSERT INTO `dict_key` VALUES (82, 'agentLogType', 'STRING', '[]', '探针日志类型', '2025-08-31 12:16:24', '2025-08-31 12:16:24', 'admin', 'admin', 0); +INSERT INTO `dict_key` VALUES (83, 'agentLogStatus', 'STRING', '[{\"name\": \"color\", \"type\": \"STRING\"}, {\"name\": \"loading\", \"type\": \"BOOLEAN\"}, {\"name\": \"installLabel\", \"type\": \"STRING\"}]', '探针日志状态', '2025-08-31 12:16:24', '2025-09-01 23:18:18', 'admin', 'admin', 0); +INSERT INTO `dict_key` VALUES (84, 'metricsChartRange', 'STRING', '[{\"name\": \"window\", \"type\": \"STRING\"}]', '指标图表区间', '2025-09-06 23:32:21', '2025-09-06 23:32:21', 'admin', 'admin', 0); +INSERT INTO `dict_key` VALUES (85, 'metricsAggregate', 'STRING', '[]', '指标聚合函数', '2025-09-07 16:54:22', '2025-09-07 16:54:22', 'admin', 'admin', 0); -- 字典值 INSERT INTO `dict_value` VALUES (3, 4, 'systemMenuType', '1', '父菜单', '{}', 10, '2023-10-26 15:58:59', '2023-10-26 15:58:59', 'admin', 'admin', 0); @@ -347,6 +394,12 @@ INSERT INTO `dict_value` VALUES (366, 53, 'terminalTheme', '{\"background\":\"#1 INSERT INTO `dict_value` VALUES (385, 57, 'hostStatus', 'ENABLED', '启用', '{\"color\": \"arcoblue\", \"status\": \"normal\", \"buttonStatus\": \"normal\"}', 10, '2024-07-17 12:51:10', '2024-07-22 16:53:53', 'admin', 'admin', 0); INSERT INTO `dict_value` VALUES (386, 57, 'hostStatus', 'DISABLED', '停用', '{\"color\": \"orangered\", \"status\": \"error\", \"buttonStatus\": \"danger\"}', 20, '2024-07-17 12:51:10', '2024-07-22 16:53:46', 'admin', 'admin', 0); INSERT INTO `dict_value` VALUES (387, 58, 'hostType', 'SSH', 'SSH', '{\"color\": \"arcoblue\"}', 10, '2024-07-17 12:51:10', '2024-07-17 15:57:24', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (388, 2, 'operatorLogType', 'terminal:sftp-chown', '修改文件归属', '{}', 73, '2024-02-23 17:54:37', '2024-10-16 10:41:59', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (389, 2, 'operatorLogType', 'terminal:sftp-chgrp', '修改文件分组', '{}', 76, '2024-02-23 17:54:37', '2024-10-16 10:41:59', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (390, 2, 'operatorLogType', 'terminal:sftp-get-content', '查看文件内容', '{}', 79, '2024-02-23 17:54:37', '2024-10-16 10:41:59', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (391, 33, 'terminalFileOperatorType', 'terminal:sftp-chown', '修改文件归属', '{}', 63, '2024-03-05 16:52:18', '2025-06-29 12:30:54', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (392, 33, 'terminalFileOperatorType', 'terminal:sftp-chgrp', '修改文件分组', '{}', 66, '2024-03-05 16:52:18', '2025-06-29 12:30:55', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (393, 33, 'terminalFileOperatorType', 'terminal:sftp-get-content', '查看文件内容', '{}', 69, '2024-03-05 16:52:18', '2025-06-29 12:30:55', 'admin', 'admin', 0); INSERT INTO `dict_value` VALUES (401, 43, 'messageType', 'LOGIN_FAILED', '登录失败', '{\"tagColor\": \"red\", \"tagLabel\": \"登录失败\", \"tagVisible\": true, \"redirectComponent\": \"0\"}', 50, '2024-08-19 18:34:27', '2024-08-19 18:34:27', 'admin', 'admin', 0); INSERT INTO `dict_value` VALUES (402, 53, 'terminalTheme', '{\"background\":\"#101216\",\"foreground\":\"#8B949E\",\"cursor\":\"#C9D1D9\",\"selectionBackground\":\"#3B5070\",\"black\":\"#000000\",\"red\":\"#F78166\",\"green\":\"#56D364\",\"yellow\":\"#E3B341\",\"blue\":\"#6CA4F8\",\"cyan\":\"#2B7489\",\"white\":\"#FFFFFF\",\"brightBlack\":\"#4D4D4D\",\"brightRed\":\"#F78166\",\"brightGreen\":\"#56D364\",\"brightYellow\":\"#E3B341\",\"brightBlue\":\"#6CA4F8\",\"brightCyan\":\"#2B7489\",\"brightWhite\":\"#FFFFFF\"}', 'GitHub Dark', '{\"dark\": true}', 150, '2024-08-19 18:53:14', '2024-08-19 18:53:20', 'admin', 'admin', 0); INSERT INTO `dict_value` VALUES (403, 53, 'terminalTheme', '{\"background\":\"#F4F4F4\",\"foreground\":\"#3E3E3E\",\"cursor\":\"#3F3F3F\",\"selectionBackground\":\"#A9C1E2\",\"black\":\"#3E3E3E\",\"red\":\"#970B16\",\"green\":\"#07962A\",\"yellow\":\"#F8EEC7\",\"blue\":\"#003E8A\",\"cyan\":\"#89D1EC\",\"white\":\"#FFFFFF\",\"brightBlack\":\"#666666\",\"brightRed\":\"#DE0000\",\"brightGreen\":\"#87D5A2\",\"brightYellow\":\"#F1D007\",\"brightBlue\":\"#2E6CBA\",\"brightCyan\":\"#1CFAFE\",\"brightWhite\":\"#FFFFFF\"}', 'Github', '{\"dark\": false}', 160, '2024-08-19 18:53:39', '2024-08-19 18:53:39', 'admin', 'admin', 0); @@ -429,6 +482,58 @@ INSERT INTO `dict_value` VALUES (518, 33, 'terminalFileOperatorType', 'terminal: INSERT INTO `dict_value` VALUES (519, 33, 'terminalFileOperatorType', 'terminal:rdp-download', '下载文件(RDP)', '{}', 110, '2024-03-05 16:52:18', '2025-06-29 12:28:52', 'admin', 'admin', 0); INSERT INTO `dict_value` VALUES (520, 58, 'hostType', 'VNC', 'VNC', '{\"color\": \"pinkpurple\"}', 30, '2025-07-01 16:02:01', '2025-07-03 00:56:56', 'admin', 'admin', 0); INSERT INTO `dict_value` VALUES (521, 27, 'terminalConnectType', 'VNC', 'VNC', '{\"color\": \"pinkpurple\"}', 40, '2025-07-01 16:02:21', '2025-07-03 00:56:54', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (522, 1, 'operatorLogModule', 'monitor:monitor-metrics', '监控指标', '{}', 2160, '2025-08-12 23:31:00', '2025-08-12 23:31:00', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (523, 2, 'operatorLogType', 'monitor-metrics:create', '创建监控指标', '{}', 10, '2025-08-12 23:31:00', '2025-08-12 23:31:00', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (524, 2, 'operatorLogType', 'monitor-metrics:update', '更新监控指标', '{}', 20, '2025-08-12 23:31:00', '2025-08-12 23:31:00', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (525, 2, 'operatorLogType', 'monitor-metrics:delete', '删除监控指标', '{}', 30, '2025-08-12 23:31:00', '2025-08-12 23:31:00', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (526, 77, 'metricsMeasurement', 'cpu', 'cpu', '{}', 10, '2025-08-12 23:31:00', '2025-08-12 23:31:00', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (527, 77, 'metricsMeasurement', 'memory', '内存', '{}', 20, '2025-08-12 23:31:00', '2025-08-12 23:31:00', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (528, 77, 'metricsMeasurement', 'load', '负载', '{}', 30, '2025-08-12 23:31:00', '2025-08-12 23:31:00', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (529, 77, 'metricsMeasurement', 'disk', '磁盘', '{}', 40, '2025-08-12 23:31:00', '2025-08-12 23:31:00', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (530, 77, 'metricsMeasurement', 'io', 'io', '{}', 50, '2025-08-12 23:31:00', '2025-08-12 23:31:00', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (531, 77, 'metricsMeasurement', 'network', '网络', '{}', 60, '2025-08-12 23:31:00', '2025-08-12 23:31:00', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (532, 77, 'metricsMeasurement', 'connections', '连接数', '{}', 70, '2025-08-12 23:31:00', '2025-08-12 23:31:00', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (533, 78, 'metricsUnit', 'BYTES', '字节', '{}', 10, '2025-08-12 23:31:00', '2025-08-12 23:31:00', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (534, 78, 'metricsUnit', 'BYTES_S', '字节/秒', '{}', 40, '2025-08-12 23:31:00', '2025-08-13 00:17:12', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (535, 78, 'metricsUnit', 'BITS_S', '比特/秒', '{}', 50, '2025-08-12 23:31:00', '2025-08-13 00:17:14', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (536, 78, 'metricsUnit', 'TEXT', '文本', '{}', 100, '2025-08-12 23:31:00', '2025-08-15 16:38:56', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (537, 78, 'metricsUnit', 'NONE', '无', '{}', 110, '2025-08-12 23:31:00', '2025-08-15 16:38:56', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (538, 78, 'metricsUnit', 'SECONDS', '秒', '{}', 30, '2025-08-12 23:55:17', '2025-08-13 00:17:07', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (539, 78, 'metricsUnit', 'COUNT', '次', '{}', 20, '2025-08-13 00:15:15', '2025-08-13 00:15:50', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (540, 78, 'metricsUnit', 'COUNT_S', '次/秒', '{}', 60, '2025-08-13 00:15:38', '2025-08-13 00:17:20', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (541, 78, 'metricsUnit', 'BITS', '比特', '{}', 15, '2025-08-13 10:34:27', '2025-08-13 10:34:27', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (542, 78, 'metricsUnit', 'PER', '百分比', '{}', 90, '2025-08-13 10:34:27', '2025-08-13 10:34:27', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (543, 79, 'monitorAlarmSwitch', '0', '关闭', '{}', 10, '2025-08-23 17:07:21', '2025-09-01 23:32:55', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (544, 79, 'monitorAlarmSwitch', '1', '开启', '{}', 20, '2025-08-23 17:07:21', '2025-09-01 23:32:50', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (545, 80, 'agentOnlineStatus', '0', '离线', '{\"icon\": \"icon-close\", \"color\": \"red\", \"status\": \"error\"}', 10, '2025-08-23 17:07:21', '2025-08-27 07:48:50', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (546, 80, 'agentOnlineStatus', '1', '在线', '{\"icon\": \"icon-check\", \"color\": \"green\", \"status\": \"pass\"}', 20, '2025-08-23 17:07:21', '2025-08-27 07:48:50', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (547, 1, 'operatorLogModule', 'monitor:monitor-host', '监控主机', '{}', 2170, '2025-08-23 17:08:17', '2025-08-23 17:08:17', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (548, 2, 'operatorLogType', 'monitor-host:update', '更新监控配置', '{}', 10, '2025-08-23 17:08:17', '2025-08-23 17:08:17', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (549, 2, 'operatorLogType', 'monitor-host:update-switch', '更新监控开关', '{}', 20, '2025-08-23 17:08:17', '2025-08-23 17:08:17', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (550, 81, 'agentInstallStatus', '0', '未安装', '{}', 10, '2025-08-24 22:49:27', '2025-08-27 07:48:12', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (551, 81, 'agentInstallStatus', '1', '已安装', '{}', 20, '2025-08-24 22:49:34', '2025-08-27 07:48:12', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (553, 82, 'agentLogType', 'OFFLINE', '下线', '{}', 10, '2025-08-31 12:16:24', '2025-08-31 12:16:24', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (554, 82, 'agentLogType', 'ONLINE', '上线', '{}', 20, '2025-08-31 12:16:24', '2025-08-31 12:16:24', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (555, 82, 'agentLogType', 'INSTALL', '安装', '{}', 30, '2025-08-31 12:16:24', '2025-08-31 12:16:24', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (556, 82, 'agentLogType', 'START', '启动', '{}', 40, '2025-08-31 12:16:24', '2025-08-31 12:16:24', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (557, 82, 'agentLogType', 'STOP', '停止', '{}', 50, '2025-08-31 12:16:24', '2025-08-31 12:16:24', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (558, 83, 'agentLogStatus', 'WAIT', '等待中', '{\"color\": \"green\", \"loading\": true, \"installLabel\": \"等待安装\"}', 10, '2025-08-31 12:16:24', '2025-09-01 23:18:53', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (559, 83, 'agentLogStatus', 'RUNNING', '运行中', '{\"color\": \"green\", \"loading\": true, \"installLabel\": \"安装中\"}', 20, '2025-08-31 12:16:24', '2025-09-01 23:18:48', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (560, 83, 'agentLogStatus', 'SUCCESS', '成功', '{\"color\": \"arcoblue\", \"loading\": false, \"installLabel\": \"安装成功\"}', 30, '2025-08-31 12:16:24', '2025-09-01 23:18:41', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (561, 83, 'agentLogStatus', 'FAILED', '失败', '{\"color\": \"red\", \"loading\": false, \"installLabel\": \"安装失败\"}', 40, '2025-08-31 12:16:24', '2025-09-01 23:18:36', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (562, 2, 'operatorLogType', 'host:install-agent', '安装主机探针', '{}', 110, '2025-08-31 20:18:44', '2025-08-31 20:18:44', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (563, 2, 'operatorLogType', 'host:update-install-status', '修改探针安装状态', '{}', 120, '2025-08-31 20:18:44', '2025-08-31 20:18:44', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (564, 2, 'operatorLogType', 'host:upload-agent-release', '上传探针发布包', '{}', 130, '2025-08-31 20:18:44', '2025-09-02 07:21:42', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (565, 84, 'metricsChartRange', '-30m', '30分钟', '{\"window\": \"1m\"}', 10, '2025-09-06 23:33:13', '2025-09-07 01:45:01', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (566, 84, 'metricsChartRange', '-2h', '2小时', '{\"window\": \"1m,5m\"}', 20, '2025-09-06 23:33:49', '2025-09-07 01:44:55', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (567, 84, 'metricsChartRange', '-24h', '24小时', '{\"window\": \"5m,1h\"}', 30, '2025-09-06 23:34:28', '2025-09-07 01:44:48', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (568, 84, 'metricsChartRange', '-7d', '1周', '{\"window\": \"1h,12h,24h\"}', 40, '2025-09-06 23:35:37', '2025-09-07 01:44:43', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (569, 84, 'metricsChartRange', '-30d', '30天', '{\"window\": \"1d\"}', 50, '2025-09-06 23:36:17', '2025-09-07 01:44:38', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (570, 84, 'metricsChartRange', '-60d', '60天', '{\"window\": \"1d\"}', 60, '2025-09-06 23:36:31', '2025-09-07 01:44:35', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (571, 85, 'metricsAggregate', 'mean', '平均值', '{}', 10, '2025-09-07 16:54:35', '2025-09-07 16:54:35', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (572, 85, 'metricsAggregate', 'max', '最大值', '{}', 20, '2025-09-07 16:54:41', '2025-09-07 16:54:59', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (573, 85, 'metricsAggregate', 'min', '最小值', '{}', 30, '2025-09-07 16:55:19', '2025-09-07 16:55:19', 'admin', 'admin', 0); +INSERT INTO `dict_value` VALUES (574, 85, 'metricsAggregate', 'sum', '总和', '{}', 40, '2025-09-07 16:55:27', '2025-09-07 16:55:27', 'admin', 'admin', 0); -- 菜单配置 INSERT INTO `system_menu` VALUES (1, 0, '工作台', NULL, 1, 10, 1, 1, 1, 0, 'IconComputer', NULL, 'workplace', '2023-07-28 10:51:50', '2024-08-11 00:05:44', 'admin', 'admin', 0); @@ -550,3 +655,15 @@ INSERT INTO `system_menu` VALUES (202, 198, '清理上传日志', 'exec:upload-t INSERT INTO `system_menu` VALUES (203, 12, '系统设置', NULL, 2, 40, 1, 1, 1, 0, 'IconSettings', NULL, 'systemSetting', '2024-06-17 20:46:15', '2024-06-17 20:46:15', 'admin', 'admin', 0); INSERT INTO `system_menu` VALUES (265, 203, '更新系统设置', 'infra:system-setting:update', 3, 20, 1, 1, 1, 0, NULL, NULL, NULL, '2024-10-09 19:25:28', '2025-01-02 22:14:45', 'admin', 'admin', 0); INSERT INTO `system_menu` VALUES (271, 177, '修改任务执行用户', 'exec:exec-job:update-exec-user', 3, 45, 1, 1, 1, 0, NULL, NULL, NULL, '2024-12-13 00:18:13', '2024-12-13 00:18:13', 'admin', 'admin', 0); +INSERT INTO `system_menu` VALUES (282, 0, '主机监控', NULL, 1, 350, 1, 1, 1, 0, 'IconComputer', NULL, 'monitorModule', '2025-08-12 23:31:02', '2025-08-12 23:38:49', 'admin', 'admin', 0); +INSERT INTO `system_menu` VALUES (283, 282, '监控指标', NULL, 2, 110, 1, 1, 1, 0, 'IconList', NULL, 'metrics', '2025-08-12 23:31:02', '2025-09-03 23:03:30', 'admin', 'admin', 0); +INSERT INTO `system_menu` VALUES (284, 283, '查询监控指标', 'monitor:monitor-metrics:query', 3, 10, 1, 1, 1, 0, NULL, NULL, NULL, '2025-08-12 23:31:02', '2025-08-12 23:31:02', 'admin', 'admin', 0); +INSERT INTO `system_menu` VALUES (285, 283, '创建监控指标', 'monitor:monitor-metrics:create', 3, 20, 1, 1, 1, 0, NULL, NULL, NULL, '2025-08-12 23:31:02', '2025-08-12 23:31:02', 'admin', 'admin', 0); +INSERT INTO `system_menu` VALUES (286, 283, '修改监控指标', 'monitor:monitor-metrics:update', 3, 30, 1, 1, 1, 0, NULL, NULL, NULL, '2025-08-12 23:31:02', '2025-08-12 23:31:02', 'admin', 'admin', 0); +INSERT INTO `system_menu` VALUES (287, 283, '删除监控指标', 'monitor:monitor-metrics:delete', 3, 40, 1, 1, 1, 0, NULL, NULL, NULL, '2025-08-12 23:31:02', '2025-08-12 23:31:02', 'admin', 'admin', 0); +INSERT INTO `system_menu` VALUES (288, 282, '主机监控', NULL, 2, 10, 1, 1, 1, 0, 'IconComputer', NULL, 'monitorHost', '2025-08-23 17:02:02', '2025-08-24 22:47:03', 'admin', 'admin', 0); +INSERT INTO `system_menu` VALUES (289, 288, '查询监控主机', 'monitor:monitor-host:query', 3, 10, 1, 1, 1, 0, NULL, NULL, NULL, '2025-08-23 17:02:02', '2025-08-23 17:02:02', 'admin', 'admin', 0); +INSERT INTO `system_menu` VALUES (290, 288, '修改监控主机', 'monitor:monitor-host:update', 3, 20, 1, 1, 1, 0, NULL, NULL, NULL, '2025-08-23 17:02:02', '2025-08-23 17:03:56', 'admin', 'admin', 0); +INSERT INTO `system_menu` VALUES (292, 288, '修改报警开关', 'monitor:monitor-host:update-switch', 3, 30, 1, 1, 1, 0, NULL, NULL, NULL, '2025-08-23 17:02:02', '2025-08-23 17:04:31', 'admin', 'admin', 0); +INSERT INTO `system_menu` VALUES (293, 64, '安装探针', 'asset:host:install-agent', 3, 110, 1, 1, 1, 0, NULL, NULL, NULL, '2025-08-31 20:18:14', '2025-08-31 20:18:14', 'admin', 'admin', 0); +INSERT INTO `system_menu` VALUES (294, 282, '主机监控详情', NULL, 2, 20, 0, 1, 1, 0, 'IconComputer', '', 'monitorDetail', '2025-09-03 23:03:20', '2025-09-03 23:03:55', 'admin', 'admin', 0); From df78fc59775ed8de439a5b35214e2c8ca2002160 Mon Sep 17 00:00:00 2001 From: lijiahangmax Date: Tue, 9 Sep 2025 23:46:02 +0800 Subject: [PATCH 12/17] =?UTF-8?q?:hammer:=20=E4=BF=AE=E6=94=B9=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=96=87=E4=BB=B6.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 13 +++++++++++-- .../src/main/resources/application-dev.yaml | 6 ++++++ .../src/main/resources/application-prod.yaml | 14 +++++++++++++- .../src/main/resources/application.yaml | 17 +++++++++++++---- 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index fd95d68b..8e4fa3bb 100644 --- a/.env.example +++ b/.env.example @@ -1,10 +1,13 @@ VOLUME_BASE=/data/orion-visor-space/docker-volumes -DEMO_MODE=false - SERVICE_PORT=1081 SPRING_PROFILES_ACTIVE=prod + +DEMO_MODE=false + +API_CORS=true SECRET_KEY=uQeacXV8b3isvKLK +API_EXPOSE_TOKEN=pmqeHOyZaumHm0Wt MYSQL_HOST=mysql MYSQL_PORT=3306 @@ -25,3 +28,9 @@ GUACD_SSH_USERNAME=guacd GUACD_SSH_PASSWORD=guacd GUACD_DRIVE_PATH=/drive +INFLUXDB_ENABLED=true +INFLUXDB_HOST=influxdb +INFLUXDB_PORT=8086 +INFLUXDB_ORG=orion-visor +INFLUXDB_BUCKET=metrics +INFLUXDB_TOKEN=Data@123456 diff --git a/orion-visor-launch/src/main/resources/application-dev.yaml b/orion-visor-launch/src/main/resources/application-dev.yaml index 09ff44f0..312c7a46 100644 --- a/orion-visor-launch/src/main/resources/application-dev.yaml +++ b/orion-visor-launch/src/main/resources/application-dev.yaml @@ -20,6 +20,12 @@ spring: threads: 2 netty-threads: 2 minimum-idle-size: 2 + influxdb: + enabled: ${INFLUXDB_ENABLED:true} + url: http://${INFLUXDB_HOST:127.0.0.1}:${INFLUXDB_PORT:8086} + org: ${INFLUXDB_ORG:orion-visor} + bucket: ${INFLUXDB_BUCKET:metrics} + token: ${INFLUXDB_TOKEN:Data@123456} boot: admin: client: diff --git a/orion-visor-launch/src/main/resources/application-prod.yaml b/orion-visor-launch/src/main/resources/application-prod.yaml index c86b4f11..c6cf2897 100644 --- a/orion-visor-launch/src/main/resources/application-prod.yaml +++ b/orion-visor-launch/src/main/resources/application-prod.yaml @@ -30,6 +30,12 @@ spring: threads: 4 netty-threads: 4 minimum-idle-size: 4 + influxdb: + enabled: ${INFLUXDB_ENABLED:true} + url: http://${INFLUXDB_HOST:127.0.0.1}:${INFLUXDB_PORT:8086} + org: ${INFLUXDB_ORG:orion-visor} + bucket: ${INFLUXDB_BUCKET:metrics} + token: ${INFLUXDB_TOKEN:Data@123456} quartz: properties: org: @@ -65,9 +71,15 @@ knife4j: orion: # 是否为演示模式 demo: ${DEMO_MODE:false} + api: + # 是否允许跨域 + cors: ${API_CORS:true} + expose: + # 暴露接口请求头值 + token: ${API_EXPOSE_TOKEN:pmqeHOyZaumHm0Wt} logging: printer: - mode: ROW + mode: PRETTY encrypt: aes: # 加密密钥 diff --git a/orion-visor-launch/src/main/resources/application.yaml b/orion-visor-launch/src/main/resources/application.yaml index a84f28e7..c04432cc 100644 --- a/orion-visor-launch/src/main/resources/application.yaml +++ b/orion-visor-launch/src/main/resources/application.yaml @@ -13,9 +13,9 @@ spring: # 文件上传相关配置项 multipart: # 单个文件大小 - max-file-size: 16MB + max-file-size: 128MB # 消息体大小 - max-request-size: 32MB + max-request-size: 128MB mvc: pathmatch: matching-strategy: ANT_PATH_MATCHER @@ -170,6 +170,12 @@ orion: prefix: ${orion.prefix}/api # 是否允许跨域 cors: true + # 对外服务 + expose: + # 暴露接口请求头 + header: X-Server-Token + # 暴露接口请求头值 + token: 96QEPoOChlMfAEPn websocket: # 公共 websocket 前缀 prefix: ${orion.prefix}/keep-alive @@ -202,14 +208,17 @@ orion: terminal: group: "terminal - 终端模块" path: "terminal" + monitor: + group: "monitor - 监控模块" + path: "monitor" logging: # 全局日志打印 printer: mode: PRETTY expression: 'execution (* org.dromara.visor..*.controller..*.*(..))' headers: - - user-agent,accept - - content-type + - User-Agent,Accept + - X-Agent-Version # 下面引用了 需要注意 field: ignore: From d2703661c8f28fc4a13e8cf98c6ab0f9b1d8473d Mon Sep 17 00:00:00 2001 From: lijiahangmax Date: Wed, 10 Sep 2025 01:43:11 +0800 Subject: [PATCH 13/17] =?UTF-8?q?:whale:=20=E4=BF=AE=E6=94=B9=20docker=20?= =?UTF-8?q?=E9=85=8D=E7=BD=AE.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 2 + .../{bug_report.yml => bug_report.yaml} | 0 .../{config.yml => config.yaml} | 0 ...ature_request.yml => feature_request.yaml} | 0 ...docker-publish.yml => docker-publish.yaml} | 6 ++- .github/workflows/e2e.yaml | 2 +- ...testing.yml => docker-compose-testing.yaml | 0 docker-compose.yml => docker-compose.yaml | 37 ++++++++++++++++++- docker/docker-build.sh | 26 +++++++++++++ docker/influxdb/Dockerfile | 8 ++++ docker/service/Dockerfile | 2 +- docker/service/entrypoint.sh | 2 +- .../log/core/annotation/IgnoreLog.java | 4 +- .../dromara/visor/launch/ReplaceVersion.java | 4 +- .../setting/components/login-setting.vue | 4 +- 15 files changed, 86 insertions(+), 11 deletions(-) rename .github/ISSUE_TEMPLATE/{bug_report.yml => bug_report.yaml} (100%) rename .github/ISSUE_TEMPLATE/{config.yml => config.yaml} (100%) rename .github/ISSUE_TEMPLATE/{feature_request.yml => feature_request.yaml} (100%) rename .github/workflows/{docker-publish.yml => docker-publish.yaml} (93%) rename docker-compose-testing.yml => docker-compose-testing.yaml (100%) rename docker-compose.yml => docker-compose.yaml (74%) create mode 100644 docker/influxdb/Dockerfile diff --git a/.env.example b/.env.example index 8e4fa3bb..c6400a8c 100644 --- a/.env.example +++ b/.env.example @@ -34,3 +34,5 @@ INFLUXDB_PORT=8086 INFLUXDB_ORG=orion-visor INFLUXDB_BUCKET=metrics INFLUXDB_TOKEN=Data@123456 +INFLUXDB_ADMIN_USERNAME=admin +INFLUXDB_ADMIN_PASSWORD=Data@123456 diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yaml similarity index 100% rename from .github/ISSUE_TEMPLATE/bug_report.yml rename to .github/ISSUE_TEMPLATE/bug_report.yaml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yaml similarity index 100% rename from .github/ISSUE_TEMPLATE/config.yml rename to .github/ISSUE_TEMPLATE/config.yaml diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yaml similarity index 100% rename from .github/ISSUE_TEMPLATE/feature_request.yml rename to .github/ISSUE_TEMPLATE/feature_request.yaml diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yaml similarity index 93% rename from .github/workflows/docker-publish.yml rename to .github/workflows/docker-publish.yaml index 16b4c128..93eb54d1 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yaml @@ -41,6 +41,10 @@ jobs: pnpm install pnpm build + - name: 📦️ Download instant-agent + working-directory: ./docker/service + run: wget https://github.com/lijiahangmax/orion-visor-agent/releases/latest/download/instance-agent-release.tar.gz -O instance-agent-release.tar.gz + - name: 📁 Prepare build context run: | cp -r ./sql ./docker/mysql/sql @@ -62,7 +66,7 @@ jobs: strategy: matrix: - service: [ adminer, guacd, mysql, redis, service, ui ] + service: [ adminer, guacd, mysql, redis, influxdb, service, ui ] env: GITHUB_REGISTRY: ghcr.io diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 87eef70c..1be49f4c 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -18,4 +18,4 @@ jobs: run: | sudo curl -L https://github.com/docker/compose/releases/download/v2.23.0/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose sudo chmod u+x /usr/local/bin/docker-compose - docker compose -f docker-compose-testing.yml up --build testing --exit-code-from testing --remove-orphans + docker compose -f docker-compose-testing.yaml up --build testing --exit-code-from testing --remove-orphans diff --git a/docker-compose-testing.yml b/docker-compose-testing.yaml similarity index 100% rename from docker-compose-testing.yml rename to docker-compose-testing.yaml diff --git a/docker-compose.yml b/docker-compose.yaml similarity index 74% rename from docker-compose.yml rename to docker-compose.yaml index 425cc70f..8810a491 100644 --- a/docker-compose.yml +++ b/docker-compose.yaml @@ -35,10 +35,18 @@ services: REDIS_PASSWORD: ${REDIS_PASSWORD:-Data@123456} REDIS_DATABASE: ${REDIS_DATABASE:-0} REDIS_DATA_VERSION: ${REDIS_DATA_VERSION:-1} + INFLUXDB_ENABLED: ${INFLUXDB_ENABLED:-true} + INFLUXDB_HOST: ${INFLUXDB_HOST:-influxdb} + INFLUXDB_PORT: ${INFLUXDB_PORT:-8086} + INFLUXDB_ORG: ${INFLUXDB_ORG:-orion-visor} + INFLUXDB_BUCKET: ${INFLUXDB_BUCKET:-metrics} + INFLUXDB_TOKEN: ${INFLUXDB_TOKEN:-Data@123456} GUACD_HOST: ${GUACD_HOST:-guacd} GUACD_PORT: ${GUACD_PORT:-4822} GUACD_DRIVE_PATH: ${GUACD_DRIVE_PATH:-/drive} - SECRET_KEY: ${SECRET_KEY:-uQeacXV8b3isvKLK} + SECRET_KEY: ${SECRET_KEY:-pmqeHOyZaumHm0Wt} + API_EXPOSE_TOKEN: ${API_EXPOSE_TOKEN:-uQeacXV8b3isvKLK} + API_CORS: ${API_CORS:-true} DEMO_MODE: ${DEMO_MODE:-false} volumes: - ${VOLUME_BASE:-/data/orion-visor-space/docker-volumes}/service/root-orion:/root/orion @@ -54,6 +62,8 @@ services: condition: service_healthy redis: condition: service_healthy + influxdb: + condition: service_healthy networks: - orion-visor-net @@ -100,6 +110,31 @@ services: networks: - orion-visor-net + influxdb: + image: registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-influxdb:latest + privileged: true + ports: + - "8086:8086" + environment: + DOCKER_INFLUXDB_INIT_MODE: setup + DOCKER_INFLUXDB_INIT_USERNAME: ${INFLUXDB_ADMIN_USERNAME:-admin} + DOCKER_INFLUXDB_INIT_PASSWORD: ${INFLUXDB_ADMIN_PASSWORD:-Data@123456} + DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: ${INFLUXDB_TOKEN:-Data@123456} + DOCKER_INFLUXDB_INIT_ORG: ${INFLUXDB_ORG:-orion-visor} + DOCKER_INFLUXDB_INIT_BUCKET: ${INFLUXDB_BUCKET:-metrics} + volumes: + - ${VOLUME_BASE:-/data/orion-visor-space/docker-volumes}/influxdb/data:/var/lib/influxdb2 + - ${VOLUME_BASE:-/data/orion-visor-space/docker-volumes}/influxdb/config:/etc/influxdb2 + restart: unless-stopped + healthcheck: + test: [ "CMD", "bash", "-c", "cat < /dev/null > /dev/tcp/127.0.0.1/8086" ] + interval: 15s + timeout: 5s + retries: 10 + start_period: 10s + networks: + - orion-visor-net + guacd: image: registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-guacd:latest ports: diff --git a/docker/docker-build.sh b/docker/docker-build.sh index 0b18e41d..d8b92646 100644 --- a/docker/docker-build.sh +++ b/docker/docker-build.sh @@ -46,6 +46,7 @@ declare -A images=( ["./service/Dockerfile"]="orion-visor-service" ["./mysql/Dockerfile"]="orion-visor-mysql" ["./redis/Dockerfile"]="orion-visor-redis" + ["./influxdb/Dockerfile"]="orion-visor-influxdb" ["./adminer/Dockerfile"]="orion-visor-adminer" ["./guacd/Dockerfile"]="orion-visor-guacd" ) @@ -68,6 +69,30 @@ function prepare_app_jar() { fi } +# 准备 instance-agent +function prepare_instance_agent() { + local target_file="./service/instance-agent-release.tar.gz" + if [ ! -f "$target_file" ]; then + echo "警告: $target_file 不存在, 正在尝试从 Github Release 下载..." + # 尝试从 GitHub Release 下载 + if curl -L --fail \ + --connect-timeout 30 --max-time 30 \ + https://github.com/lijiahangmax/orion-visor-agent/releases/latest/download/instance-agent-release.tar.gz \ + -o "$target_file"; then + echo "已成功下载到 $target_file" + fi + + # 如果下载失败, 提示用户手动下载 + echo "错误: 无法从 Release 获取 instance-agent-release.tar.gz" + echo "请手动从以下地址下载, 并放置到 $target_file" + echo " 1) https://github.com/lijiahangmax/orion-visor-agent/raw/main/instance-agent-release.tar.gz" + echo " 2) https://gitee.com/lijiahangmax/orion-visor-agent/raw/main/instance-agent-release.tar.gz" + exit 1 + else + echo "$target_file 已存在, 无需下载." + fi +} + # 准备前端 dist 目录 function prepare_dist_directory() { local source_dir="../orion-visor-ui/dist" @@ -185,6 +210,7 @@ fi # 检查资源 echo "正在检查并准备必要的构建资源..." prepare_app_jar +prepare_instance_agent prepare_dist_directory prepare_sql_directory echo "所有前置资源已准备完毕" diff --git a/docker/influxdb/Dockerfile b/docker/influxdb/Dockerfile new file mode 100644 index 00000000..2a47b7c5 --- /dev/null +++ b/docker/influxdb/Dockerfile @@ -0,0 +1,8 @@ +FROM --platform=$TARGETPLATFORM influxdb:2 + +# 系统时区 +ARG TZ=Asia/Shanghai + +# 设置时区 +RUN ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime && \ + echo "${TZ}" > /etc/timezone diff --git a/docker/service/Dockerfile b/docker/service/Dockerfile index 6d8a5a2a..15b3866a 100644 --- a/docker/service/Dockerfile +++ b/docker/service/Dockerfile @@ -24,7 +24,7 @@ RUN chmod +x /app/entrypoint.sh # 复制 jar 包 COPY ./service/orion-visor-launch.jar /app/app.jar # 复制探针包 -ADD ./service./instant-agent-release.tar.gz /app/instant-agent-release +ADD ./service/instant-agent-release.tar.gz /app/instant-agent-release # 启动检测 HEALTHCHECK --interval=15s --timeout=5s --retries=5 --start-period=10s \ diff --git a/docker/service/entrypoint.sh b/docker/service/entrypoint.sh index a19be15d..dac1d10f 100644 --- a/docker/service/entrypoint.sh +++ b/docker/service/entrypoint.sh @@ -1,6 +1,6 @@ #!/bin/sh -AGENT_RELEASE_DIR="/root/orion/visor/instant-agent-release" +AGENT_RELEASE_DIR="/root/orion/orion-visor/instant-agent-release" DEFAULT_AGENT_DIR="/app/instant-agent-release" # 确保父目录存在 diff --git a/orion-visor-framework/orion-visor-spring-boot-starter-log/src/main/java/org/dromara/visor/framework/log/core/annotation/IgnoreLog.java b/orion-visor-framework/orion-visor-spring-boot-starter-log/src/main/java/org/dromara/visor/framework/log/core/annotation/IgnoreLog.java index 7a9c335c..a72fcd92 100644 --- a/orion-visor-framework/orion-visor-spring-boot-starter-log/src/main/java/org/dromara/visor/framework/log/core/annotation/IgnoreLog.java +++ b/orion-visor-framework/orion-visor-spring-boot-starter-log/src/main/java/org/dromara/visor/framework/log/core/annotation/IgnoreLog.java @@ -29,8 +29,8 @@ import java.lang.annotation.*; /** * 不执行统一日志打印 *

- * 如果设置在方法上,则忽略该方法的日志打印 - * 如果设置到参数上,则忽略该参数的日志打印 + * 如果设置在方法上, 则忽略该方法的日志打印 + * 如果设置到参数上, 则忽略该参数的日志打印 * * @author Jiahang Li * @version 1.0.0 diff --git a/orion-visor-launch/src/test/java/org/dromara/visor/launch/ReplaceVersion.java b/orion-visor-launch/src/test/java/org/dromara/visor/launch/ReplaceVersion.java index 93d05ef7..f52c75e2 100644 --- a/orion-visor-launch/src/test/java/org/dromara/visor/launch/ReplaceVersion.java +++ b/orion-visor-launch/src/test/java/org/dromara/visor/launch/ReplaceVersion.java @@ -48,8 +48,8 @@ public class ReplaceVersion { private static final String[] DOCKER_FILES = new String[]{ "docker/docker-build.sh", "docker/project-build.sh", - "docker-compose.yml", - "docker-compose-testing.yml" + "docker-compose.yaml", + "docker-compose-testing.yaml" }; private static final String[] POM_FILES = new String[]{ diff --git a/orion-visor-ui/src/views/system/setting/components/login-setting.vue b/orion-visor-ui/src/views/system/setting/components/login-setting.vue index 19ef5ab8..ec8e164c 100644 --- a/orion-visor-ui/src/views/system/setting/components/login-setting.vue +++ b/orion-visor-ui/src/views/system/setting/components/login-setting.vue @@ -35,7 +35,7 @@ checked-text="开启" unchecked-text="关闭" /> @@ -105,7 +105,7 @@ From bf4b1f97024d65185ae27a2515e302c417a7e43e Mon Sep 17 00:00:00 2001 From: lijiahangmax Date: Wed, 10 Sep 2025 01:44:58 +0800 Subject: [PATCH 14/17] =?UTF-8?q?:bookmark:=20=E4=BF=AE=E6=94=B9=E7=89=88?= =?UTF-8?q?=E6=9C=AC.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yaml | 2 +- docker/docker-build.sh | 2 +- docker/project-build.sh | 2 +- .../main/java/org/dromara/visor/common/constant/AppConst.java | 2 +- orion-visor-dependencies/pom.xml | 2 +- .../test/java/org/dromara/visor/launch/ReplaceVersion.java | 4 ++-- orion-visor-ui/.env.development | 2 +- orion-visor-ui/.env.production | 2 +- orion-visor-ui/package.json | 2 +- pom.xml | 2 +- 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 8810a491..845935d2 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,6 +1,6 @@ version: '3.3' -# latest = 2.4.3 +# latest = 2.5.0 # 支持以下源 # lijiahangmax/* diff --git a/docker/docker-build.sh b/docker/docker-build.sh index d8b92646..e9797cae 100644 --- a/docker/docker-build.sh +++ b/docker/docker-build.sh @@ -7,7 +7,7 @@ set -e source ./project-build.sh "$@" # 版本号 -version=2.4.3 +version=2.5.0 # 是否推送镜像 push_image=false # 是否构建 latest diff --git a/docker/project-build.sh b/docker/project-build.sh index c944f22c..e4e5c237 100644 --- a/docker/project-build.sh +++ b/docker/project-build.sh @@ -4,7 +4,7 @@ set -e # DockerContext: orion-visor # 版本号 -version=2.4.3 +version=2.5.0 # 是否构建 service export build_service=false # 是否构建 ui diff --git a/orion-visor-common/src/main/java/org/dromara/visor/common/constant/AppConst.java b/orion-visor-common/src/main/java/org/dromara/visor/common/constant/AppConst.java index 31ecc24f..caa4bc74 100644 --- a/orion-visor-common/src/main/java/org/dromara/visor/common/constant/AppConst.java +++ b/orion-visor-common/src/main/java/org/dromara/visor/common/constant/AppConst.java @@ -36,7 +36,7 @@ public interface AppConst extends OrionConst { /** * 同 ${orion.version} 迭代时候需要手动更改 */ - String VERSION = "2.4.3"; + String VERSION = "2.5.0"; /** * 同 ${spring.application.name} diff --git a/orion-visor-dependencies/pom.xml b/orion-visor-dependencies/pom.xml index 6991b4f3..86ff5680 100644 --- a/orion-visor-dependencies/pom.xml +++ b/orion-visor-dependencies/pom.xml @@ -14,7 +14,7 @@ https://github.com/dromara/orion-visor - 2.4.3 + 2.5.0 2.7.17 2.7.15 1.5.0 diff --git a/orion-visor-launch/src/test/java/org/dromara/visor/launch/ReplaceVersion.java b/orion-visor-launch/src/test/java/org/dromara/visor/launch/ReplaceVersion.java index f52c75e2..fe8edd6f 100644 --- a/orion-visor-launch/src/test/java/org/dromara/visor/launch/ReplaceVersion.java +++ b/orion-visor-launch/src/test/java/org/dromara/visor/launch/ReplaceVersion.java @@ -39,9 +39,9 @@ import java.util.function.Function; */ public class ReplaceVersion { - private static final String TARGET_VERSION = "2.4.2"; + private static final String TARGET_VERSION = "2.4.3"; - private static final String REPLACE_VERSION = "2.4.3"; + private static final String REPLACE_VERSION = "2.5.0"; private static final String PATH = new File("").getAbsolutePath(); diff --git a/orion-visor-ui/.env.development b/orion-visor-ui/.env.development index cde81650..14e7553f 100644 --- a/orion-visor-ui/.env.development +++ b/orion-visor-ui/.env.development @@ -3,4 +3,4 @@ VITE_API_BASE_URL=http://127.0.0.1:9200/orion-visor/api # websocket 路径 VITE_WS_BASE_URL=ws://127.0.0.1:9200/orion-visor/keep-alive # 版本号 -VITE_APP_VERSION=2.4.3 +VITE_APP_VERSION=2.5.0 diff --git a/orion-visor-ui/.env.production b/orion-visor-ui/.env.production index e99ebffc..4b89d4ec 100644 --- a/orion-visor-ui/.env.production +++ b/orion-visor-ui/.env.production @@ -3,4 +3,4 @@ VITE_API_BASE_URL=/orion-visor/api # websocket 路径 VITE_WS_BASE_URL=/orion-visor/keep-alive # 版本号 -VITE_APP_VERSION=2.4.3 +VITE_APP_VERSION=2.5.0 diff --git a/orion-visor-ui/package.json b/orion-visor-ui/package.json index fbf966f0..77bbb39f 100644 --- a/orion-visor-ui/package.json +++ b/orion-visor-ui/package.json @@ -1,7 +1,7 @@ { "name": "orion-visor-ui", "description": "Orion Visor UI", - "version": "2.4.3", + "version": "2.5.0", "private": true, "author": "Jiahang Li", "license": "Apache 2.0", diff --git a/pom.xml b/pom.xml index fe8eacc0..0e981882 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ - 2.4.3 + 2.5.0 8 8 3.0.0-M5 From 697d29c6f2f3dea41c83b1330f08e5fa82c91083 Mon Sep 17 00:00:00 2001 From: lijiahangmax Date: Wed, 10 Sep 2025 18:11:54 +0800 Subject: [PATCH 15/17] =?UTF-8?q?:hammer:=20=E4=BF=AE=E6=94=B9=E9=85=8D?= =?UTF-8?q?=E7=BD=AE.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/bug_report.yaml | 3 +-- .github/ISSUE_TEMPLATE/feature_request.yaml | 2 +- .github/workflows/docker-publish.yaml | 2 +- README.md | 7 ++++++- docker/service/Dockerfile | 2 +- docker/service/entrypoint.sh | 4 ++-- .../visor/common/constant/FileConst.java | 12 +++++++----- .../asset/api/impl/HostAgentApiImpl.java | 3 +++ .../visor/module/asset/entity/vo/HostVO.java | 15 ++++++++++++--- .../agent/intstall/AbstractAgentInstaller.java | 4 ++-- .../service/impl/HostAgentServiceImpl.java | 18 ++++++++---------- .../components/monitor-host-card-list.vue | 4 ++-- .../components/monitor-host-table.vue | 4 ++-- .../types/use-monitor-host-list.ts | 4 ++-- sql/init-4-data.sql | 2 +- 15 files changed, 51 insertions(+), 35 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 8653f407..67cc45e2 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -1,7 +1,7 @@ name: 错误报告 description: File a bug report. title: "[错误报告]: " -labels: [""] +labels: [ "" ] body: - type: markdown attributes: @@ -42,7 +42,6 @@ body: - 其他 validations: required: true - - type: textarea id: what-happened attributes: diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 9d9892be..59730be8 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -1,7 +1,7 @@ name: 功能改进 description: 提出新功能建议 (请提交到需求收集帖) title: "[功能建议]: " -labels: [""] +labels: [ "" ] body: - type: markdown attributes: diff --git a/.github/workflows/docker-publish.yaml b/.github/workflows/docker-publish.yaml index 93eb54d1..be3a9c77 100644 --- a/.github/workflows/docker-publish.yaml +++ b/.github/workflows/docker-publish.yaml @@ -41,7 +41,7 @@ jobs: pnpm install pnpm build - - name: 📦️ Download instant-agent + - name: 📦️ Download instance-agent working-directory: ./docker/service run: wget https://github.com/lijiahangmax/orion-visor-agent/releases/latest/download/instance-agent-release.tar.gz -O instance-agent-release.tar.gz diff --git a/README.md b/README.md index 289002d2..9c0b084c 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ * **文件管理**:支持远程主机 SFTP 大文件的批量上传、下载和在线编辑等操作。 * **批量操作**:支持批量执行主机命令、多主机文件分发等功能。 * **计划任务**:支持配置 cron 表达式,定时执行主机命令。 -* **系统监控**:(开发中)。 +* **系统监控**:支持对主机 CPU、内存、磁盘、网络等系统指标的监控和告警。 * **安全可靠**:动态配置权限,记录用户操作日志,提供简单的审计功能。 ## 演示环境 @@ -88,6 +88,7 @@ docker compose up -d * SpringBoot 2.7+ * Mysql 8.0+ * Redis 6.0+ +* InfluxDB 2.7+ * Vue 3.5+ * Arco Design 2.56+ @@ -155,6 +156,10 @@ QQ群: 755242157 本项目遵循 [Apache-2.0](https://github.com/dromara/orion-visor/blob/main/LICENSE) 开源许可证。 +## 贡献者 + +[![Contributors](https://contri.buzz/api/wall?repo=dromara/orion-visor)](https://github.com/dromara/orion-visor, "Contributors") + ## Gitee 最有价值的开源项目 GVP ![GVP](docs/assets/gvp.jpg?time=20250627 "GVP") diff --git a/docker/service/Dockerfile b/docker/service/Dockerfile index 15b3866a..b04ef717 100644 --- a/docker/service/Dockerfile +++ b/docker/service/Dockerfile @@ -24,7 +24,7 @@ RUN chmod +x /app/entrypoint.sh # 复制 jar 包 COPY ./service/orion-visor-launch.jar /app/app.jar # 复制探针包 -ADD ./service/instant-agent-release.tar.gz /app/instant-agent-release +ADD ./service/instance-agent-release.tar.gz /app/instance-agent-release # 启动检测 HEALTHCHECK --interval=15s --timeout=5s --retries=5 --start-period=10s \ diff --git a/docker/service/entrypoint.sh b/docker/service/entrypoint.sh index dac1d10f..d163a824 100644 --- a/docker/service/entrypoint.sh +++ b/docker/service/entrypoint.sh @@ -1,7 +1,7 @@ #!/bin/sh -AGENT_RELEASE_DIR="/root/orion/orion-visor/instant-agent-release" -DEFAULT_AGENT_DIR="/app/instant-agent-release" +AGENT_RELEASE_DIR="/root/orion/orion-visor/instance-agent-release" +DEFAULT_AGENT_DIR="/app/instance-agent-release" # 确保父目录存在 mkdir -p "$(dirname "$AGENT_RELEASE_DIR")" diff --git a/orion-visor-common/src/main/java/org/dromara/visor/common/constant/FileConst.java b/orion-visor-common/src/main/java/org/dromara/visor/common/constant/FileConst.java index 1bc002fb..1a68275a 100644 --- a/orion-visor-common/src/main/java/org/dromara/visor/common/constant/FileConst.java +++ b/orion-visor-common/src/main/java/org/dromara/visor/common/constant/FileConst.java @@ -37,15 +37,17 @@ public interface FileConst { String SCRIPT = "script"; - String INSTANT_AGENT_PATH = "instant-agent"; + String INSTANCE_AGENT_PATH = "instance-agent"; - String INSTANT_AGENT_NAME = "instant_agent"; + String INSTANCE_AGENT_NAME = "instance_agent"; - String INSTANT_AGENT_RELEASE = "instant-agent-release"; + String INSTANCE_AGENT_FILE_FORMAT = "instance_agent_{}_{}{}"; - String INSTANT_AGENT_RELEASE_TEMP = "instant-agent-release-temp"; + String INSTANCE_AGENT_RELEASE = "instance-agent-release"; - String INSTANT_AGENT_RELEASE_TAR_GZ = "instant-agent-release.tar.gz"; + String INSTANCE_AGENT_RELEASE_TEMP = "instance-agent-release-temp"; + + String INSTANCE_AGENT_RELEASE_TAR_GZ = "instance-agent-release.tar.gz"; String VERSION = ".version"; diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/api/impl/HostAgentApiImpl.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/api/impl/HostAgentApiImpl.java index 84a13b0c..e5d31a5c 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/api/impl/HostAgentApiImpl.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/api/impl/HostAgentApiImpl.java @@ -83,6 +83,9 @@ public class HostAgentApiImpl implements HostAgentApi { .filter(Strings::isNotBlank) .map(Long::parseLong) .collect(Collectors.toList()); + if (logIdList.isEmpty()) { + return Lists.empty(); + } // 查询数据库 return hostAgentLogDAO.selectBatchIds(logIdList) .stream() diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/vo/HostVO.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/vo/HostVO.java index fed4d555..623155ca 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/vo/HostVO.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/entity/vo/HostVO.java @@ -72,12 +72,21 @@ public class HostVO implements Serializable { @Schema(description = "主机地址") private String address; - @Schema(description = "主机端口") - private Integer port; - @Schema(description = "主机状态") private String status; + @Schema(description = "agentKey") + private String agentKey; + + @Schema(description = "探针版本") + private String agentVersion; + + @Schema(description = "探针安装状态") + private Integer agentInstallStatus; + + @Schema(description = "探针在线状态") + private Integer agentOnlineStatus; + @Schema(description = "描述") private String description; diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/agent/intstall/AbstractAgentInstaller.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/agent/intstall/AbstractAgentInstaller.java index d4a3ef64..38c8a3db 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/agent/intstall/AbstractAgentInstaller.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/agent/intstall/AbstractAgentInstaller.java @@ -86,7 +86,7 @@ public abstract class AbstractAgentInstaller implements AgentInstaller { this.params = params; this.logId = params.getLogId(); this.startScriptName = Const.START + HostOsTypeEnum.of(params.getOsType()).getScriptSuffix(); - this.uploadAgentName = FileConst.INSTANT_AGENT_NAME + HostOsTypeEnum.of(params.getOsType()).getBinarySuffix(); + this.uploadAgentName = FileConst.INSTANCE_AGENT_NAME + HostOsTypeEnum.of(params.getOsType()).getBinarySuffix(); } @Override @@ -154,7 +154,7 @@ public abstract class AbstractAgentInstaller implements AgentInstaller { protected String getAgentHomePath() { return PathUtils.buildAppPath(HostOsTypeEnum.WINDOWS.name().equals(params.getOsType()), sshConfig.getUsername(), - FileConst.INSTANT_AGENT_PATH) + Const.SLASH; + FileConst.INSTANCE_AGENT_PATH) + Const.SLASH; } /** diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostAgentServiceImpl.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostAgentServiceImpl.java index d22d15d4..5b45b621 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostAgentServiceImpl.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostAgentServiceImpl.java @@ -75,8 +75,6 @@ import java.util.stream.Collectors; @Service public class HostAgentServiceImpl implements HostAgentService { - private static final String AGENT_FILE_FORMAT = "agent_{}_{}{}"; - private String localVersion; @Value("${orion.api.expose.token}") @@ -95,7 +93,7 @@ public class HostAgentServiceImpl implements HostAgentService { public void readLocalAgentVersion() { log.info("HostAgentService-readLocalAgentVersion start"); // 文件路径 - String path = PathUtils.getOrionPath(FileConst.INSTANT_AGENT_RELEASE + Const.SLASH + FileConst.VERSION); + String path = PathUtils.getOrionPath(FileConst.INSTANCE_AGENT_RELEASE + Const.SLASH + FileConst.VERSION); log.info("HostAgentService-readLocalAgentVersion path: {}", path); try { if (!Files1.isFile(path)) { @@ -191,9 +189,9 @@ public class HostAgentServiceImpl implements HostAgentService { Valid.notBlank(fileName, ErrorMessage.FILE_EXTENSION_TYPE); Valid.isTrue(fileName.endsWith(Const.SUFFIX_TAR_GZ), ErrorMessage.FILE_EXTENSION_TYPE); // 保存文件 - String releaseDir = PathUtils.getOrionPath(FileConst.INSTANT_AGENT_RELEASE); - String releaseTempDir = PathUtils.getOrionPath(FileConst.INSTANT_AGENT_RELEASE_TEMP); - File releaseTempFile = new File(releaseTempDir + Const.SLASH + FileConst.INSTANT_AGENT_RELEASE_TAR_GZ); + String releaseDir = PathUtils.getOrionPath(FileConst.INSTANCE_AGENT_RELEASE); + String releaseTempDir = PathUtils.getOrionPath(FileConst.INSTANCE_AGENT_RELEASE_TEMP); + File releaseTempFile = new File(releaseTempDir + Const.SLASH + FileConst.INSTANCE_AGENT_RELEASE_TAR_GZ); log.info("HostAgentService.installAgent start releaseTempDir: {}, releaseTempFile: {}", releaseTempDir, releaseTempFile.getAbsolutePath()); try { // 创建目录 @@ -269,7 +267,7 @@ public class HostAgentServiceImpl implements HostAgentService { Valid.isTrue(supportSsh, ErrorMessage.PLEASE_CHECK_HOST_SSH, host.getName()); // 文件名称 HostOsTypeEnum os = HostOsTypeEnum.of(host.getOsType()); - String agentFileName = Strings.format(AGENT_FILE_FORMAT, + String agentFileName = Strings.format(FileConst.INSTANCE_AGENT_FILE_FORMAT, os.name().toLowerCase(), host.getArchType().toLowerCase(), os.getBinarySuffix()); @@ -278,9 +276,9 @@ public class HostAgentServiceImpl implements HostAgentService { .hostId(host.getId()) .osType(host.getOsType()) .agentKey(host.getAgentKey()) - .agentFilePath(PathUtils.getOrionPath(FileConst.INSTANT_AGENT_RELEASE + Const.SLASH + agentFileName)) - .configFilePath(PathUtils.getOrionPath(FileConst.INSTANT_AGENT_RELEASE + Const.SLASH + FileConst.CONFIG_YAML)) - .startScriptPath(PathUtils.getOrionPath(FileConst.INSTANT_AGENT_RELEASE + Const.SLASH + Const.START + os.getScriptSuffix())) + .agentFilePath(PathUtils.getOrionPath(FileConst.INSTANCE_AGENT_RELEASE + Const.SLASH + agentFileName)) + .configFilePath(PathUtils.getOrionPath(FileConst.INSTANCE_AGENT_RELEASE + Const.SLASH + FileConst.CONFIG_YAML)) + .startScriptPath(PathUtils.getOrionPath(FileConst.INSTANCE_AGENT_RELEASE + Const.SLASH + Const.START + os.getScriptSuffix())) .build(); taskParams.add(params); // 添加待检查文件 diff --git a/orion-visor-ui/src/views/monitor/monitor-host/components/monitor-host-card-list.vue b/orion-visor-ui/src/views/monitor/monitor-host/components/monitor-host-card-list.vue index 0b6fdd94..e93ce296 100644 --- a/orion-visor-ui/src/views/monitor/monitor-host/components/monitor-host-card-list.vue +++ b/orion-visor-ui/src/views/monitor/monitor-host/components/monitor-host-card-list.vue @@ -287,14 +287,14 @@ @click="setInstallSuccess(record.installLog)"> 安装成功 - + - {{ toggleDictValue(AlarmSwitchKey, record.alarmSwitch, 'label') + '报警' }} + {{ toggleDictValue(AlarmSwitchKey, record.alarmSwitch, 'label') + '告警' }} diff --git a/orion-visor-ui/src/views/monitor/monitor-host/components/monitor-host-table.vue b/orion-visor-ui/src/views/monitor/monitor-host/components/monitor-host-table.vue index fe224806..2d65c7f1 100644 --- a/orion-visor-ui/src/views/monitor/monitor-host/components/monitor-host-table.vue +++ b/orion-visor-ui/src/views/monitor/monitor-host/components/monitor-host-table.vue @@ -317,14 +317,14 @@ @click="setInstallSuccess(record.installLog)"> 安装成功 - + - {{ toggleDictValue(AlarmSwitchKey, record.alarmSwitch, 'label') + '报警' }} + {{ toggleDictValue(AlarmSwitchKey, record.alarmSwitch, 'label') + '告警' }} diff --git a/orion-visor-ui/src/views/monitor/monitor-host/types/use-monitor-host-list.ts b/orion-visor-ui/src/views/monitor/monitor-host/types/use-monitor-host-list.ts index 6e072b3e..3fe83588 100644 --- a/orion-visor-ui/src/views/monitor/monitor-host/types/use-monitor-host-list.ts +++ b/orion-visor-ui/src/views/monitor/monitor-host/types/use-monitor-host-list.ts @@ -107,13 +107,13 @@ export default function useMonitorHostList(options: UseMonitorHostListOptions) { }); }; - // 更新报警开关 + // 更新告警开关 const toggleAlarmSwitch = async (record: MonitorHostQueryResponse) => { const dict = toggleDict(AlarmSwitchKey, record.alarmSwitch); Modal.confirm({ title: `${dict.label}确认`, titleAlign: 'start', - content: `确定要${dict.label}报警功能吗?`, + content: `确定要${dict.label}告警功能吗?`, okText: '确定', onOk: async () => { try { diff --git a/sql/init-4-data.sql b/sql/init-4-data.sql index 18bf65d3..1ce094b5 100644 --- a/sql/init-4-data.sql +++ b/sql/init-4-data.sql @@ -664,6 +664,6 @@ INSERT INTO `system_menu` VALUES (287, 283, '删除监控指标', 'monitor:monit INSERT INTO `system_menu` VALUES (288, 282, '主机监控', NULL, 2, 10, 1, 1, 1, 0, 'IconComputer', NULL, 'monitorHost', '2025-08-23 17:02:02', '2025-08-24 22:47:03', 'admin', 'admin', 0); INSERT INTO `system_menu` VALUES (289, 288, '查询监控主机', 'monitor:monitor-host:query', 3, 10, 1, 1, 1, 0, NULL, NULL, NULL, '2025-08-23 17:02:02', '2025-08-23 17:02:02', 'admin', 'admin', 0); INSERT INTO `system_menu` VALUES (290, 288, '修改监控主机', 'monitor:monitor-host:update', 3, 20, 1, 1, 1, 0, NULL, NULL, NULL, '2025-08-23 17:02:02', '2025-08-23 17:03:56', 'admin', 'admin', 0); -INSERT INTO `system_menu` VALUES (292, 288, '修改报警开关', 'monitor:monitor-host:update-switch', 3, 30, 1, 1, 1, 0, NULL, NULL, NULL, '2025-08-23 17:02:02', '2025-08-23 17:04:31', 'admin', 'admin', 0); +INSERT INTO `system_menu` VALUES (292, 288, '修改告警开关', 'monitor:monitor-host:update-switch', 3, 30, 1, 1, 1, 0, NULL, NULL, NULL, '2025-08-23 17:02:02', '2025-08-23 17:04:31', 'admin', 'admin', 0); INSERT INTO `system_menu` VALUES (293, 64, '安装探针', 'asset:host:install-agent', 3, 110, 1, 1, 1, 0, NULL, NULL, NULL, '2025-08-31 20:18:14', '2025-08-31 20:18:14', 'admin', 'admin', 0); INSERT INTO `system_menu` VALUES (294, 282, '主机监控详情', NULL, 2, 20, 0, 1, 1, 0, 'IconComputer', '', 'monitorDetail', '2025-09-03 23:03:20', '2025-09-03 23:03:55', 'admin', 'admin', 0); From 3156ae1dff09620f1a58580456f942f113b24303 Mon Sep 17 00:00:00 2001 From: lijiahangmax Date: Wed, 10 Sep 2025 21:53:53 +0800 Subject: [PATCH 16/17] =?UTF-8?q?:pencil2:=20=E6=B7=BB=E5=8A=A0=E5=88=87?= =?UTF-8?q?=E5=9B=BE.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 +++++ docs/assets/screenshot/monitor-detail.png | Bin 0 -> 237416 bytes docs/assets/screenshot/monitor-list.png | Bin 0 -> 157058 bytes .../compoments/metrics-chart.vue | 2 +- .../components/monitor-host-card-list.vue | 8 +++++++- .../components/monitor-host-table.vue | 8 +++++++- 6 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 docs/assets/screenshot/monitor-detail.png create mode 100644 docs/assets/screenshot/monitor-list.png diff --git a/README.md b/README.md index 9c0b084c..398cbea4 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,11 @@ docker compose up -d ![主机列表](docs/assets/screenshot/host-list.png?time=20250627 "主机列表") +#### 主机监控 + +![主机监控](docs/assets/screenshot/monitor-list.png?time=20250627 "主机监控") +![监控详情](docs/assets/screenshot/monitor-detail.png?time=20250627 "监控详情") + #### 批量执行 ![批量执行](docs/assets/screenshot/exec-command.png?time=20250627 "批量执行") diff --git a/docs/assets/screenshot/monitor-detail.png b/docs/assets/screenshot/monitor-detail.png new file mode 100644 index 0000000000000000000000000000000000000000..ca38b3d6da3ef35bc9072fabce19c5edc5ad1b86 GIT binary patch literal 237416 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%0D%!vYp~ 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

m$hei4zZedcTbaT-D!rUZ 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

(_&F@R-kDb+a>8->5>Kg2ia&0tk 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 z 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