Compare commits

..
27 Commits
Author SHA1 Message Date
Darc Z 3b5ee464a7 Add DDNS support (#324)
* feat: add ddns updater framework

Note: no functionality implemented yet

* feat: add webhook ddns provider

* feat: update dashboard template

* fix: check nil and cron task string

* fix: webhook string formated with unexcepted param

* fix: webhook header split error

* feat: cloudflare ddns provider

* refract: move ddns update trigger into ReportSystemInfo

* lang: update other languages text

* fix: clear codes and logics

* fix: move update ddns to goroutine to avoid blocking

* fix: clear unused codes

* fix: update timestamp to prevent cache
2024-02-24 21:10:27 +08:00
github-actions[bot] c4b2c47beb update contributors[no ci] 2024-02-23 14:55:59 +00:00
1ridic 0835f13f28 优化安装逻辑 (#325)
* Optimize install process

If `getenforce` is not exist, then do not execute
it. This fixes the `command not found` error on
Arch Linux systems without SELinux installed.

* bump installer version
2024-02-23 22:55:42 +08:00
github-actions[bot] ee3cda8aed update contributors[no ci] 2024-02-21 12:19:50 +00:00
604fc7d4c0 Arch 修复 nezha-agent 主目录遗留问题 (#323)
* Arch 修复 nezha-agent 主目录遗留问题

使用useradd -m创建的用户会产生主目录文件夹,使用userdel需要添加-r参数

* Update install_en.sh

* bump installer version
2024-02-21 20:19:34 +08:00
github-actions[bot] 5c549261dd update contributors[no ci] 2024-02-19 13:00:09 +00:00
HsukqiLee 00041c0ebd 增加了网络页面对 Neko Meui 主题的支持 (#322)
* 修改 Neko-mdui 主题的菜单栏

增加了“网络”选项卡

* 增加 Neko-mdui 主题的网络页面
2024-02-19 20:59:49 +08:00
xykt dbfea9a00b 对历史延迟图表进行了相关修复/美化工作 (#321)
* Update network.html

修复以下问题:
- 延迟默认最高300ms,超过后默认抹平
- 曲线上有很多ping值为0的无效散点,导致毛刺很多,干扰效果
- 图标的y轴比例失调,上方大片留白,大大降低了有效显示区域
- 默认只显示最近不到半小时左右的延迟表现,想看全天需要拖动,影响效果
- 曲线不显示极大极小值,不够直观

* Update config.go

修复了以下问题:
- 延迟默认最高300ms,超过后默认抹平
- 曲线上有很多ping值为0的无效散点,导致毛刺很多,干扰效果
- 图标的y轴比例失调,上方大片留白,大大降低了有效显示区域
- 默认只显示最近不到半小时左右的延迟表现,想看全天需要拖动,影响效果
- 曲线不显示极大极小值,不够直观
2024-02-19 13:52:43 +08:00
github-actions[bot] e8b208edfa update contributors[no ci] 2024-02-18 12:00:59 +00:00
Ko no dio 7d38634933 feat: add index on monitor history (#320) 2024-02-18 20:00:36 +08:00
github-actions[bot] 5bd4b57cbf update contributors[no ci] 2024-02-14 05:12:49 +00:00
Ko no dioandLvGJ fdb2fd8839 fix: revert service html and block service in backend (#318)
Co-authored-by: LvGJ <[email protected]>
2024-02-14 13:12:24 +08:00
github-actions[bot] f3fac9325c update contributors[no ci] 2024-02-13 15:09:58 +00:00
Ko no dioandLvGJ 626613441d fix: add toggle to service (#317)
* fix: short nework word

* fix: add toggle on service

* fix: delete console log update main.js version

---------

Co-authored-by: LvGJ <[email protected]>
2024-02-13 23:09:40 +08:00
github-actions[bot] b86b73a433 update contributors[no ci] 2024-02-12 06:16:26 +00:00
Ko no dioandLvGJ e8b8e59bd7 feat: add network monitor hitory (#316) · 三网ping
* feat: add network monitor hitory

* fix: revert proto change and add indexStore

* fix: update monitor delete unuse monitor history

* fix: delete unuse monitor type

---------

Co-authored-by: LvGJ <[email protected]>
2024-02-12 14:16:04 +08:00
naiba c9bcba6f28 ♻️ 更换可用镜像地址 2024-02-09 20:59:22 +08:00
github-actions[bot] ad9dc5470a update contributors[no ci] 2024-01-18 05:35:21 +00:00
黑歌 8539fb6559 replace daocloud with fgit (#315)
* Update add custom mirror

* Update add custom mirror

* bump install version
2024-01-18 13:34:58 +08:00
github-actions[bot] 11bcc47d49 update contributors[no ci] 2023-12-26 02:26:27 +00:00
Crazy bb789f7652 add supervise daemon for alpine (#313)
alpine在自动更新agent以后不会自动重启agent,导致批量掉线,添加守护进程以后问题解决
2023-12-26 10:26:11 +08:00
dependabot[bot] 51c1e41123 Bump golang.org/x/crypto from 0.15.0 to 0.17.0 (#312)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.15.0 to 0.17.0.
- [Commits](https://github.com/golang/crypto/compare/v0.15.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-19 09:43:33 +08:00
naiba 83efdc6258 replace statifile cdn with bytedance cdn 2023-12-11 21:53:56 +08:00
github-actions[bot] bf58ad24b5 update contributors[no ci] 2023-12-08 13:17:02 +00:00
spiritlhl 53581d48fc 更新判断面板是否是通过docker安装的逻辑 (#311)
* 更新判断面板是否是通过docker安装的逻辑

* Updated logic for determining whether a panel is installed via docker or not
2023-12-08 21:16:36 +08:00
naiba b00a00e791 Add comment about missing riscv64 image in latest Alpine image 2023-12-08 09:55:17 +08:00
github-actions[bot] b91f29077c update contributors[no ci] 2023-12-07 13:59:01 +00:00
51 changed files with 2534 additions and 696 deletions
+1
View File
@@ -1,3 +1,4 @@
# latest 镜像还没有 riscv64 https://hub.docker.com/_/alpine/tags
FROM alpine:edge
ARG TARGETOS
+72 -54
View File
@@ -4,7 +4,7 @@
<br>
<small><i>LOGO designed by <a href="https://xio.ng" target="_blank">熊大</a> .</i></small>
<br><br>
<img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/naiba/nezha?color=brightgreen&style=for-the-badge&logo=github&label=Dashboard">&nbsp;<img src="https://img.shields.io/github/v/release/nezhahq/agent?color=brightgreen&label=Agent&style=for-the-badge&logo=github">&nbsp;<img src="https://img.shields.io/github/actions/workflow/status/nezhahq/agent/agent.yml?label=Agent%20CI&logo=github&style=for-the-badge">&nbsp;<img src="https://img.shields.io/badge/Installer-v0.15.3-brightgreen?style=for-the-badge&logo=linux">
<img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/naiba/nezha?color=brightgreen&style=for-the-badge&logo=github&label=Dashboard">&nbsp;<img src="https://img.shields.io/github/v/release/nezhahq/agent?color=brightgreen&label=Agent&style=for-the-badge&logo=github">&nbsp;<img src="https://img.shields.io/github/actions/workflow/status/nezhahq/agent/agent.yml?label=Agent%20CI&logo=github&style=for-the-badge">&nbsp;<img src="https://img.shields.io/badge/Installer-v0.15.8-brightgreen?style=for-the-badge&logo=linux">
<br>
<br>
<p>:trollface: <b>Nezha Monitoring: Self-hostable, lightweight, servers and websites monitoring and O&M tool.</b></p>
@@ -65,20 +65,26 @@ You can change the dashboard language in the settings page (`/setting`) after th
<a href="https://github.com/cantoblanco" title="Kris">
<img src="https://avatars.githubusercontent.com/u/116849421?v=4" width="50;" alt="Kris"/>
</a>
<a href="https://github.com/CosmosZ-code" title="CosmosZ-code">
<img src="https://avatars.githubusercontent.com/u/81398224?v=4" width="50;" alt="CosmosZ-code"/>
<a href="https://github.com/spiritLHLS" title="spiritlhl">
<img src="https://avatars.githubusercontent.com/u/103393591?v=4" width="50;" alt="spiritlhl"/>
</a>
<a href="https://github.com/liuyanxi975" title="刘颜溪">
<img src="https://avatars.githubusercontent.com/u/24417037?v=4" width="50;" alt="刘颜溪"/>
</a>
<a href="https://github.com/applexad" title="Applexad">
<img src="https://avatars.githubusercontent.com/u/35923940?v=4" width="50;" alt="Applexad"/>
<a href="https://github.com/CosmosZ-code" title="CosmosZ-code">
<img src="https://avatars.githubusercontent.com/u/81398224?v=4" width="50;" alt="CosmosZ-code"/>
</a>
<a href="https://github.com/uubulb" title="UUBulb">
<img src="https://avatars.githubusercontent.com/u/35923940?v=4" width="50;" alt="UUBulb"/>
</a>
<a href="https://github.com/lvgj-stack" title="Ko no dio">
<img src="https://avatars.githubusercontent.com/u/38449861?v=4" width="50;" alt="Ko no dio"/>
</a>
<a href="https://github.com/hhhkkk520" title="Kris">
<img src="https://avatars.githubusercontent.com/u/52115472?v=4" width="50;" alt="Kris"/>
</a>
<a href="https://github.com/spiritLHLS" title="spiritlhl">
<img src="https://avatars.githubusercontent.com/u/103393591?v=4" width="50;" alt="spiritlhl"/>
<a href="https://github.com/1ridic" title="1ridic">
<img src="https://avatars.githubusercontent.com/u/88495501?v=4" width="50;" alt="1ridic"/>
</a>
<a href="https://github.com/Mmx233" title="Mmx">
<img src="https://avatars.githubusercontent.com/u/36563672?v=4" width="50;" alt="Mmx"/>
@@ -86,65 +92,77 @@ You can change the dashboard language in the settings page (`/setting`) after th
<a href="https://github.com/rootmelo92118" title="rootmelo92118">
<img src="https://avatars.githubusercontent.com/u/32770959?v=4" width="50;" alt="rootmelo92118"/>
</a>
<a href="https://github.com/1ridic" title="1ridic">
<img src="https://avatars.githubusercontent.com/u/88495501?v=4" width="50;" alt="1ridic"/>
</a>
<a href="https://github.com/coreff" title="Core F">
<img src="https://avatars.githubusercontent.com/u/38347122?v=4" width="50;" alt="Core F"/>
</a>
<a href="https://github.com/Creling" title="Creling">
<img src="https://avatars.githubusercontent.com/u/43109504?v=4" width="50;" alt="Creling"/>
</a>
<a href="https://github.com/ch8o" title="no-name-now">
<img src="https://avatars.githubusercontent.com/u/9103372?v=4" width="50;" alt="no-name-now"/>
<a href="https://github.com/iilemon" title="Sean">
<img src="https://avatars.githubusercontent.com/u/33201711?v=4" width="50;" alt="Sean"/>
</a>
<a href="https://github.com/fscarmen" title="fscarmen">
<img src="https://avatars.githubusercontent.com/u/62703343?v=4" width="50;" alt="fscarmen"/>
</a>
<a href="https://github.com/iilemon" title="Sean">
<img src="https://avatars.githubusercontent.com/u/33201711?v=4" width="50;" alt="Sean"/>
<a href="https://github.com/ch8o" title="no-name-now">
<img src="https://avatars.githubusercontent.com/u/9103372?v=4" width="50;" alt="no-name-now"/>
</a>
<a href="https://github.com/Es-dese" title="Esdese">
<img src="https://avatars.githubusercontent.com/u/71542548?v=4" width="50;" alt="Esdese"/>
<a href="https://github.com/Creling" title="Creling">
<img src="https://avatars.githubusercontent.com/u/43109504?v=4" width="50;" alt="Creling"/>
</a>
<a href="https://github.com/GreenTeodoro839" title="GreenTeodoro839">
<img src="https://avatars.githubusercontent.com/u/77104800?v=4" width="50;" alt="GreenTeodoro839"/>
<a href="https://github.com/coreff" title="Core F">
<img src="https://avatars.githubusercontent.com/u/38347122?v=4" width="50;" alt="Core F"/>
</a>
<a href="https://github.com/techotaku" title="Ian Li">
<img src="https://avatars.githubusercontent.com/u/1948179?v=4" width="50;" alt="Ian Li"/>
<a href="https://github.com/colour93" title="玖叁">
<img src="https://avatars.githubusercontent.com/u/64313711?v=4" width="50;" alt="玖叁"/>
</a>
<a href="https://github.com/KorenKrita" title="KorenKrita">
<img src="https://avatars.githubusercontent.com/u/22239339?v=4" width="50;" alt="KorenKrita"/>
</a>
<a href="https://github.com/MartijnLindeman" title="Martijn Lindeman">
<img src="https://avatars.githubusercontent.com/u/78365708?v=4" width="50;" alt="Martijn Lindeman"/>
</a>
<a href="https://github.com/nickfox-taterli" title="Tater Li">
<img src="https://avatars.githubusercontent.com/u/19658596?v=4" width="50;" alt="Tater Li"/>
</a>
<a href="https://github.com/hmsjy2017" title="Tony">
<img src="https://avatars.githubusercontent.com/u/42692274?v=4" width="50;" alt="Tony"/>
</a>
<a href="https://github.com/adminsama" title="adminsama">
<img src="https://avatars.githubusercontent.com/u/60880076?v=4" width="50;" alt="adminsama"/>
</a>
<a href="https://github.com/acgpiano" title="Acgpiano">
<img src="https://avatars.githubusercontent.com/u/15900800?v=4" width="50;" alt="Acgpiano"/>
</a>
<a href="https://github.com/guoyongchang" title="guoyongchang">
<img src="https://avatars.githubusercontent.com/u/10484506?v=4" width="50;" alt="guoyongchang"/>
</a>
<a href="https://github.com/yuanweize" title="I">
<img src="https://avatars.githubusercontent.com/u/30067203?v=4" width="50;" alt="I"/>
</a>
<a href="https://github.com/unclezs" title="unclezs">
<img src="https://avatars.githubusercontent.com/u/42318775?v=4" width="50;" alt="unclezs"/>
<a href="https://github.com/arkylin" title="凌">
<img src="https://avatars.githubusercontent.com/u/35104502?v=4" width="50;" alt=""/>
</a>
<a href="https://github.com/ysicing" title="缘生">
<img src="https://avatars.githubusercontent.com/u/8605565?v=4" width="50;" alt="缘生"/>
</a>
<a href="https://github.com/colour93" title="玖叁">
<img src="https://avatars.githubusercontent.com/u/64313711?v=4" width="50;" alt="玖叁"/>
<a href="https://github.com/xykt" title="xykt">
<img src="https://avatars.githubusercontent.com/u/152045469?v=4" width="50;" alt="xykt"/>
</a>
<a href="https://github.com/unclezs" title="unclezs">
<img src="https://avatars.githubusercontent.com/u/42318775?v=4" width="50;" alt="unclezs"/>
</a>
<a href="https://github.com/nap0o" title="nap0o">
<img src="https://avatars.githubusercontent.com/u/144927971?v=4" width="50;" alt="nap0o"/>
</a>
<a href="https://github.com/yuanweize" title="I">
<img src="https://avatars.githubusercontent.com/u/30067203?v=4" width="50;" alt="I"/>
</a>
<a href="https://github.com/guoyongchang" title="guoyongchang">
<img src="https://avatars.githubusercontent.com/u/10484506?v=4" width="50;" alt="guoyongchang"/>
</a>
<a href="https://github.com/acgpiano" title="Acgpiano">
<img src="https://avatars.githubusercontent.com/u/15900800?v=4" width="50;" alt="Acgpiano"/>
</a>
<a href="https://github.com/adminsama" title="adminsama">
<img src="https://avatars.githubusercontent.com/u/60880076?v=4" width="50;" alt="adminsama"/>
</a>
<a href="https://github.com/hmsjy2017" title="Tony">
<img src="https://avatars.githubusercontent.com/u/42692274?v=4" width="50;" alt="Tony"/>
</a>
<a href="https://github.com/nickfox-taterli" title="Tater Li">
<img src="https://avatars.githubusercontent.com/u/19658596?v=4" width="50;" alt="Tater Li"/>
</a>
<a href="https://github.com/MartijnLindeman" title="Martijn Lindeman">
<img src="https://avatars.githubusercontent.com/u/78365708?v=4" width="50;" alt="Martijn Lindeman"/>
</a>
<a href="https://github.com/KorenKrita" title="KorenKrita">
<img src="https://avatars.githubusercontent.com/u/22239339?v=4" width="50;" alt="KorenKrita"/>
</a>
<a href="https://github.com/techotaku" title="Ian Li">
<img src="https://avatars.githubusercontent.com/u/1948179?v=4" width="50;" alt="Ian Li"/>
</a>
<a href="https://github.com/HsukqiLee" title="HsukqiLee">
<img src="https://avatars.githubusercontent.com/u/79034142?v=4" width="50;" alt="HsukqiLee"/>
</a>
<a href="https://github.com/GreenTeodoro839" title="GreenTeodoro839">
<img src="https://avatars.githubusercontent.com/u/77104800?v=4" width="50;" alt="GreenTeodoro839"/>
</a>
<a href="https://github.com/Es-dese" title="Esdese">
<img src="https://avatars.githubusercontent.com/u/71542548?v=4" width="50;" alt="Esdese"/>
</a>
<a href="https://github.com/wwng2333" title="Crazy">
<img src="https://avatars.githubusercontent.com/u/17147265?v=4" width="50;" alt="Crazy"/>
</a><!--GAMFC_DELIMITER_END-->
## Star History
+25 -4
View File
@@ -1,11 +1,13 @@
package controller
import (
"github.com/gin-gonic/gin"
"github.com/naiba/nezha/pkg/mygin"
"github.com/naiba/nezha/service/singleton"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"github.com/naiba/nezha/pkg/mygin"
"github.com/naiba/nezha/service/singleton"
)
type apiV1 struct {
@@ -25,7 +27,8 @@ func (v *apiV1) serve() {
}))
r.GET("/server/list", v.serverList)
r.GET("/server/details", v.serverDetails)
mr := v.r.Group("monitor")
mr.GET("/:id", v.monitorHistoriesById)
}
// serverList 获取服务器列表 不传入Query参数则获取全部
@@ -65,3 +68,21 @@ func (v *apiV1) serverDetails(c *gin.Context) {
}
c.JSON(200, singleton.ServerAPI.GetAllStatus())
}
func (v *apiV1) monitorHistoriesById(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.AbortWithStatusJSON(400, gin.H{"code": 400, "message": "id参数错误"})
return
}
server, ok := singleton.ServerList[id]
if !ok {
c.AbortWithStatusJSON(404, gin.H{
"code": 404,
"message": "id不存在",
})
return
}
c.JSON(200, singleton.MonitorAPI.GetMonitorHistories(map[string]any{"server_id": server.ID}))
}
+115
View File
@@ -5,6 +5,7 @@ import (
"log"
"net/http"
"regexp"
"strconv"
"strings"
"sync"
"time"
@@ -48,6 +49,9 @@ func (cp *commonPage) serve() {
cr.Use(cp.checkViewPassword) // 前端查看密码鉴权
cr.GET("/", cp.home)
cr.GET("/service", cp.service)
// TODO: 界面直接跳转使用该接口
cr.GET("/network/:id", cp.network)
cr.GET("/network", cp.network)
cr.GET("/ws", cp.ws)
cr.POST("/terminal", cp.createTerminal)
}
@@ -114,6 +118,11 @@ func (p *commonPage) service(c *gin.Context) {
var statsStore map[uint64]model.CycleTransferStats
copier.Copy(&stats, singleton.ServiceSentinelShared.LoadStats())
copier.Copy(&statsStore, singleton.AlertsCycleTransferStatsStore)
for k, service := range stats {
if !service.Monitor.EnableShowInService {
delete(stats, k)
}
}
return []interface {
}{
stats, statsStore,
@@ -127,6 +136,112 @@ func (p *commonPage) service(c *gin.Context) {
}))
}
func (cp *commonPage) network(c *gin.Context) {
var (
monitorHistory *model.MonitorHistory
servers []*model.Server
serverIdsWithMonitor []uint64
monitorInfos = []byte("{}")
id uint64
)
if len(singleton.SortedServerList) > 0 {
id = singleton.SortedServerList[0].ID
}
if err := singleton.DB.Model(&model.MonitorHistory{}).Select("monitor_id, server_id").
Where("monitor_id != 0 and server_id != 0").Limit(1).First(&monitorHistory).Error; err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "请求失败",
Msg: "请求参数有误:" + "server monitor history not found",
Link: "/",
Btn: "返回重试",
}, true)
return
} else {
if monitorHistory == nil || monitorHistory.ServerID == 0 {
if len(singleton.SortedServerList) > 0 {
id = singleton.SortedServerList[0].ID
}
} else {
id = monitorHistory.ServerID
}
}
idStr := c.Param("id")
if idStr != "" {
var err error
id, err = strconv.ParseUint(idStr, 10, 64)
if err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "请求失败",
Msg: "请求参数有误:" + err.Error(),
Link: "/",
Btn: "返回重试",
}, true)
return
}
_, ok := singleton.ServerList[id]
if !ok {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "请求失败",
Msg: "请求参数有误:" + "server id not found",
Link: "/",
Btn: "返回重试",
}, true)
return
}
}
monitorHistories := singleton.MonitorAPI.GetMonitorHistories(map[string]any{"server_id": id})
monitorInfos, _ = utils.Json.Marshal(monitorHistories)
_, isMember := c.Get(model.CtxKeyAuthorizedUser)
_, isViewPasswordVerfied := c.Get(model.CtxKeyViewPasswordVerified)
if err := singleton.DB.Model(&model.MonitorHistory{}).
Select("distinct(server_id)").
Where("server_id != 0").
Find(&serverIdsWithMonitor).
Error; err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "请求失败",
Msg: "请求参数有误:" + "no server with monitor histories",
Link: "/",
Btn: "返回重试",
}, true)
return
}
if isMember || isViewPasswordVerfied {
for _, server := range singleton.SortedServerList {
for _, id := range serverIdsWithMonitor {
if server.ID == id {
servers = append(servers, server)
}
}
}
} else {
for _, server := range singleton.SortedServerListForGuest {
for _, id := range serverIdsWithMonitor {
if server.ID == id {
servers = append(servers, server)
}
}
}
}
serversBytes, _ := utils.Json.Marshal(Data{
Now: time.Now().Unix() * 1000,
Servers: servers,
})
c.HTML(http.StatusOK, "theme-"+singleton.Conf.Site.Theme+"/network", mygin.CommonEnvironment(c, gin.H{
"Servers": string(serversBytes),
"MonitorInfos": string(monitorInfos),
"CustomCode": singleton.Conf.Site.CustomCode,
"MaxTCPPingValue": singleton.Conf.MaxTCPPingValue,
}))
}
func (cp *commonPage) getServerStat(c *gin.Context) ([]byte, error) {
v, err, _ := cp.requestGroup.Do("serverStats", func() (any, error) {
singleton.SortedServerLock.RLock()
+23 -1
View File
@@ -11,6 +11,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/jinzhu/copier"
"gorm.io/gorm"
"github.com/naiba/nezha/model"
"github.com/naiba/nezha/pkg/mygin"
@@ -185,7 +186,17 @@ func (ma *memberAPI) delete(c *gin.Context) {
var err error
switch c.Param("model") {
case "server":
err = singleton.DB.Unscoped().Delete(&model.Server{}, "id = ?", id).Error
err := singleton.DB.Transaction(func(tx *gorm.DB) error {
err = singleton.DB.Unscoped().Delete(&model.Server{}, "id = ?", id).Error
if err != nil {
return err
}
err = singleton.DB.Unscoped().Delete(&model.MonitorHistory{}, "server_id = ?", id).Error
if err != nil {
return err
}
return nil
})
if err == nil {
// 删除服务器
singleton.ServerLock.Lock()
@@ -289,6 +300,8 @@ type serverForm struct {
Tag string
Note string
HideForGuest string
EnableDDNS string
DDNSDomain string
}
func (ma *memberAPI) addOrEditServer(c *gin.Context) {
@@ -304,6 +317,8 @@ func (ma *memberAPI) addOrEditServer(c *gin.Context) {
s.Tag = sf.Tag
s.Note = sf.Note
s.HideForGuest = sf.HideForGuest == "on"
s.EnableDDNS = sf.EnableDDNS == "on"
s.DDNSDomain = sf.DDNSDomain
if s.ID == 0 {
s.Secret, err = utils.GenerateRandomString(18)
if err == nil {
@@ -383,6 +398,7 @@ type monitorForm struct {
MaxLatency float32
LatencyNotify string
EnableTriggerTask string
EnableShowInService string
FailTriggerTasksRaw string
RecoverTriggerTasksRaw string
}
@@ -404,6 +420,7 @@ func (ma *memberAPI) addOrEditMonitor(c *gin.Context) {
m.LatencyNotify = mf.LatencyNotify == "on"
m.MinLatency = mf.MinLatency
m.MaxLatency = mf.MaxLatency
m.EnableShowInService = mf.EnableShowInService == "on"
m.EnableTriggerTask = mf.EnableTriggerTask == "on"
m.RecoverTriggerTasksRaw = mf.RecoverTriggerTasksRaw
m.FailTriggerTasksRaw = mf.FailTriggerTasksRaw
@@ -427,6 +444,11 @@ func (ma *memberAPI) addOrEditMonitor(c *gin.Context) {
err = singleton.DB.Save(&m).Error
}
}
if m.Cover == 0 {
err = singleton.DB.Unscoped().Delete(&model.MonitorHistory{}, "monitor_id = ? and server_id in (?)", m.ID, strings.Split(m.SkipServersRaw[1:len(m.SkipServersRaw)-1], ",")).Error
} else {
err = singleton.DB.Unscoped().Delete(&model.MonitorHistory{}, "monitor_id = ? and server_id not in (?)", m.ID, strings.Split(m.SkipServersRaw[1:len(m.SkipServersRaw)-1], ",")).Error
}
}
if err == nil {
err = singleton.ServiceSentinelShared.OnMonitorUpdate(m)
+13 -3
View File
@@ -49,10 +49,20 @@ func DispatchTask(serviceSentinelDispatchBus <-chan model.Monitor) {
workedServerIndex++
continue
}
if task.Cover == model.MonitorCoverIgnoreAll && task.SkipServers[singleton.SortedServerList[workedServerIndex].ID] {
singleton.SortedServerList[workedServerIndex].TaskStream.Send(task.PB())
workedServerIndex++
continue
}
if task.Cover == model.MonitorCoverAll && !task.SkipServers[singleton.SortedServerList[workedServerIndex].ID] {
singleton.SortedServerList[workedServerIndex].TaskStream.Send(task.PB())
workedServerIndex++
continue
}
// 找到合适机器执行任务,跳出循环
singleton.SortedServerList[workedServerIndex].TaskStream.Send(task.PB())
workedServerIndex++
break
// singleton.SortedServerList[workedServerIndex].TaskStream.Send(task.PB())
// workedServerIndex++
// break
}
singleton.SortedServerLock.RUnlock()
}
+2 -2
View File
@@ -24,7 +24,7 @@ require (
github.com/spf13/viper v1.17.0
github.com/stretchr/testify v1.8.4
github.com/xanzy/go-gitlab v0.94.0
golang.org/x/crypto v0.15.0
golang.org/x/crypto v0.17.0
golang.org/x/oauth2 v0.14.0
golang.org/x/sync v0.5.0
golang.org/x/text v0.14.0
@@ -80,7 +80,7 @@ require (
golang.org/x/arch v0.3.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.18.0 // indirect
golang.org/x/sys v0.14.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 // indirect
+6 -6
View File
@@ -339,8 +339,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -478,12 +478,12 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8=
golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+30
View File
@@ -110,6 +110,21 @@ type Config struct {
v *viper.Viper
IgnoredIPNotificationServerIDs map[uint64]bool // [ServerID] -> bool(值为true代表当前ServerID在特定服务器列表内)
MaxTCPPingValue int32
AvgPingCount int
// 动态域名解析更新
DDNS struct {
Enable bool
Provider string
AccessID string
AccessSecret string
WebhookURL string
WebhookMethod string
WebhookRequestBody string
WebhookHeaders string
MaxRetries uint32
}
}
// Read 读取配置文件并应用
@@ -144,6 +159,21 @@ func (c *Config) Read(path string) error {
if c.Location == "" {
c.Location = "Asia/Shanghai"
}
if c.MaxTCPPingValue == 0 {
c.MaxTCPPingValue = 1000
}
if c.AvgPingCount == 0 {
c.AvgPingCount = 2
}
if c.DDNS.Provider == "" {
c.DDNS.Provider = "webhook"
}
if c.DDNS.WebhookMethod == "" {
c.DDNS.WebhookMethod = "POST"
}
if c.DDNS.MaxRetries == 0 {
c.DDNS.MaxRetries = 3
}
c.updateIgnoredIPNotificationID()
return nil
+4 -2
View File
@@ -4,10 +4,11 @@ import (
"fmt"
"log"
"github.com/naiba/nezha/pkg/utils"
pb "github.com/naiba/nezha/proto"
"github.com/robfig/cron/v3"
"gorm.io/gorm"
"github.com/naiba/nezha/pkg/utils"
pb "github.com/naiba/nezha/proto"
)
const (
@@ -49,6 +50,7 @@ type Monitor struct {
Cover uint8
EnableTriggerTask bool `gorm:"default: false"`
EnableShowInService bool `gorm:"default: false"`
FailTriggerTasksRaw string `gorm:"default:'[]'"`
RecoverTriggerTasksRaw string `gorm:"default:'[]'"`
FailTriggerTasks []uint64 `gorm:"-" json:"-"` // 失败时执行的触发任务id
+16 -5
View File
@@ -1,11 +1,22 @@
package model
import (
"time"
"gorm.io/gorm"
)
// MonitorHistory 历史监控记录
type MonitorHistory struct {
Common
MonitorID uint64
AvgDelay float32 // 平均延迟,毫秒
Up uint64 // 检查状态良好计数
Down uint64 // 检查状态异常计数
ID uint64 `gorm:"primaryKey"`
CreatedAt time.Time `gorm:"index;<-:create;index:idx_server_id_created_at_monitor_id_avg_delay"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
MonitorID uint64 `gorm:"index:idx_server_id_created_at_monitor_id_avg_delay"`
ServerID uint64 `gorm:"index:idx_server_id_created_at_monitor_id_avg_delay"`
AvgDelay float32 `gorm:"index:idx_server_id_created_at_monitor_id_avg_delay"` // 平均延迟,毫秒
Up uint64 // 检查状态良好计数
Down uint64 // 检查状态异常计数
Data string
}
+4 -1
View File
@@ -17,6 +17,8 @@ type Server struct {
Note string `json:"-"` // 管理员可见备注
DisplayIndex int // 展示排序,越大越靠前
HideForGuest bool // 对游客隐藏
EnableDDNS bool // 是否启用DDNS 未在配置文件中启用DDNS 或 DDNS检查时间为0时此项无效
DDNSDomain string // DDNS中的前缀 如基础域名为abc.oracle DDNSName为mjj 就会把mjj.abc.oracle解析服务器IP 为空则停用
Host *Host `gorm:"-"`
State *HostState `gorm:"-"`
@@ -51,5 +53,6 @@ func (s Server) Marshal() template.JS {
tag, _ := utils.Json.Marshal(s.Tag)
note, _ := utils.Json.Marshal(s.Note)
secret, _ := utils.Json.Marshal(s.Secret)
return template.JS(fmt.Sprintf(`{"ID":%d,"Name":%s,"Secret":%s,"DisplayIndex":%d,"Tag":%s,"Note":%s,"HideForGuest": %s}`, s.ID, name, secret, s.DisplayIndex, tag, note, boolToString(s.HideForGuest))) // #nosec
ddnsDomain, _ := utils.Json.Marshal(s.DDNSDomain)
return template.JS(fmt.Sprintf(`{"ID":%d,"Name":%s,"Secret":%s,"DisplayIndex":%d,"Tag":%s,"Note":%s,"HideForGuest": %s,"EnableDDNS": %s,"DDNSDomain": %s}`, s.ID, name, secret, s.DisplayIndex, tag, note, boolToString(s.HideForGuest), boolToString(s.EnableDDNS), ddnsDomain)) // #nosec
}
+12
View File
@@ -609,3 +609,15 @@ other = "Hide for Guest"
[Menu]
other = "Menu"
[NetworkSpiter]
other = "Network"
[EnableShowInService]
other = "Enable Show in Service"
[EnableDDNS]
other = "Enable DDNS"
[DDNSDomain]
other = "DDNS Domain"
+12
View File
@@ -609,3 +609,15 @@ other = "Ocultar para Invitados"
[Menu]
other = "Menú"
[NetworkSpiter]
other = "Red"
[EnableShowInService]
other = "Mostrar en servicio"
[EnableDDNS]
other = "Habilitar DDNS"
[DDNSDomain]
other = "Dominio DDNS"
+13 -1
View File
@@ -608,4 +608,16 @@ other = "信息"
other = "对游客隐藏"
[Menu]
other = "菜单"
other = "菜单"
[NetworkSpiter]
other = "网络"
[EnableShowInService]
other = "在服务中显示"
[EnableDDNS]
other = "启用DDNS"
[DDNSDomain]
other = "DDNS域名"
+13 -1
View File
@@ -608,4 +608,16 @@ other = "信息"
other = "對遊客隱藏"
[Menu]
other = "菜單"
other = "菜單"
[NetworkSpiter]
other = "網絡"
[EnableShowInService]
other = "在服務中顯示"
[EnableDDNS]
other = "啟用DDNS"
[DDNSDomain]
other = "DDNS網域"
+11
View File
@@ -302,6 +302,7 @@ function addOrEditServer(server, conf) {
modal.find("input[name=id]").val(server ? server.ID : null);
modal.find("input[name=name]").val(server ? server.Name : null);
modal.find("input[name=Tag]").val(server ? server.Tag : null);
modal.find("input[name=DDNSDomain]").val(server ? server.DDNSDomain : null);
modal
.find("input[name=DisplayIndex]")
.val(server ? server.DisplayIndex : null);
@@ -321,6 +322,11 @@ function addOrEditServer(server, conf) {
} else {
modal.find(".ui.hideforguest.checkbox").checkbox("set unchecked");
}
if (server && server.EnableDDNS) {
modal.find(".ui.enableddns.checkbox").checkbox("set checked");
} else {
modal.find(".ui.enableddns.checkbox").checkbox("set unchecked");
}
showFormModal(".server.modal", "#serverForm", "/api/server");
}
@@ -339,6 +345,11 @@ function addOrEditMonitor(monitor) {
modal.find("select[name=Type]").val(monitor ? monitor.Type : 1);
modal.find("select[name=Cover]").val(monitor ? monitor.Cover : 0);
modal.find("input[name=NotificationTag]").val(monitor ? monitor.NotificationTag : null);
if (monitor && monitor.EnableShowInService) {
modal.find(".ui.nb-show-in-service.checkbox").checkbox("set checked")
} else {
modal.find(".ui.nb-show-in-service.checkbox").checkbox("set unchecked")
}
if (monitor && monitor.Notify) {
modal.find(".ui.nb-notify.checkbox").checkbox("set checked");
} else {
+4 -4
View File
@@ -6,11 +6,11 @@
{{.Version}}</small>
</div>
</div>
<script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdn.staticfile.org/semantic-ui/2.4.1/semantic.min.js"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.min.js"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.js"></script>
<script src="/static/semantic-ui-alerts.min.js"></script>
<script src="https://cdn.staticfile.org/vue/2.6.14/vue.min.js"></script>
<script src="/static/main.js?v20230614"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
<script src="/static/main.js?v20240224"></script>
<script>
(function () {
updateLang({{.LANG }});
+2 -2
View File
@@ -9,8 +9,8 @@
<meta content="telephone=no" name="format-detection">
<title>{{.Title}}</title>
<link rel="stylesheet" type="text/css"
href="https://cdn.staticfile.org/semantic-ui/2.4.1/semantic.min.css">
<link href="https://cdn.staticfile.org/font-logos/0.17/font-logos.min.css" type="text/css"
href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.css">
<link href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/font-logos/0.17/font-logos.min.css" type="text/css"
rel="stylesheet" />
<link rel="stylesheet" type="text/css" href="/static/semantic-ui-alerts.min.css">
<link rel="stylesheet" type="text/css" href="/static/main.css?v2022042314">
+2 -1
View File
@@ -15,6 +15,7 @@
{{else}}
<a class='item{{if eq .MatchedPath "/"}} active{{end}}' href="/"><i class="home icon"></i>{{tr "Home"}}</a>
<a class='item{{if eq .MatchedPath "/service"}} active{{end}}' href="/service"><i class="rss icon"></i>{{tr "Services"}}</a>
<a class='item{{if eq .MatchedPath "/network"}} active{{end}}' href="/network"><i class="server icon"></i>{{tr "NetworkSpiter"}}</a>
{{end}}
<div class="right menu">
<div class="item">
@@ -50,4 +51,4 @@
</div>
</div>
{{template "component/confirm" .}}
{{end}}
{{end}}
+7
View File
@@ -24,6 +24,12 @@
<option value="3">TCP-Ping</option>
</select>
</div>
<div class="field">
<div class="ui nb-show-in-service checkbox">
<input name="EnableShowInService" type="checkbox" tabindex="0" class="hidden" />
<label>{{tr "EnableShowInService"}}</label>
</div>
</div>
<div class="field">
<label>{{tr "Duration"}}</label>
<input type="number" name="Duration" placeholder="{{tr "Seconds"}}" />
@@ -110,3 +116,4 @@
</div>
</div>
{{end}}
+10
View File
@@ -26,6 +26,16 @@
<label>{{tr "HideForGuest"}}</label>
</div>
</div>
<div class="field">
<div class="ui enableddns checkbox">
<input name="EnableDDNS" type="checkbox" tabindex="0" />
<label>{{tr "EnableDDNS"}}</label>
</div>
</div>
<div class="field">
<label>{{tr "DDNSDomain"}}</label>
<input type="text" name="DDNSDomain" placeholder="{{tr "DDNSDomain"}}">
</div>
<div class="field">
<label>{{tr "Note"}}</label>
<textarea name="Note"></textarea>
+1 -1
View File
@@ -68,4 +68,4 @@
<script>
$(".checkbox").checkbox();
</script>
{{end}}
{{end}}
+5 -1
View File
@@ -29,6 +29,8 @@
<th>IP</th>
<th>{{tr "VersionNumber"}}</th>
<th>{{tr "HideForGuest"}}</th>
<th>{{tr "EnableDDNS"}}</th>
<th>{{tr "DDNSDomain"}}</th>
<th>{{tr "Secret"}}</th>
<th>{{tr "OneKeyInstall"}}</th>
<th>{{tr "Note"}}</th>
@@ -45,6 +47,8 @@
<td>{{$server.Host.IP}}</td>
<td>{{$server.Host.Version}}</td>
<td>{{$server.HideForGuest}}</td>
<td>{{$server.EnableDDNS}}</td>
<td>{{$server.DDNSDomain}}</td>
<td>
<button class="ui icon green mini button" data-clipboard-text="{{$server.Secret}}" data-tooltip="{{tr "ClickToCopy"}}">
<i class="copy icon"></i>
@@ -85,7 +89,7 @@
</div>
{{template "component/server" .}}
{{template "common/footer" .}}
<script src="https://cdn.staticfile.org/clipboard.js/2.0.10/clipboard.min.js" type="application/javascript"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/clipboard.js/2.0.10/clipboard.min.js" type="application/javascript"></script>
<script>
var clipboard = new ClipboardJS('.ui.icon.green.mini.button');
const checkBoxList = document.querySelectorAll('tbody > tr > td > input.nezha-servers[type=checkbox]')
+4 -4
View File
@@ -8,7 +8,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>tty@{{.ServerName}} - {{.Title}}</title>
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
<link href="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/xterm/4.11.0/xterm.css" type="text/css" rel="stylesheet"/>
<link href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/xterm/4.11.0/xterm.css" type="text/css" rel="stylesheet"/>
</head>
<style>
@@ -28,9 +28,9 @@
<body onresize="onResize()">
<div id="terminal-container"></div>
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/xterm/4.11.0/xterm.js" type="application/javascript"></script>
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/xterm/4.11.0/addons/attach/xterm-addon-attach.js"></script>
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/xterm/4.11.0/addons/fit/xterm-addon-fit.js"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/xterm/4.11.0/xterm.js" type="application/javascript"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/xterm/4.11.0/addons/attach/xterm-addon-attach.js"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/xterm/4.11.0/addons/fit/xterm-addon-fit.js"></script>
<script>
let sendResizing = false;
+4 -4
View File
@@ -8,11 +8,11 @@
</small>
</div>
</div>
<script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdn.staticfile.org/semantic-ui/2.4.1/semantic.min.js"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.min.js"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.js"></script>
<script src="/static/semantic-ui-alerts.min.js"></script>
<script src="https://cdn.staticfile.org/vue/2.6.14/vue.min.js"></script>
<script src="/static/main.js?v20230614"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
<script src="/static/main.js?v20240224"></script>
<script>
(function () {
updateLang({{.LANG }});
+3 -3
View File
@@ -9,10 +9,10 @@
<meta content="telephone=no" name="format-detection">
<title>{{.Title}}</title>
<link rel="stylesheet" type="text/css"
href="https://cdn.staticfile.org/semantic-ui/2.4.1/semantic.min.css">
<link href="https://cdn.staticfile.org/font-logos/0.17/font-logos.min.css" type="text/css"
href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.css">
<link href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/font-logos/0.17/font-logos.min.css" type="text/css"
rel="stylesheet" />
<link href="https://cdn.staticfile.org/bootstrap-icons/1.10.3/font/bootstrap-icons.css" type="text/css"
<link href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/bootstrap-icons/1.8.1/font/bootstrap-icons.min.css" type="text/css"
rel="stylesheet" />
<link rel="stylesheet" type="text/css" href="/static/semantic-ui-alerts.min.css">
<link rel="stylesheet" type="text/css" href="/static/main.css?v2022042314">
+1 -1
View File
@@ -15,7 +15,7 @@
<div v-for="server in group.data" :id="server.ID" class="ui card">
<div class="content" v-if="server.Host" style="margin-top: 10px; padding-bottom: 5px">
<div class="header">
<img v-if="server.Host.CountryCode" style="border-radius: 50%;box-shadow:-1px -1px 2px #eee, 1px 1px 2px #000;width:19px;" :src="'https://cdn.staticfile.org/flag-icon-css/6.6.5/flags/1x1/'+server.Host.CountryCode + '.svg'" alt="国家"/>&nbsp;<i v-if='server.Host.Platform == "darwin"'
<img v-if="server.Host.CountryCode" style="border-radius: 50%;box-shadow:-1px -1px 2px #eee, 1px 1px 2px #000;width:19px;" :src="'https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/flag-icon-css/4.1.5/flags/1x1/'+server.Host.CountryCode + '.svg'" alt="国家"/>&nbsp;<i v-if='server.Host.Platform == "darwin"'
class="apple icon"></i><i v-else-if='isWindowsPlatform(server.Host.Platform)'
class="windows icon"></i><i v-else :class="'fl-' + getFontLogoClass(server.Host.Platform)"></i>
@#server.Name + (server.live?'':'[{{tr "Offline"}}]')#@
+5 -4
View File
@@ -10,8 +10,8 @@
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
<link rel="stylesheet" href="/static/theme-daynight/css/main.css?v202108042286">
<link href="https://cdn.staticfile.org/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script>
<link href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.min.js"></script>
{{if ts .CustomCode}}
{{.CustomCode|safe}}
@@ -39,6 +39,7 @@
<ul>
<li><a href="/">{{tr "Home"}}</a></li>
<li><a href="/service">{{tr "Services"}}</a></li>
<li><a href="/network">{{tr "NetworkSpiter"}}</a></li>
{{if .Admin}}
<li><a href="/server">{{tr "AdminPanel"}}</a></li>
{{else}}
@@ -165,8 +166,8 @@
</footer>
<script src="/static/theme-daynight/js/main.js?v202102012266"></script>
<script src="https://cdn.staticfile.org/vue/2.6.14/vue.min.js"></script>
<script src="https://cdn.staticfile.org/limonte-sweetalert2/11.4.4/sweetalert2.all.min.js"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/limonte-sweetalert2/11.4.4/sweetalert2.all.min.js"></script>
<script>
const initData = JSON.parse('{{.Servers}}').servers;
+268
View File
@@ -0,0 +1,268 @@
{{define "theme-daynight/network"}}
<!doctype html>
<html lang="{{.Conf.Language}}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>{{.Title}}</title>
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
<link rel="stylesheet" href="/static/theme-daynight/css/main.css?v202108042286">
<link href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.min.js"></script>
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/echarts/5.3.0-rc.1/echarts.min.js"></script>
{{if ts .CustomCode}}
{{.CustomCode|safe}}
{{end}}
</head>
<body data-theme="light" data-gridlist="grid">
<header>
<section class="nav-bar clearfix">
<figure class="logo">
<a href="/">
<img src="/static/logo.svg?v20210804" alt='{{tr "NezhaMonitoring"}}' width="50" height="50">
</a>
<a href="/">{{.Conf.Site.Brand}}</a>
</figure>
<div class="icon-container">
<div class="row cf">
<div class="three col">
<div class="hamburger" id="hamburger-icon"><span class="line"></span><span
class="line"></span><span class="line"></span></div>
</div>
</div>
</div>
<nav class="nav-menu">
<ul>
<li><a href="/">{{tr "Home"}}</a></li>
<li><a href="/service">{{tr "Services"}}</a></li>
<li><a href="/network">{{tr "NetworkSpiter"}}</a></li>
{{if .Admin}}
<li><a href="/server">{{tr "AdminPanel"}}</a></li>
{{else}}
<li><a href="/login">{{tr "Login"}}</a></li>
{{end}}
</ul>
</nav>
</section>
</header>
<main>
<div id="network">
<div class="server-info-container" v-for='server in servers' :id="server.ID" style="font-size: .6em">
<div class="info-body" @click="redirectNetwork(server.ID)">
<ul class="server-info-body-container">
<li>
<h3>@#server.Name#@</h3>
</li>
<li><img :src="'/static/theme-daynight/img/flag/'+(server.Host&&server.Host.CountryCode?server.Host.CountryCode.toUpperCase():'CN')+'.png'"
:title="server.Host.CountryCode.toUpperCase()" /></li>
</ul>
</div>
</div>
</div>
<div class="network-chart" style="height: 800px;overflow: hidden">
<div id="monitor-info-container" style="height: 520px;max-width: 1400px">
</div>
</div>
<div class="sidebar-container">
<ul>
<li><i class="fas fa-sun" title='{{tr "LightMode"}}'></i><span>{{tr "LightMode"}}</span></li>
<li><i class="fas fa-moon" title='{{tr "DarkMode"}}'></i><span>{{tr "DarkMode"}}</span></li>
<li><i class="fas fa-th" title='{{tr "GridLayout"}}'></i><span>{{tr "GridLayout"}}</span></li>
<li><i class="fas fa-list-ul" title='{{tr "ListLayout"}}'></i><span>{{tr "ListLayout"}}</span></li>
</ul>
</div>
</main>
<section class="dark-light-toggle">
<label class="switcher">
<input type="checkbox" name="theme" id="dark-light" />
<div>
<i class="fas fa-adjust"></i>
</div>
</label>
</section>
<!-- Back to top button -->
<a id="back-to-top"></a>
<footer>
<div class="footer-container">
<div><a href="https://github.com/naiba/nezha" target="_blank">Powered by {{tr "NezhaMonitoring"}} · {{.Version}}</a>
<p>&copy; <span id="copyright-date">
<script>document.getElementById('copyright-date').appendChild(document.createTextNode(new Date().getFullYear()))</script>
</span> · <a href="https://blog.jackiesung.com" target="_blank">Theme designed by Jackie Sung</a>
</p>
</div>
</div>
</footer>
<script src="/static/theme-daynight/js/main.js?v202102012266"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/limonte-sweetalert2/11.4.4/sweetalert2.all.min.js"></script>
<script>
const monitorInfo = JSON.parse('{{.MonitorInfos}}');
const initData = JSON.parse('{{.Servers}}').servers;
let MaxTCPPingValue = {{.MaxTCPPingValue}};
if (MaxTCPPingValue == null) {
MaxTCPPingValue = 300;
}
// 基于准备好的dom,初始化echarts实例
var myChart = echarts.init(document.getElementById('monitor-info-container'));
// 使用刚指定的配置项和数据显示图表。
var statusCards = new Vue({
el: '#network',
delimiters: ['@#', '#@'],
data: {
servers: initData,
cache: [],
option: {
tooltip: {
trigger: 'axis',
position: function (pt) {
return [pt[0], '10%'];
},
formatter: function(params){
let result = params[0].axisValueLabel + "<br />";
params.forEach(function(item){
result += item.marker + item.seriesName + ": " + item.value[1].toFixed(2) + " ms<br />";
})
return result;
},
confine: true,
transitionDuration: 0
},
title: {
left: 'center',
text: "",
textStyle: {}
},
legend: {
top: '5%',
data: [],
textStyle: {
fontSize: 14
}
},
toolbox: {
feature: {
dataZoom: {
yAxisIndex: 'none'
},
restore: {},
saveAsImage: {}
}
},
dataZoom: [
{
start: 94,
end: 100
}
],
xAxis: {
type: 'time',
boundaryGap: false
},
yAxis: {
type: 'value',
boundaryGap: [0, '100%']
},
series: [],
}
},
mounted() {
this.DarkMode();
this.parseMonitorInfo(monitorInfo);
},
methods: {
DarkMode() {
const hour = new Date(Date.now()).getHours()
if (hour > 17 || hour < 4) {
document.querySelector("input[name=theme]").checked = true;
document.getElementsByTagName("BODY")[0].setAttribute('data-theme', 'dark');
document.getElementById("monitor-info-container").style.backgroundColor = "#1E1E1E";
}
},
redirectNetwork(id) {
this.getMonitorHistory(id)
.then(function(monitorInfo) {
var vm = network.__vue__;
vm.parseMonitorInfo(monitorInfo);
})
.catch(function(error){
window.location.href = "/404";
})
},
getMonitorHistory(id) {
return $.ajax({
url: "/api/v1/monitor/"+id,
method: "GET"
});
},
parseMonitorInfo(monitorInfo) {
let tSeries = [];
let tLegendData = [];
for (let i = 0; i < monitorInfo.result.length; i++) {
let loss = 0;
let data = [];
for (let j = 0; j < monitorInfo.result[i].created_at.length; j++) {
avgDelay = monitorInfo.result[i].avg_delay[j];
if (avgDelay > 0.9 * MaxTCPPingValue) {
loss += 1
}
data.push([monitorInfo.result[i].created_at[j], avgDelay]);
}
lossRate = ((loss / monitorInfo.result[i].created_at.length) * 100).toFixed(1);
legendName = monitorInfo.result[i].monitor_name +" "+ lossRate + "%";
tLegendData.push(legendName);
tSeries.push({
name: legendName,
type: 'line',
smooth: true,
symbol: 'none',
data: data
});
}
this.option.title.text = monitorInfo.result[0].server_name;
this.option.series = tSeries;
this.option.legend.data = tLegendData;
const hour = new Date(Date.now()).getHours()
if (hour > 17 || hour < 4) {
this.option.legend.textStyle.color = "#F1F1F2";
this.option.title.textStyle.color = "#ccc";
}
myChart.clear();
myChart.setOption(this.option);
}
}
});
</script>
</body>
<style>
#network {
display: grid;
/*grid-template-columns: repeat(5, 1fr);*/
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
grid-gap: 1em;
width: 100%;
width: calc(100vw - 6em);
max-width: 1400px;
margin: 1em auto;
align-content: start;
}
#monitor-info-container{
margin: 0em auto;
align-content: start;
background-color: #F1F1F2;
}
</style>
</html>
{{end}}
+5 -4
View File
@@ -8,9 +8,9 @@
<title>{{.Title}}</title>
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
<link rel="stylesheet" href="/static/theme-daynight/css/service.css?v202108042286" />
<link rel="stylesheet" href="https://cdn.staticfile.org/mdui/1.0.2/css/mdui.min.css"/>
<link href="https://cdn.staticfile.org/font-awesome/6.0.0/css/all.min.css" rel="stylesheet" />
<script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script>
<link rel="stylesheet" href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/mdui/1.0.2/css/mdui.min.css"/>
<link href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/font-awesome/6.0.0/css/all.min.css" rel="stylesheet" />
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.min.js"></script>
{{if ts .CustomCode}}
{{.CustomCode|safe}}
@@ -43,6 +43,7 @@
<ul>
<li><a href="/">{{tr "Home"}}</a></li>
<li><a href="/service">{{tr "Services"}}</a></li>
<li><a href="/network">{{tr "NetworkSpiter"}}</a></li>
{{if .Admin}}
<li><a href="/server">{{tr "AdminPanel"}}</a></li>
{{else}}
@@ -80,7 +81,7 @@
{{end}}
</div>
</section>
{{end}}
{{end}}
</section>
{{if .CycleTransferStats}}
<h2 class="mdui-m-t-5 mdui-text-center">{{tr "CycleTransferStats"}}</h2>
+227
View File
@@ -0,0 +1,227 @@
{{define "theme-default/network"}}
{{template "common/header" .}}
{{if ts .CustomCode}}
{{.CustomCode|safe}}
{{end}}
{{template "common/menu" .}}
<div class="nb-container" id="app">
<div class="ui container">
<div class="service-status">
<table class="ui celled table">
<button class="ui nezha-primary-btn button"
v-for="server in servers"
style="margin-top: 3px"
@click="redirectNetwork(server.ID)">
@#server.Name#@ &nbsp;<i :class="server.Host.CountryCode + ' flag'"></i>
</button>
</table>
</div>
</div>
<div class="ui container" style="max-width: 95vw">
<div ref="chartDom" style="border-radius: 28px; margin-top: 15px;height: 520px;max-width: 1400px;overflow: hidden"></div>
</div>
</div>
{{template "common/footer" .}}
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/echarts/5.3.0-rc.1/echarts.min.js"></script>
<script>
const monitorInfo = JSON.parse('{{.MonitorInfos}}');
const initData = JSON.parse('{{.Servers}}').servers;
let MaxTCPPingValue = {{.MaxTCPPingValue}};
if (MaxTCPPingValue == null) {
MaxTCPPingValue = 1000;
}
new Vue({
el: '#app',
delimiters: ['@#', '#@'],
data: {
servers: initData,
option: {
tooltip: {
trigger: 'axis',
position: function (pt) {
return [pt[0], '10%'];
},
formatter: function(params){
let result = params[0].axisValueLabel + "<br />";
params.forEach(function(item){
result += item.marker + item.seriesName + ": " + item.value[1].toFixed(2) + " ms<br />";
})
return result;
},
confine: true,
transitionDuration: 0
},
title: {
left: 'center',
text: "",
textStyle: {}
},
legend: {
top: '5%',
data: [],
textStyle: {
fontSize: 14
}
},
backgroundColor: 'rgba(255, 255, 255, 0.8)',
toolbox: {
feature: {
dataZoom: {
yAxisIndex: 'none'
},
restore: {},
saveAsImage: {}
}
},
dataZoom: [
{
start: 0,
end: 100
}
],
xAxis: {
type: 'time',
boundaryGap: false
},
yAxis: {
type: 'value',
boundaryGap: false
},
series: [],
},
chartOnOff: true,
},
mounted() {
this.renderChart();
this.parseMonitorInfo(monitorInfo);
},
methods: {
getFontLogoClass(str) {
if (["almalinux",
"alpine",
"aosc",
"apple",
"archlinux",
"archlabs",
"artix",
"budgie",
"centos",
"coreos",
"debian",
"deepin",
"devuan",
"docker",
"elementary",
"fedora",
"ferris",
"flathub",
"freebsd",
"gentoo",
"gnu-guix",
"illumos",
"kali-linux",
"linuxmint",
"mageia",
"mandriva",
"manjaro",
"nixos",
"openbsd",
"opensuse",
"pop-os",
"raspberry-pi",
"redhat",
"rocky-linux",
"sabayon",
"slackware",
"snappy",
"solus",
"tux",
"ubuntu",
"void",
"zorin"].indexOf(str)
> -1) {
return str;
}
if (['openwrt', 'linux', "immortalwrt"].indexOf(str) > -1) {
return 'tux';
}
if (str == 'amazon') {
return 'redhat';
}
if (str == 'arch') {
return 'archlinux';
}
return '';
},
redirectNetwork(id) {
this.getMonitorHistory(id)
.then(function(monitorInfo) {
var vm = app.__vue__;
vm.parseMonitorInfo(monitorInfo);
})
.catch(function(error){
window.location.href = "/404";
})
},
getMonitorHistory(id) {
return $.ajax({
url: "/api/v1/monitor/"+id,
method: "GET"
});
},
parseMonitorInfo(monitorInfo) {
let tSeries = [];
let tLegendData = [];
for (let i = 0; i < monitorInfo.result.length; i++) {
let loss = 0;
let data = [];
for (let j = 0; j < monitorInfo.result[i].created_at.length; j++) {
avgDelay = Math.round(monitorInfo.result[i].avg_delay[j]);
if (avgDelay > 0.9 * MaxTCPPingValue) {
loss += 1
}
if (avgDelay > 0) {
data.push([monitorInfo.result[i].created_at[j], avgDelay]);
}
}
lossRate = ((loss / monitorInfo.result[i].created_at.length) * 100).toFixed(1);
legendName = monitorInfo.result[i].monitor_name +" "+ lossRate + "%";
tLegendData.push(legendName);
tSeries.push({
name: legendName,
type: 'line',
smooth: true,
symbol: 'none',
data: data,
markPoint: {
data: [
{ type: 'max', symbol: 'pin', name: 'Max', itemStyle: { color: '#f00' } },
{ type: 'min', symbol: 'pin', name: 'Min', itemStyle: { color: '#0f0' } }
]
}
});
}
this.option.title.text = monitorInfo.result[0].server_name;
this.option.series = tSeries;
this.option.legend.data = tLegendData;
this.myChart.clear();
this.myChart.setOption(this.option);
},
isWindowsPlatform(str) {
return str.includes('Windows')
},
renderChart() {
this.myChart = echarts.init(this.$refs.chartDom);
this.myChart.setOption(this.option);
},
},
beforeDestroy() {
this.myChart.dispose();
this.myChart = null;
},
});
</script>
{{end}}
+5 -5
View File
@@ -14,8 +14,8 @@
<meta http-equiv="Content-Language" content="zh" />
<!-- Styles -->
<link rel="stylesheet" type="text/css" href="https://cdn.staticfile.org/semantic-ui/2.4.1/semantic.min.css">
<link rel="stylesheet" type="text/css" href="https://cdn.staticfile.org/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" type="text/css" href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.css">
<link rel="stylesheet" type="text/css" href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" type="text/css" href="/static/semantic-ui-alerts.min.css">
<link rel="stylesheet" href="/static/theme-hotaru/css/core.css?v202012121912" type="text/css">
<link rel="stylesheet" href="/static/theme-hotaru/css/main.css?v202101171153" type="text/css">
@@ -141,10 +141,10 @@
<a href="/server">{{tr "AdminPanel"}}</a>
</p>
</footer>
<script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdn.staticfile.org/semantic-ui/2.4.1/semantic.min.js"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.min.js"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.js"></script>
<script src="/static/semantic-ui-alerts.min.js"></script>
<script src="https://cdn.staticfile.org/vue/2.6.14/vue.min.js"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
<script>
const initData = JSON.parse('{{.Servers}}').servers;
var statusCards = new Vue({
+5 -5
View File
@@ -9,7 +9,7 @@
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
<!-- MDUI CSS -->
<link rel="stylesheet" href="https://cdn.staticfile.org/mdui/1.0.2/css/mdui.min.css"/>
<link rel="stylesheet" href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/mdui/1.0.2/css/mdui.min.css"/>
<link rel="stylesheet" href="/static/theme-mdui/mdui.css" type="text/css">
<style>
.mdui-table td, .mdui-table th{padding: 6px;}
@@ -37,7 +37,7 @@
<div class="mdui-col" v-for='server in servers' :id="server.ID">
<div :class="'mdui-card mt' + (server.live?'':' offline')">
<div class="mdui-card-header">
<img class="mdui-card-header-avatar" :src="'https://cdn.staticfile.org/flag-icon-css/4.1.5/flags/1x1/' + (server.Host.CountryCode?server.Host.CountryCode:'cn') + '.svg'"/>
<img class="mdui-card-header-avatar" :src="'https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/flag-icon-css/4.1.5/flags/1x1/' + (server.Host.CountryCode?server.Host.CountryCode:'cn') + '.svg'"/>
<div class="mdui-card-header-title">@#server.Name#@</div>
<div class="mdui-card-header-subtitle">@#server.Host.CountryCode.toUpperCase()#@ | @#server.Host.Platform#@ @#server.Host.PlatformVersion#@</div>
</div>
@@ -153,9 +153,9 @@
{{template "theme-mdui/footer" .}}
<script src="/static/theme-mdui/mdui.js"></script>
<script src="https://cdn.staticfile.org/mdui/1.0.2/js/mdui.min.js"></script>
<script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdn.staticfile.org/vue/2.6.14/vue.min.js"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/mdui/1.0.2/js/mdui.min.js"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.min.js"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
<script>
var container=document.querySelector("#container");
+1
View File
@@ -12,6 +12,7 @@
{{else}}
<a href="/" class='mdui-ripple mdui-ripple-white mdui-hoverable{{if eq .MatchedPath "/"}} mdui-tab-active{{end}}'><i class="mdui-icon material-icons">home</i>{{tr "Home"}}</a>
<a href="/service" class='mdui-ripple mdui-ripple-white mdui-hoverable{{if eq .MatchedPath "/service"}} mdui-tab-active{{end}}'><i class="mdui-icon material-icons">accessibility</i>{{tr "Services"}}</a>
<a href="/network" class='mdui-ripple mdui-ripple-white mdui-hoverable{{if eq .MatchedPath "/network"}} mdui-tab-active{{end}}'><i class="mdui-icon material-icons">network_check</i>{{tr "NetworkSpiter"}}</a>
{{end}}
<div class="mdui-toolbar-spacer"></div>
{{if .Admin}}
+255
View File
@@ -0,0 +1,255 @@
{{define "theme-mdui/network"}}
<!doctype html>
<html lang="{{.Conf.Language}}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>{{.Title}}</title>
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
<!-- MDUI CSS -->
<link rel="stylesheet" href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/mdui/1.0.2/css/mdui.min.css"/>
<link rel="stylesheet" href="/static/theme-mdui/mdui.css" type="text/css">
<style>
.mdui-table td, .mdui-table th{padding: 6px;}
.progress{width: 10%;min-width: 75px;}
.progress-text{font-size: 16px;font-weight: 800;position: relative;top: 4px;left: 6px;}
.offline st,.offline at,.offline gt,.offline .progress-text{color: grey;}
a{text-decoration:none;color:#333;}.mdui-theme-layout-dark a{color:#fff;}
</style>
{{if ts .CustomCode}}
{{.CustomCode|safe}}
{{end}}
</head>
<body>
{{template "theme-mdui/menu" .}}
<div class="nb-container" id="app">
<div class="ui container">
<div class="service-status">
<table class="ui celled table">
<button
v-for="server in servers"
@click="redirectNetwork(server.ID)"
class="mdui-btn mdui-btn-raised mdui-color-theme mdui-color-indigo mdui-text-color-white"
style="margin-top: 6px;margin-left:5px">
<img :src="'https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/flag-icon-css/4.1.5/flags/4x3/' + (server.Host.CountryCode?server.Host.CountryCode:'cn') + '.svg'" alt="Flag Icon" style="vertical-align: middle;height:14px"> <span>@#server.Name#@ &nbsp;</span></button>
</table>
</div>
</div>
<div class="ui container" style="max-width: 95vw">
<div ref="chartDom" style="border-radius: 28px; margin-top: 15px;height: 520px;max-width: 1400px;overflow: hidden"></div>
</div>
</div>
{{template "theme-mdui/footer" .}}
<script src="/static/theme-mdui/mdui.js"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/mdui/1.0.2/js/mdui.min.js"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.min.js"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/echarts/5.3.0-rc.1/echarts.min.js"></script>
<script>
const monitorInfo = JSON.parse('{{.MonitorInfos}}');
const initData = JSON.parse('{{.Servers}}').servers;
let MaxTCPPingValue = {{.MaxTCPPingValue}};
if (MaxTCPPingValue == null) {
MaxTCPPingValue = 1000;
}
new Vue({
el: '#app',
delimiters: ['@#', '#@'],
data: {
servers: initData,
option: {
tooltip: {
trigger: 'axis',
position: function (pt) {
return [pt[0], '10%'];
},
formatter: function(params){
let result = params[0].axisValueLabel + "<br />";
params.forEach(function(item){
result += item.marker + item.seriesName + ": " + item.value[1].toFixed(2) + " ms<br />";
})
return result;
},
confine: true,
transitionDuration: 0
},
title: {
left: 'center',
text: "",
textStyle: {}
},
legend: {
top: '5%',
data: [],
textStyle: {
fontSize: 14
}
},
backgroundColor: 'rgba(255, 255, 255, 0.8)',
toolbox: {
feature: {
dataZoom: {
yAxisIndex: 'none'
},
restore: {},
saveAsImage: {}
}
},
dataZoom: [
{
start: 0,
end: 100
}
],
xAxis: {
type: 'time',
boundaryGap: false
},
yAxis: {
type: 'value',
boundaryGap: false
},
series: [],
},
chartOnOff: true,
},
mounted() {
this.renderChart();
this.parseMonitorInfo(monitorInfo);
},
methods: {
getFontLogoClass(str) {
if (["almalinux",
"alpine",
"aosc",
"apple",
"archlinux",
"archlabs",
"artix",
"budgie",
"centos",
"coreos",
"debian",
"deepin",
"devuan",
"docker",
"elementary",
"fedora",
"ferris",
"flathub",
"freebsd",
"gentoo",
"gnu-guix",
"illumos",
"kali-linux",
"linuxmint",
"mageia",
"mandriva",
"manjaro",
"nixos",
"openbsd",
"opensuse",
"pop-os",
"raspberry-pi",
"redhat",
"rocky-linux",
"sabayon",
"slackware",
"snappy",
"solus",
"tux",
"ubuntu",
"void",
"zorin"].indexOf(str)
> -1) {
return str;
}
if (['openwrt', 'linux', "immortalwrt"].indexOf(str) > -1) {
return 'tux';
}
if (str == 'amazon') {
return 'redhat';
}
if (str == 'arch') {
return 'archlinux';
}
return '';
},
redirectNetwork(id) {
this.getMonitorHistory(id)
.then(function(monitorInfo) {
var vm = app.__vue__;
vm.parseMonitorInfo(monitorInfo);
})
.catch(function(error){
window.location.href = "/404";
})
},
getMonitorHistory(id) {
return $.ajax({
url: "/api/v1/monitor/"+id,
method: "GET"
});
},
parseMonitorInfo(monitorInfo) {
let tSeries = [];
let tLegendData = [];
for (let i = 0; i < monitorInfo.result.length; i++) {
let loss = 0;
let data = [];
for (let j = 0; j < monitorInfo.result[i].created_at.length; j++) {
avgDelay = Math.round(monitorInfo.result[i].avg_delay[j]);
if (avgDelay > 0.9 * MaxTCPPingValue) {
loss += 1
}
if (avgDelay > 0) {
data.push([monitorInfo.result[i].created_at[j], avgDelay]);
}
}
lossRate = ((loss / monitorInfo.result[i].created_at.length) * 100).toFixed(1);
legendName = monitorInfo.result[i].monitor_name +" "+ lossRate + "%";
tLegendData.push(legendName);
tSeries.push({
name: legendName,
type: 'line',
smooth: true,
symbol: 'none',
data: data,
markPoint: {
data: [
{ type: 'max', symbol: 'pin', name: 'Max', itemStyle: { color: '#f00' } },
{ type: 'min', symbol: 'pin', name: 'Min', itemStyle: { color: '#0f0' } }
]
}
});
}
this.option.title.text = monitorInfo.result[0].server_name;
this.option.series = tSeries;
this.option.legend.data = tLegendData;
this.myChart.clear();
this.myChart.setOption(this.option);
},
isWindowsPlatform(str) {
return str.includes('Windows')
},
renderChart() {
this.myChart = echarts.init(this.$refs.chartDom);
this.myChart.setOption(this.option);
},
},
beforeDestroy() {
this.myChart.dispose();
this.myChart = null;
},
});
</script>
</body>
</html>
{{end}}
+4 -4
View File
@@ -9,7 +9,7 @@
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
<!-- MDUI CSS -->
<link rel="stylesheet" href="https://cdn.staticfile.org/mdui/1.0.2/css/mdui.min.css"/>
<link rel="stylesheet" href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/mdui/1.0.2/css/mdui.min.css"/>
<link rel="stylesheet" href="/static/theme-mdui/mdui.css" type="text/css">
{{if ts .CustomCode}}
{{.CustomCode|safe}}
@@ -116,9 +116,9 @@
{{template "theme-mdui/footer" .}}
<script src="/static/theme-mdui/mdui.js"></script>
<script src="https://cdn.staticfile.org/mdui/1.0.2/js/mdui.min.js"></script>
<script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdn.staticfile.org/vue/2.6.14/vue.min.js"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/mdui/1.0.2/js/mdui.min.js"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.min.js"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
<script>
var container=document.querySelector("#container");
container.style.minHeight=window.innerHeight-document.body.clientHeight+container.clientHeight+'px';
+3 -3
View File
@@ -8,7 +8,7 @@
<title>{{.Title}}</title>
<!-- MDUI CSS -->
<link rel="stylesheet" href="https://cdn.staticfile.org/mdui/1.0.2/css/mdui.min.css"/>
<link rel="stylesheet" href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/mdui/1.0.2/css/mdui.min.css"/>
<link rel="stylesheet" href="/static/theme-mdui/mdui.css" type="text/css">
{{if ts .CustomCode}}
{{.CustomCode|safe}}
@@ -34,8 +34,8 @@
</div>
</div>
<script src="https://cdn.staticfile.org/mdui/1.0.2/js/mdui.min.js"></script>
<script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/mdui/1.0.2/js/mdui.min.js"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.min.js"></script>
<script>
var $input = mdui.$('#container').find('.mdui-textfield-input');
var $dialog = new mdui.Dialog(mdui.$('.mdui-dialog'));
@@ -18,6 +18,7 @@
<ul class="nav navbar-nav">
<li><a href="/">{{tr "Home" }}</a></li>
<li><a href="/service">{{tr "Services" }}</a></li>
<li><a href="/network">{{tr "NetworkSpiter" }}</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li class="dropdown">
@@ -45,3 +46,4 @@
</div>
</div>
{{end}}
+5 -4
View File
@@ -12,8 +12,8 @@
<link rel="stylesheet" href="/static/theme-server-status/css/dark.css">
<link rel="stylesheet" href="/static/theme-server-status/css/light.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="https://cdn.staticfile.org/font-logos/0.17/font-logos.min.css">
<link rel="stylesheet" href="https://cdn.staticfile.org/semantic-ui/2.4.1/semantic.min.css">
<link rel="stylesheet" href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/font-logos/0.17/font-logos.min.css">
<link rel="stylesheet" href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/[email protected]/css/flag-icons.min.css">
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
@@ -26,8 +26,9 @@
{{end}}
<script src="/static/theme-server-status/js/jquery.min.js"></script>
<script src="/static/theme-server-status/js/bootstrap.min.js"></script>
<script src="https://cdn.staticfile.org/vue/2.6.14/vue.min.js"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
<script src="/static/theme-server-status/js/mixin.js"></script>
<script src="https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/echarts/5.3.0-rc.1/echarts.min.js"></script>
</head>
<body>
{{end}}
{{end}}
+217
View File
@@ -0,0 +1,217 @@
{{define "theme-server-status/network"}}
{{template "theme-server-status/header" .}}
<div id="app">
{{template "theme-server-status/content-nav" .}}
<div class="container table-responsive content" style="max-width: 95vw">
<table class="table table-striped table-condensed table-hover">
<button class="ui nezha-primary-btn button"
v-for="server in servers"
style="margin-top: 3px"
@click="redirectNetwork(server.ID)">
@#server.Name#@&nbsp;&nbsp;<i :class="'fi fi-' + server.Host.CountryCode"></i><span class="node-cell-location-text text-uppercase">&nbsp;&nbsp;@#server.Host.CountryCode#@</span>
</button>
</table>
</div>
<div class="container table-responsive content" style="max-width: 95vw">
<div ref="chartDom" style="border-radius: 28px; margin-top: 15px;height: 520px;max-width: 1400px;overflow: hidden"></div>
</div>
{{template "theme-server-status/content-footer" .}}
</div>
<script>
const monitorInfo = JSON.parse('{{.MonitorInfos}}');
const initData = JSON.parse('{{.Servers}}').servers;
let MaxTCPPingValue = {{.MaxTCPPingValue}};
if (MaxTCPPingValue == null) {
MaxTCPPingValue = 300;
}
new Vue({
el: '#app',
delimiters: ['@#', '#@'],
data: {
servers: initData,
option: {
tooltip: {
trigger: 'axis',
position: function (pt) {
return [pt[0], '10%'];
},
formatter: function(params){
let result = params[0].axisValueLabel + "<br />";
params.forEach(function(item){
result += item.marker + item.seriesName + ": " + item.value[1].toFixed(2) + " ms<br />";
})
return result;
},
confine: true,
transitionDuration: 0
},
title: {
left: 'center',
text: "",
textStyle: {}
},
legend: {
top: '5%',
data: [],
textStyle: {
fontSize: 14
}
},
backgroundColor: 'rgba(255, 255, 255, 0.8)',
toolbox: {
feature: {
dataZoom: {
yAxisIndex: 'none'
},
restore: {},
saveAsImage: {}
}
},
dataZoom: [
{
start: 94,
end: 100
}
],
xAxis: {
type: 'time',
boundaryGap: false
},
yAxis: {
type: 'value',
boundaryGap: [0, '100%']
},
series: [],
},
chartOnOff: true,
},
mixins: [mixinsVue],
created() {
this.initTheme();
},
mounted() {
this.renderChart();
this.parseMonitorInfo(monitorInfo);
},
methods: {
getFontLogoClass(str) {
if (["almalinux",
"alpine",
"aosc",
"apple",
"archlinux",
"archlabs",
"artix",
"budgie",
"centos",
"coreos",
"debian",
"deepin",
"devuan",
"docker",
"elementary",
"fedora",
"ferris",
"flathub",
"freebsd",
"gentoo",
"gnu-guix",
"illumos",
"kali-linux",
"linuxmint",
"mageia",
"mandriva",
"manjaro",
"nixos",
"openbsd",
"opensuse",
"pop-os",
"raspberry-pi",
"redhat",
"rocky-linux",
"sabayon",
"slackware",
"snappy",
"solus",
"tux",
"ubuntu",
"void",
"zorin"].indexOf(str)
> -1) {
return str;
}
if (['openwrt', 'linux', "immortalwrt"].indexOf(str) > -1) {
return 'tux';
}
if (str == 'amazon') {
return 'redhat';
}
if (str == 'arch') {
return 'archlinux';
}
return '';
},
redirectNetwork(id) {
this.getMonitorHistory(id)
.then(function(monitorInfo) {
var vm = app.__vue__;
vm.parseMonitorInfo(monitorInfo);
})
.catch(function(error){
window.location.href = "/404";
})
},
getMonitorHistory(id) {
return $.ajax({
url: "/api/v1/monitor/"+id,
method: "GET"
});
},
parseMonitorInfo(monitorInfo) {
let tSeries = [];
let tLegendData = [];
for (let i = 0; i < monitorInfo.result.length; i++) {
let loss = 0;
let data = [];
for (let j = 0; j < monitorInfo.result[i].created_at.length; j++) {
avgDelay = monitorInfo.result[i].avg_delay[j];
if (avgDelay > 0.9 * MaxTCPPingValue) {
loss += 1
}
data.push([monitorInfo.result[i].created_at[j], avgDelay]);
}
lossRate = ((loss / monitorInfo.result[i].created_at.length) * 100).toFixed(1);
legendName = monitorInfo.result[i].monitor_name +" "+ lossRate + "%";
tLegendData.push(legendName);
tSeries.push({
name: legendName,
type: 'line',
smooth: true,
symbol: 'none',
data: data
});
}
this.option.title.text = monitorInfo.result[0].server_name;
this.option.series = tSeries;
this.option.legend.data = tLegendData;
this.myChart.clear();
this.myChart.setOption(this.option);
},
isWindowsPlatform(str) {
return str.includes('Windows')
},
renderChart() {
this.myChart = echarts.init(this.$refs.chartDom);
this.myChart.setOption(this.option);
},
},
beforeDestroy() {
this.myChart.dispose();
this.myChart = null;
},
});
</script>
{{template "theme-server-status/footer" .}}
{{end}}
+10
View File
@@ -12,3 +12,13 @@ site:
brand: "nz_site_title"
cookiename: "nezha-dashboard" #浏览器 Cookie 字段名,可不改
theme: "default"
ddns:
enable: false
provider: "webhook"
accessid: ""
accesssecret: ""
webhookmethod: ""
webhookurl: ""
webhookrequestbody: ""
webhookheaders: ""
maxretries: 3
+2 -2
View File
@@ -38,8 +38,8 @@ $download = "https://github.com/$agentrepo/releases/download/$agenttag/$file"
$nssmdownload="https://github.com/$nssmrepo/releases/download/$nssmtag/nssm.zip"
Write-Host "Location:$region,connect directly!" -BackgroundColor DarkRed -ForegroundColor Green
}else{
$download = "https://dn-dao-github-mirror.daocloud.io/$agentrepo/releases/download/$agenttag/$file"
$nssmdownload="https://dn-dao-github-mirror.daocloud.io/$nssmrepo/releases/download/$nssmtag/nssm.zip"
$download = "https://kkgithub.com/$agentrepo/releases/download/$agenttag/$file"
$nssmdownload="https://kkgithub.com/$nssmrepo/releases/download/$nssmtag/nssm.zip"
Write-Host "Location:CN,use mirror address" -BackgroundColor DarkRed -ForegroundColor Green
}
echo $download
+311 -278
View File
@@ -14,7 +14,7 @@ NZ_AGENT_SERVICE="/etc/systemd/system/nezha-agent.service"
NZ_AGENT_SERVICERC="/etc/init.d/nezha-agent"
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
NZ_VERSION="v0.15.3"
NZ_VERSION="v0.15.8"
red='\033[0;31m'
green='\033[0;32m'
@@ -34,15 +34,15 @@ pre_check() {
## os_arch
if [[ $(uname -m | grep 'x86_64') != "" ]]; then
os_arch="amd64"
elif [[ $(uname -m | grep 'i386\|i686') != "" ]]; then
elif [[ $(uname -m | grep 'i386\|i686') != "" ]]; then
os_arch="386"
elif [[ $(uname -m | grep 'aarch64\|armv8b\|armv8l') != "" ]]; then
elif [[ $(uname -m | grep 'aarch64\|armv8b\|armv8l') != "" ]]; then
os_arch="arm64"
elif [[ $(uname -m | grep 'arm') != "" ]]; then
elif [[ $(uname -m | grep 'arm') != "" ]]; then
os_arch="arm"
elif [[ $(uname -m | grep 's390x') != "" ]]; then
elif [[ $(uname -m | grep 's390x') != "" ]]; then
os_arch="s390x"
elif [[ $(uname -m | grep 'riscv64') != "" ]]; then
elif [[ $(uname -m | grep 'riscv64') != "" ]]; then
os_arch="riscv64"
fi
@@ -50,39 +50,57 @@ pre_check() {
if [[ -z "${CN}" ]]; then
if [[ $(curl -m 10 -s https://ipapi.co/json | grep 'China') != "" ]]; then
echo "根据ipapi.co提供的信息,当前IP可能在中国"
read -e -r -p "是否选用中国镜像完成安装? [Y/n] " input
read -e -r -p "是否选用中国镜像完成安装? [Y/n] (自定义镜像输入 3):" input
case $input in
[yY][eE][sS] | [yY])
echo "使用中国镜像"
CN=true
[yY][eE][sS] | [yY])
echo "使用中国镜像"
CN=true
;;
[nN][oO] | [nN])
echo "不使用中国镜像"
[nN][oO] | [nN])
echo "不使用中国镜像"
;;
[3])
echo "使用自定义镜像"
read -e -r -p "请输入自定义镜像 (例如:dn-dao-github-mirror.daocloud.io),留空为不使用: " input
case $input in
*)
echo "使用中国镜像"
CN=true
CUSTOM_MIRROR=$input
;;
esac
;;
*)
echo "使用中国镜像"
CN=true
;;
esac
fi
fi
if [[ -z "${CN}" ]]; then
GITHUB_RAW_URL="raw.githubusercontent.com/naiba/nezha/master"
GITHUB_URL="github.com"
Get_Docker_URL="get.docker.com"
Get_Docker_Argu=" "
Docker_IMG="ghcr.io\/naiba\/nezha-dashboard"
GITHUB_RELEASE_URL="github.com/naiba/nezha/releases/latest/download"
else
if [[ -n "${CUSTOM_MIRROR}" ]]; then
GITHUB_RAW_URL="gitee.com/naibahq/nezha/raw/master"
GITHUB_URL="dn-dao-github-mirror.daocloud.io"
GITHUB_URL=$CUSTOM_MIRROR
Get_Docker_URL="get.docker.com"
Get_Docker_Argu=" -s docker --mirror Aliyun"
Docker_IMG="registry.cn-shanghai.aliyuncs.com\/naibahq\/nezha-dashboard"
GITHUB_RELEASE_URL="hub.fgit.cf/naiba/nezha/releases/latest/download"
else
if [[ -z "${CN}" ]]; then
GITHUB_RAW_URL="raw.githubusercontent.com/naiba/nezha/master"
GITHUB_URL="github.com"
Get_Docker_URL="get.docker.com"
Get_Docker_Argu=" "
Docker_IMG="ghcr.io\/naiba\/nezha-dashboard"
else
GITHUB_RAW_URL="gitee.com/naibahq/nezha/raw/master"
GITHUB_URL="kkgithub.com"
Get_Docker_URL="get.docker.com"
Get_Docker_Argu=" -s docker --mirror Aliyun"
Docker_IMG="registry.cn-shanghai.aliyuncs.com\/naibahq\/nezha-dashboard"
fi
fi
}
confirm() {
@@ -127,39 +145,39 @@ before_show_menu() {
install_base() {
(command -v git >/dev/null 2>&1 && command -v curl >/dev/null 2>&1 && command -v wget >/dev/null 2>&1 && command -v unzip >/dev/null 2>&1 && command -v getenforce >/dev/null 2>&1) ||
(install_soft curl wget git unzip)
(install_soft curl wget git unzip)
}
install_arch(){
install_arch() {
echo -e "${green}提示: ${plain} Arch安装libselinux需添加nezha-agent用户,安装完会自动删除,建议手动检查一次\n"
read -e -r -p "是否安装libselinux? [Y/n] " input
case $input in
[yY][eE][sS] | [yY])
useradd -m nezha-agent
sed -i "$ a\nezha-agent ALL=(ALL ) NOPASSWD:ALL" /etc/sudoers
sudo -iu nezha-agent bash -c 'gpg --keyserver keys.gnupg.net --recv-keys BE22091E3EF62275;
[yY][eE][sS] | [yY])
useradd -m nezha-agent
sed -i "$ a\nezha-agent ALL=(ALL ) NOPASSWD:ALL" /etc/sudoers
sudo -iu nezha-agent bash -c 'gpg --keyserver keys.gnupg.net --recv-keys BE22091E3EF62275;
cd /tmp; git clone https://aur.archlinux.org/libsepol.git; cd libsepol; makepkg -si --noconfirm --asdeps; cd ..;
git clone https://aur.archlinux.org/libselinux.git; cd libselinux; makepkg -si --noconfirm; cd ..;
rm -rf libsepol libselinux'
sed -i '/nezha-agent/d' /etc/sudoers && sleep 30s && killall -u nezha-agent&&userdel nezha-agent
echo -e "${red}提示: ${plain}已删除用户nezha-agent,请务必手动核查一遍!\n"
sed -i '/nezha-agent/d' /etc/sudoers && sleep 30s && killall -u nezha-agent && userdel -r nezha-agent
echo -e "${red}提示: ${plain}已删除用户nezha-agent,请务必手动核查一遍!\n"
;;
[nN][oO] | [nN])
echo "不安装libselinux"
[nN][oO] | [nN])
echo "不安装libselinux"
;;
*)
echo "不安装libselinux"
exit 0
*)
echo "不安装libselinux"
exit 0
;;
esac
}
install_soft() {
(command -v yum >/dev/null 2>&1 && yum makecache && yum install $* selinux-policy -y) ||
(command -v apt >/dev/null 2>&1 && apt update && apt install $* selinux-utils -y) ||
(command -v pacman >/dev/null 2>&1 && pacman -Syu $* base-devel --noconfirm && install_arch) ||
(command -v apt-get >/dev/null 2>&1 && apt-get update && apt-get install $* selinux-utils -y) ||
(command -v apk >/dev/null 2>&1 && apk update && apk add $* -f)
(command -v apt >/dev/null 2>&1 && apt update && apt install $* selinux-utils -y) ||
(command -v pacman >/dev/null 2>&1 && pacman -Syu $* base-devel --noconfirm && install_arch) ||
(command -v apt-get >/dev/null 2>&1 && apt-get update && apt-get install $* selinux-utils -y) ||
(command -v apk >/dev/null 2>&1 && apk update && apk add $* -f)
}
install_dashboard() {
@@ -174,16 +192,16 @@ install_dashboard() {
echo "您可能已经安装过面板端,重复安装会覆盖数据,请注意备份。"
read -e -r -p "是否退出安装? [Y/n] " input
case $input in
[yY][eE][sS] | [yY])
echo "退出安装"
exit 0
[yY][eE][sS] | [yY])
echo "退出安装"
exit 0
;;
[nN][oO] | [nN])
echo "继续安装"
[nN][oO] | [nN])
echo "继续安装"
;;
*)
echo "退出安装"
exit 0
*)
echo "退出安装"
exit 0
;;
esac
fi
@@ -212,9 +230,9 @@ install_dashboard() {
install_dashboard_standalone() {
install_base
echo -e "> 安装面板"
# 哪吒监控文件夹
if [ ! -d "${NZ_DASHBOARD_PATH}/app" ]; then
mkdir -p $NZ_DASHBOARD_PATH
@@ -222,38 +240,39 @@ install_dashboard_standalone() {
echo "您可能已经安装过面板端,重复安装会覆盖数据,请注意备份。"
read -e -r -p "是否退出安装? [Y/n] " input
case $input in
[yY][eE][sS] | [yY])
echo "退出安装"
exit 0
[yY][eE][sS] | [yY])
echo "退出安装"
exit 0
;;
[nN][oO] | [nN])
echo "继续安装"
[nN][oO] | [nN])
echo "继续安装"
;;
*)
echo "退出安装"
exit 0
*)
echo "退出安装"
exit 0
;;
esac
fi
if [ ! -d "${NZ_DASHBOARD_PATH}/resource/template/theme-custom" ] || [ ! -d "${NZ_DASHBOARD_PATH}/resource/static/custom" ]; then
mkdir -p "${NZ_DASHBOARD_PATH}/resource/template/theme-custom" "${NZ_DASHBOARD_PATH}/resource/static/custom" >/dev/null 2>&1
fi
chmod 777 -R $NZ_DASHBOARD_PATH
modify_dashboard_config_standalone 0
if [[ $# == 0 ]]; then
before_show_menu
fi
}
selinux(){
selinux() {
#判断当前的状态
if [ "$os_alpine" != 1 ];then
command -v getenforce >/dev/null 2>&1
if [ $? -eq 0 ]; then
getenforce | grep '[Ee]nfor'
if [ $? -eq 0 ];then
if [ $? -eq 0 ]; then
echo -e "SELinux是开启状态,正在关闭!"
setenforce 0 &>/dev/null
find_key="SELINUX="
@@ -297,8 +316,8 @@ install_agent() {
fi
unzip -qo nezha-agent_linux_${os_arch}.zip &&
mv nezha-agent $NZ_AGENT_PATH &&
rm -rf nezha-agent_linux_${os_arch}.zip README.md
mv nezha-agent $NZ_AGENT_PATH &&
rm -rf nezha-agent_linux_${os_arch}.zip README.md
if [ $# -ge 3 ]; then
modify_agent_config "$@"
@@ -314,7 +333,7 @@ install_agent() {
modify_agent_config() {
echo -e "> 修改Agent配置"
if [ "$os_alpine" != 1 ];then
if [ "$os_alpine" != 1 ]; then
wget -t 2 -T 10 -O $NZ_AGENT_SERVICE https://${GITHUB_RAW_URL}/script/nezha-agent.service >/dev/null 2>&1
if [[ $? != 0 ]]; then
echo -e "${red}文件下载失败,请检查本机能否连接 ${GITHUB_RAW_URL}${plain}"
@@ -331,11 +350,11 @@ modify_agent_config() {
if [ $# -lt 3 ]; then
echo "请先在管理面板上添加Agent,记录下密钥" &&
read -ep "请输入一个解析到面板所在IP的域名(不可套CDN): " nz_grpc_host &&
read -ep "请输入面板RPC端口 (默认值 5555): " nz_grpc_port &&
read -ep "请输入Agent 密钥: " nz_client_secret &&
read -ep "是否启用针对 gRPC 端口的 SSL/TLS加密 (--tls),需要请按 [y],默认是不需要,不理解用户可回车跳过: " nz_grpc_proxy
grep -qiw 'Y' <<< "${nz_grpc_proxy}" && args='--tls'
read -ep "请输入一个解析到面板所在IP的域名(不可套CDN): " nz_grpc_host &&
read -ep "请输入面板RPC端口 (默认值 5555): " nz_grpc_port &&
read -ep "请输入Agent 密钥: " nz_client_secret &&
read -ep "是否启用针对 gRPC 端口的 SSL/TLS加密 (--tls),需要请按 [y],默认是不需要,不理解用户可回车跳过: " nz_grpc_proxy
grep -qiw 'Y' <<<"${nz_grpc_proxy}" && args='--tls'
if [[ -z "${nz_grpc_host}" || -z "${nz_client_secret}" ]]; then
echo -e "${red}所有选项都不能为空${plain}"
before_show_menu
@@ -354,7 +373,7 @@ modify_agent_config() {
fi
fi
if [ "$os_alpine" != 1 ];then
if [ "$os_alpine" != 1 ]; then
sed -i "s/nz_grpc_host/${nz_grpc_host}/" ${NZ_AGENT_SERVICE}
sed -i "s/nz_grpc_port/${nz_grpc_port}/" ${NZ_AGENT_SERVICE}
sed -i "s/nz_client_secret/${nz_client_secret}/" ${NZ_AGENT_SERVICE}
@@ -368,7 +387,7 @@ modify_agent_config() {
echo -e "Agent配置 ${green}修改成功,请稍等重启生效${plain}"
if [ "$os_alpine" != 1 ];then
if [ "$os_alpine" != 1 ]; then
systemctl daemon-reload
systemctl enable nezha-agent
systemctl restart nezha-agent
@@ -399,14 +418,14 @@ modify_dashboard_config() {
fi
echo "关于 GitHub Oauth2 应用:在 https://github.com/settings/developers 创建,无需审核,Callback 填 http(s)://域名或IP/oauth2/callback" &&
echo "关于 Gitee Oauth2 应用:在 https://gitee.com/oauth/applications 创建,无需审核,Callback 填 http(s)://域名或IP/oauth2/callback" &&
read -ep "请输入 OAuth2 提供商(github/gitlab/jihulab/gitee,默认 github): " nz_oauth2_type &&
read -ep "请输入 Oauth2 应用的 Client ID: " nz_github_oauth_client_id &&
read -ep "请输入 Oauth2 应用的 Client Secret: " nz_github_oauth_client_secret &&
read -ep "请输入 GitHub/Gitee 登录名作为管理员,多个以逗号隔开: " nz_admin_logins &&
read -ep "请输入站点标题: " nz_site_title &&
read -ep "请输入站点访问端口: (默认 8008)" nz_site_port &&
read -ep "请输入用于 Agent 接入的 RPC 端口: (默认 5555)" nz_grpc_port
echo "关于 Gitee Oauth2 应用:在 https://gitee.com/oauth/applications 创建,无需审核,Callback 填 http(s)://域名或IP/oauth2/callback" &&
read -ep "请输入 OAuth2 提供商(github/gitlab/jihulab/gitee,默认 github): " nz_oauth2_type &&
read -ep "请输入 Oauth2 应用的 Client ID: " nz_github_oauth_client_id &&
read -ep "请输入 Oauth2 应用的 Client Secret: " nz_github_oauth_client_secret &&
read -ep "请输入 GitHub/Gitee 登录名作为管理员,多个以逗号隔开: " nz_admin_logins &&
read -ep "请输入站点标题: " nz_site_title &&
read -ep "请输入站点访问端口: (默认 8008)" nz_site_port &&
read -ep "请输入用于 Agent 接入的 RPC 端口: (默认 5555)" nz_grpc_port
if [[ -z "${nz_admin_logins}" || -z "${nz_github_oauth_client_id}" || -z "${nz_github_oauth_client_secret}" || -z "${nz_site_title}" ]]; then
echo -e "${red}所有选项都不能为空${plain}"
@@ -450,31 +469,31 @@ modify_dashboard_config() {
modify_dashboard_config_standalone() {
echo -e "> 修改面板配置"
echo -e "正在下载配置模板"
wget -t 2 -T 10 -O /tmp/nezha-config.yaml https://${GITHUB_RAW_URL}/script/config.yaml >/dev/null 2>&1
if [[ $? != 0 ]]; then
echo -e "${red}下载文件失败,请检查本机能否连接 ${GITHUB_RAW_URL}${plain}"
return 0
fi
echo "关于 GitHub Oauth2 应用:在 https://github.com/settings/developers 创建,无需审核,Callback 填 http(s)://域名或IP/oauth2/callback" &&
echo "关于 Gitee Oauth2 应用:在 https://gitee.com/oauth/applications 创建,无需审核,Callback 填 http(s)://域名或IP/oauth2/callback" &&
read -ep "请输入 OAuth2 提供商(github/gitlab/jihulab/gitee,默认 github): " nz_oauth2_type &&
read -ep "请输入 Oauth2 应用的 Client ID: " nz_github_oauth_client_id &&
read -ep "请输入 Oauth2 应用的 Client Secret: " nz_github_oauth_client_secret &&
read -ep "请输入 GitHub/Gitee 登录名作为管理员,多个以逗号隔开: " nz_admin_logins &&
read -ep "请输入站点标题: " nz_site_title &&
read -ep "请输入站点访问端口: (默认 8008)" nz_site_port &&
read -ep "请输入用于 Agent 接入的 RPC 端口: (默认 5555)" nz_grpc_port
echo "关于 Gitee Oauth2 应用:在 https://gitee.com/oauth/applications 创建,无需审核,Callback 填 http(s)://域名或IP/oauth2/callback" &&
read -ep "请输入 OAuth2 提供商(github/gitlab/jihulab/gitee,默认 github): " nz_oauth2_type &&
read -ep "请输入 Oauth2 应用的 Client ID: " nz_github_oauth_client_id &&
read -ep "请输入 Oauth2 应用的 Client Secret: " nz_github_oauth_client_secret &&
read -ep "请输入 GitHub/Gitee 登录名作为管理员,多个以逗号隔开: " nz_admin_logins &&
read -ep "请输入站点标题: " nz_site_title &&
read -ep "请输入站点访问端口: (默认 8008)" nz_site_port &&
read -ep "请输入用于 Agent 接入的 RPC 端口: (默认 5555)" nz_grpc_port
if [[ -z "${nz_admin_logins}" || -z "${nz_github_oauth_client_id}" || -z "${nz_github_oauth_client_secret}" || -z "${nz_site_title}" ]]; then
echo -e "${red}所有选项都不能为空${plain}"
before_show_menu
return 1
fi
if [[ -z "${nz_site_port}" ]]; then
nz_site_port=8008
fi
@@ -484,7 +503,7 @@ modify_dashboard_config_standalone() {
if [[ -z "${nz_oauth2_type}" ]]; then
nz_oauth2_type=github
fi
sed -i "s/nz_oauth2_type/${nz_oauth2_type}/" /tmp/nezha-config.yaml
sed -i "s/nz_admin_logins/${nz_admin_logins}/" /tmp/nezha-config.yaml
sed -i "s/nz_grpc_port/${nz_grpc_port}/" /tmp/nezha-config.yaml
@@ -493,15 +512,15 @@ modify_dashboard_config_standalone() {
sed -i "s/nz_language/zh-CN/" /tmp/nezha-config.yaml
sed -i "s/nz_site_title/${nz_site_title}/" /tmp/nezha-config.yaml
sed -i "s/80/${nz_site_port}/" /tmp/nezha-config.yaml
mkdir -p $NZ_DASHBOARD_PATH/data
mv -f /tmp/nezha-config.yaml ${NZ_DASHBOARD_PATH}/data/config.yaml
echo -e "正在下载服务文件"
if [ "$os_alpine" != 1 ];then
if [ "$os_alpine" != 1 ]; then
wget -t 2 -T 10 -O $NZ_DASHBOARD_SERVICE https://${GITHUB_RAW_URL}/script/nezha-dashboard.service >/dev/null 2>&1
else
else
wget -t 2 -T 10 -O $NZ_DASHBOARD_SERVICERC https://${GITHUB_RAW_URL}/script/nezha-dashboard >/dev/null 2>&1
chmod +x $NZ_DASHBOARD_SERVICERC
if [[ $? != 0 ]]; then
@@ -510,11 +529,10 @@ modify_dashboard_config_standalone() {
fi
fi
echo -e "面板配置 ${green}修改成功,请稍等重启生效${plain}"
restart_and_update_standalone
if [[ $# == 0 ]]; then
before_show_menu
fi
@@ -550,7 +568,7 @@ restart_and_update() {
restart_and_update_standalone() {
echo -e "> 重启并更新面板"
cd $NZ_DASHBOARD_PATH
if [ "$os_alpine" != 1 ]; then
@@ -559,9 +577,9 @@ restart_and_update_standalone() {
rc-service nezha-dashboard stop
fi
wget -qO app.zip $GITHUB_RELEASE_URL/dashboard-linux-$os_arch.zip >/dev/null 2>&1 && unzip -qq app.zip && mv dist/dashboard-linux-$os_arch app && rm -r app.zip dist
wget -qO app.zip https://${GITHUB_URL}/naiba/nezha/releases/latest/download/dashboard-linux-$os_arch.zip >/dev/null 2>&1 && unzip -qq app.zip && mv dist/dashboard-linux-$os_arch app && rm -r app.zip dist
if [ "$os_alpine" != 1 ];then
if [ "$os_alpine" != 1 ]; then
systemctl daemon-reload
systemctl enable nezha-dashboard
systemctl restart nezha-dashboard
@@ -569,14 +587,14 @@ restart_and_update_standalone() {
rc-update add nezha-dashboard
rc-service nezha-dashboard restart
fi
if [[ $? == 0 ]]; then
echo -e "${green}哪吒监控 重启成功${plain}"
echo -e "默认管理面板地址:${yellow}域名:站点访问端口${plain}"
else
echo -e "${red}重启失败,可能是因为启动时间超过了两秒,请稍后查看日志信息${plain}"
fi
if [[ $# == 0 ]]; then
before_show_menu
fi
@@ -605,19 +623,19 @@ start_dashboard() {
start_dashboard_standalone() {
echo -e "> 启动面板"
if [ "$os_alpine" != 1 ]; then
systemctl start nezha-dashboard
else
rc-service nezha-dashboard start
fi
if [[ $? == 0 ]]; then
echo -e "${green}哪吒监控 启动成功${plain}"
else
echo -e "${red}启动失败,请稍后查看日志信息${plain}"
fi
if [[ $# == 0 ]]; then
before_show_menu
fi
@@ -646,19 +664,19 @@ stop_dashboard() {
stop_dashboard_standalone() {
echo -e "> 停止面板"
if [ "$os_alpine" != 1 ]; then
systemctl stop nezha-dashboard
else
rc-service nezha-dashboard stop
fi
if [[ $? == 0 ]]; then
echo -e "${green}哪吒监控 停止成功${plain}"
else
echo -e "${red}停止失败,请稍后查看日志信息${plain}"
fi
if [[ $# == 0 ]]; then
before_show_menu
fi
@@ -681,7 +699,7 @@ show_dashboard_log() {
show_dashboard_log_standalone() {
echo -e "> 获取面板日志"
if [ "$os_alpine" != 1 ]; then
journalctl -xf -u nezha-dashboard.service
else
@@ -704,8 +722,8 @@ uninstall_dashboard() {
fi
rm -rf $NZ_DASHBOARD_PATH
docker rmi -f ghcr.io/naiba/nezha-dashboard > /dev/null 2>&1
docker rmi -f registry.cn-shanghai.aliyuncs.com/naibahq/nezha-dashboard > /dev/null 2>&1
docker rmi -f ghcr.io/naiba/nezha-dashboard >/dev/null 2>&1
docker rmi -f registry.cn-shanghai.aliyuncs.com/naibahq/nezha-dashboard >/dev/null 2>&1
clean_all
if [[ $# == 0 ]]; then
@@ -715,7 +733,7 @@ uninstall_dashboard() {
uninstall_dashboard_standalone() {
echo -e "> 卸载管理面板"
rm -rf $NZ_DASHBOARD_PATH
if [ "$os_alpine" != 1 ]; then
@@ -731,7 +749,7 @@ uninstall_dashboard_standalone() {
fi
clean_all
if [[ $# == 0 ]]; then
before_show_menu
fi
@@ -754,7 +772,7 @@ show_agent_log() {
uninstall_agent() {
echo -e "> 卸载Agent"
if [ "$os_alpine" != 1 ];then
if [ "$os_alpine" != 1 ]; then
systemctl disable nezha-agent.service
systemctl stop nezha-agent.service
rm -rf $NZ_AGENT_SERVICE
@@ -776,7 +794,7 @@ uninstall_agent() {
restart_agent() {
echo -e "> 重启Agent"
if [ "$os_alpine" != 1 ];then
if [ "$os_alpine" != 1 ]; then
systemctl restart nezha-agent.service
else
rc-service nezha-agent restart
@@ -797,14 +815,29 @@ select_version() {
DOCKER_COMPOSE_COMMAND=""
if docker compose version >/dev/null 2>&1; then
DOCKER_COMPOSE_COMMAND="docker compose"
if $DOCKER_COMPOSE_COMMAND ls | grep -qw "$NZ_DASHBOARD_PATH/docker-compose.yaml" >/dev/null 2>&1; then
NEZHA_IMAGES=$(docker images --format "{{.Repository}}:{{.Tag}}" | grep -w "nezha-dashboard")
if [ -n "$NEZHA_IMAGES" ]; then
echo "存在带有 nezha-dashboard 仓库的 Docker 镜像:"
echo "$NEZHA_IMAGES"
IS_DOCKER_NEZHA=1
return
else
echo "未找到带有 nezha-dashboard 仓库的 Docker 镜像。"
fi
fi
elif command -v docker-compose >/dev/null 2>&1; then
DOCKER_COMPOSE_COMMAND="docker-compose"
fi
if [[ -n $DOCKER_COMPOSE_COMMAND ]]; then
if $DOCKER_COMPOSE_COMMAND ls | grep -qw "$NZ_DASHBOARD_PATH/docker-compose.yaml" >/dev/null 2>&1; then
IS_DOCKER_NEZHA=1
return
if $DOCKER_COMPOSE_COMMAND -f "$NZ_DASHBOARD_PATH/docker-compose.yaml" config >/dev/null 2>&1; then
NEZHA_IMAGES=$(docker images --format "{{.Repository}}:{{.Tag}}" | grep -w "nezha-dashboard")
if [ -n "$NEZHA_IMAGES" ]; then
echo "存在带有 nezha-dashboard 仓库的 Docker 镜像:"
echo "$NEZHA_IMAGES"
IS_DOCKER_NEZHA=1
return
else
echo "未找到带有 nezha-dashboard 仓库的 Docker 镜像。"
fi
fi
fi
@@ -815,16 +848,16 @@ select_version() {
while true; do
read -e -r -p "请输入数字 [1-2]" option
case "${option}" in
1)
IS_DOCKER_NEZHA=1
break
1)
IS_DOCKER_NEZHA=1
break
;;
2)
IS_DOCKER_NEZHA=0
break
2)
IS_DOCKER_NEZHA=0
break
;;
*)
echo "输入有误,请重新输入"
*)
echo "输入有误,请重新输入"
;;
esac
done
@@ -877,98 +910,98 @@ show_menu() {
echo && read -ep "请输入选择 [0-13]: " num
if [[ $IS_DOCKER_NEZHA == 1 ]]; then
case "${num}" in
0)
exit 0
0)
exit 0
;;
1)
install_dashboard
1)
install_dashboard
;;
2)
modify_dashboard_config
2)
modify_dashboard_config
;;
3)
start_dashboard
3)
start_dashboard
;;
4)
stop_dashboard
4)
stop_dashboard
;;
5)
restart_and_update
5)
restart_and_update
;;
6)
show_dashboard_log
6)
show_dashboard_log
;;
7)
uninstall_dashboard
7)
uninstall_dashboard
;;
8)
install_agent
8)
install_agent
;;
9)
modify_agent_config
9)
modify_agent_config
;;
10)
show_agent_log
10)
show_agent_log
;;
11)
uninstall_agent
11)
uninstall_agent
;;
12)
restart_agent
12)
restart_agent
;;
13)
update_script
13)
update_script
;;
*)
echo -e "${red}请输入正确的数字 [0-13]${plain}"
*)
echo -e "${red}请输入正确的数字 [0-13]${plain}"
;;
esac
elif [[ $IS_DOCKER_NEZHA == 0 ]]; then
case "${num}" in
0)
exit 0
0)
exit 0
;;
1)
install_dashboard_standalone
1)
install_dashboard_standalone
;;
2)
modify_dashboard_config_standalone
2)
modify_dashboard_config_standalone
;;
3)
start_dashboard_standalone
3)
start_dashboard_standalone
;;
4)
stop_dashboard_standalone
4)
stop_dashboard_standalone
;;
5)
restart_and_update_standalone
5)
restart_and_update_standalone
;;
6)
show_dashboard_log_standalone
6)
show_dashboard_log_standalone
;;
7)
uninstall_dashboard_standalone
7)
uninstall_dashboard_standalone
;;
8)
install_agent
8)
install_agent
;;
9)
modify_agent_config
9)
modify_agent_config
;;
10)
show_agent_log
10)
show_agent_log
;;
11)
uninstall_agent
11)
uninstall_agent
;;
12)
restart_agent
12)
restart_agent
;;
13)
update_script
13)
update_script
;;
*)
echo -e "${red}请输入正确的数字 [0-13]${plain}"
*)
echo -e "${red}请输入正确的数字 [0-13]${plain}"
;;
esac
else
@@ -981,129 +1014,129 @@ pre_check
if [[ $# > 0 ]]; then
if [[ $IS_DOCKER_NEZHA == 1 ]]; then
case $1 in
"install_dashboard")
install_dashboard 0
"install_dashboard")
install_dashboard 0
;;
"modify_dashboard_config")
modify_dashboard_config 0
"modify_dashboard_config")
modify_dashboard_config 0
;;
"start_dashboard")
start_dashboard 0
"start_dashboard")
start_dashboard 0
;;
"stop_dashboard")
stop_dashboard 0
"stop_dashboard")
stop_dashboard 0
;;
"restart_and_update")
restart_and_update 0
"restart_and_update")
restart_and_update 0
;;
"show_dashboard_log")
show_dashboard_log 0
"show_dashboard_log")
show_dashboard_log 0
;;
"uninstall_dashboard")
uninstall_dashboard 0
"uninstall_dashboard")
uninstall_dashboard 0
;;
"install_agent")
shift
if [ $# -ge 3 ]; then
install_agent "$@"
else
install_agent 0
fi
"install_agent")
shift
if [ $# -ge 3 ]; then
install_agent "$@"
else
install_agent 0
fi
;;
"modify_agent_config")
modify_agent_config 0
"modify_agent_config")
modify_agent_config 0
;;
"show_agent_log")
show_agent_log 0
"show_agent_log")
show_agent_log 0
;;
"uninstall_agent")
uninstall_agent 0
"uninstall_agent")
uninstall_agent 0
;;
"restart_agent")
restart_agent 0
"restart_agent")
restart_agent 0
;;
"update_script")
update_script 0
"update_script")
update_script 0
;;
*) show_usage ;;
*) show_usage ;;
esac
elif [[ $IS_DOCKER_NEZHA == 0 ]]; then
case $1 in
"install_dashboard")
install_dashboard_standalone 0
"install_dashboard")
install_dashboard_standalone 0
;;
"modify_dashboard_config")
modify_dashboard_config_standalone 0
"modify_dashboard_config")
modify_dashboard_config_standalone 0
;;
"start_dashboard")
start_dashboard_standalone 0
"start_dashboard")
start_dashboard_standalone 0
;;
"stop_dashboard")
stop_dashboard_standalone 0
"stop_dashboard")
stop_dashboard_standalone 0
;;
"restart_and_update")
restart_and_update_standalone 0
"restart_and_update")
restart_and_update_standalone 0
;;
"show_dashboard_log")
show_dashboard_log_standalone 0
"show_dashboard_log")
show_dashboard_log_standalone 0
;;
"uninstall_dashboard")
uninstall_dashboard_standalone 0
"uninstall_dashboard")
uninstall_dashboard_standalone 0
;;
"install_agent")
shift
if [ $# -ge 3 ]; then
install_agent "$@"
else
install_agent 0
fi
"install_agent")
shift
if [ $# -ge 3 ]; then
install_agent "$@"
else
install_agent 0
fi
;;
"modify_agent_config")
modify_agent_config 0
"modify_agent_config")
modify_agent_config 0
;;
"show_agent_log")
show_agent_log 0
"show_agent_log")
show_agent_log 0
;;
"uninstall_agent")
uninstall_agent 0
"uninstall_agent")
uninstall_agent 0
;;
"restart_agent")
restart_agent 0
"restart_agent")
restart_agent 0
;;
"update_script")
update_script 0
"update_script")
update_script 0
;;
*) show_usage ;;
*) show_usage ;;
esac
else
case $1 in
"install_agent")
shift
if [ $# -ge 3 ]; then
install_agent "$@"
else
install_agent 0
fi
"install_agent")
shift
if [ $# -ge 3 ]; then
install_agent "$@"
else
install_agent 0
fi
;;
"modify_agent_config")
modify_agent_config 0
"modify_agent_config")
modify_agent_config 0
;;
"show_agent_log")
show_agent_log 0
"show_agent_log")
show_agent_log 0
;;
"uninstall_agent")
uninstall_agent 0
"uninstall_agent")
uninstall_agent 0
;;
"restart_agent")
restart_agent 0
"restart_agent")
restart_agent 0
;;
"update_script")
update_script 0
"update_script")
update_script 0
;;
*) show_usage ;;
*) show_usage ;;
esac
fi
else
select_version
show_menu
fi
fi
+308 -276
View File
@@ -14,7 +14,7 @@ NZ_AGENT_SERVICE="/etc/systemd/system/nezha-agent.service"
NZ_AGENT_SERVICERC="/etc/init.d/nezha-agent"
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
NZ_VERSION="v0.15.3"
NZ_VERSION="v0.15.8"
red='\033[0;31m'
green='\033[0;32m'
@@ -34,15 +34,15 @@ pre_check() {
## os_arch
if [[ $(uname -m | grep 'x86_64') != "" ]]; then
os_arch="amd64"
elif [[ $(uname -m | grep 'i386\|i686') != "" ]]; then
elif [[ $(uname -m | grep 'i386\|i686') != "" ]]; then
os_arch="386"
elif [[ $(uname -m | grep 'aarch64\|armv8b\|armv8l') != "" ]]; then
elif [[ $(uname -m | grep 'aarch64\|armv8b\|armv8l') != "" ]]; then
os_arch="arm64"
elif [[ $(uname -m | grep 'arm') != "" ]]; then
elif [[ $(uname -m | grep 'arm') != "" ]]; then
os_arch="arm"
elif [[ $(uname -m | grep 's390x') != "" ]]; then
elif [[ $(uname -m | grep 's390x') != "" ]]; then
os_arch="s390x"
elif [[ $(uname -m | grep 'riscv64') != "" ]]; then
elif [[ $(uname -m | grep 'riscv64') != "" ]]; then
os_arch="riscv64"
fi
@@ -50,37 +50,54 @@ pre_check() {
if [[ -z "${CN}" ]]; then
if [[ $(curl -m 10 -s https://ipapi.co/json | grep 'China') != "" ]]; then
echo "According to the information provided by ipapi.co, the current IP may be in China"
read -e -r -p "Is the installation done with a Chinese Mirror? [Y/n] " input
read -e -r -p "Is the installation done with a Chinese Mirror? [Y/n] (Custom Mirror Input 3):" input
case $input in
[yY][eE][sS] | [yY])
echo "Use Chinese Mirror"
CN=true
[yY][eE][sS] | [yY])
echo "Use Chinese Mirror"
CN=true
;;
[nN][oO] | [nN])
echo "No Use Chinese Mirror"
[nN][oO] | [nN])
echo "No Use Chinese Mirror"
;;
[3])
echo "Use Custom Mirror"
read -e -r -p "Please enter a custom image (e.g. :dn-dao-github-mirror.daocloud.io), leave blank to nouse: " input
case $input in
*)
echo "No Use Chinese Mirror"
CUSTOM_MIRROR=$input
;;
esac
;;
*)
echo "No Use Chinese Mirror"
;;
esac
fi
fi
if [[ -z "${CN}" ]]; then
GITHUB_RAW_URL="raw.githubusercontent.com/naiba/nezha/master"
GITHUB_URL="github.com"
Get_Docker_URL="get.docker.com"
Get_Docker_Argu=" "
Docker_IMG="ghcr.io\/naiba\/nezha-dashboard"
GITHUB_RELEASE_URL="github.com/naiba/nezha/releases/latest/download"
else
if [[ -n "${CUSTOM_MIRROR}" ]]; then
GITHUB_RAW_URL="gitee.com/naibahq/nezha/raw/master"
GITHUB_URL="dn-dao-github-mirror.daocloud.io"
GITHUB_URL=$CUSTOM_MIRROR
Get_Docker_URL="get.docker.com"
Get_Docker_Argu=" -s docker --mirror Aliyun"
Docker_IMG="registry.cn-shanghai.aliyuncs.com\/naibahq\/nezha-dashboard"
GITHUB_RELEASE_URL="hub.fgit.cf/naiba/nezha/releases/latest/download"
else
if [[ -z "${CN}" ]]; then
GITHUB_RAW_URL="raw.githubusercontent.com/naiba/nezha/master"
GITHUB_URL="github.com"
Get_Docker_URL="get.docker.com"
Get_Docker_Argu=" "
Docker_IMG="ghcr.io\/naiba\/nezha-dashboard"
else
GITHUB_RAW_URL="gitee.com/naibahq/nezha/raw/master"
GITHUB_URL="kkgithub.com"
Get_Docker_URL="get.docker.com"
Get_Docker_Argu=" -s docker --mirror Aliyun"
Docker_IMG="registry.cn-shanghai.aliyuncs.com\/naibahq\/nezha-dashboard"
fi
fi
}
@@ -126,39 +143,39 @@ before_show_menu() {
install_base() {
(command -v git >/dev/null 2>&1 && command -v curl >/dev/null 2>&1 && command -v wget >/dev/null 2>&1 && command -v unzip >/dev/null 2>&1 && command -v getenforce >/dev/null 2>&1) ||
(install_soft curl wget git unzip tzdata)
(install_soft curl wget git unzip tzdata)
}
install_arch(){
install_arch() {
echo -e "${green}Info: ${plain} Archlinux needs to add nezha-agent user to install libselinux. It will be deleted automatically after installation. It is recommended to check manually\n"
read -e -r -p "Do you need to install libselinux? [Y/n] " input
case $input in
[yY][eE][sS] | [yY])
useradd -m nezha-agent
sed -i "$ a\nezha-agent ALL=(ALL ) NOPASSWD:ALL" /etc/sudoers
sudo -iu nezha-agent bash -c 'gpg --keyserver keys.gnupg.net --recv-keys BE22091E3EF62275;
[yY][eE][sS] | [yY])
useradd -m nezha-agent
sed -i "$ a\nezha-agent ALL=(ALL ) NOPASSWD:ALL" /etc/sudoers
sudo -iu nezha-agent bash -c 'gpg --keyserver keys.gnupg.net --recv-keys BE22091E3EF62275;
cd /tmp; git clone https://aur.archlinux.org/libsepol.git; cd libsepol; makepkg -si --noconfirm --asdeps; cd ..;
git clone https://aur.archlinux.org/libselinux.git; cd libselinux; makepkg -si --noconfirm; cd ..;
rm -rf libsepol libselinux'
sed -i '/nezha-agent/d' /etc/sudoers && sleep 30s && killall -u nezha-agent&&userdel nezha-agent
echo -e "${red}Info: ${plain}user nezha-agent has been deleted, Be sure to check it manually!\n"
sed -i '/nezha-agent/d' /etc/sudoers && sleep 30s && killall -u nezha-agent && userdel -r nezha-agent
echo -e "${red}Info: ${plain}user nezha-agent has been deleted, Be sure to check it manually!\n"
;;
[nN][oO] | [nN])
echo "Libselinux will not be installed"
[nN][oO] | [nN])
echo "Libselinux will not be installed"
;;
*)
echo "Libselinux will not be installed"
exit 0
*)
echo "Libselinux will not be installed"
exit 0
;;
esac
}
install_soft() {
(command -v yum >/dev/null 2>&1 && yum makecache && yum install $* selinux-policy -y) ||
(command -v apt >/dev/null 2>&1 && apt update && apt install $* selinux-utils -y) ||
(command -v pacman >/dev/null 2>&1 && pacman -Syu $* base-devel --noconfirm && install_arch) ||
(command -v apt-get >/dev/null 2>&1 && apt-get update && apt-get install $* selinux-utils -y) ||
(command -v apk >/dev/null 2>&1 && apk update && apk add $* -f)
(command -v apt >/dev/null 2>&1 && apt update && apt install $* selinux-utils -y) ||
(command -v pacman >/dev/null 2>&1 && pacman -Syu $* base-devel --noconfirm && install_arch) ||
(command -v apt-get >/dev/null 2>&1 && apt-get update && apt-get install $* selinux-utils -y) ||
(command -v apk >/dev/null 2>&1 && apk update && apk add $* -f)
}
install_dashboard() {
@@ -173,16 +190,16 @@ install_dashboard() {
echo "You may have already installed the dashboard, repeated installation will overwrite the data, please pay attention to backup."
read -e -r -p "Exit the installation? [Y/n] " input
case $input in
[yY][eE][sS] | [yY])
echo "Exit the installation."
exit 0
[yY][eE][sS] | [yY])
echo "Exit the installation."
exit 0
;;
[nN][oO] | [nN])
echo "Continue."
[nN][oO] | [nN])
echo "Continue."
;;
*)
echo "Exit the installation."
exit 0
*)
echo "Exit the installation."
exit 0
;;
esac
fi
@@ -210,9 +227,9 @@ install_dashboard() {
install_dashboard_standalone() {
install_base
echo -e "> Install Panel"
# Nezha Monitoring Folder
if [ ! -d "${NZ_DASHBOARD_PATH}/app" ]; then
mkdir -p $NZ_DASHBOARD_PATH
@@ -220,16 +237,16 @@ install_dashboard_standalone() {
echo "You may have already installed the dashboard, repeated installation will overwrite the data, please pay attention to backup."
read -e -r -p "Exit the installation? [Y/n] " input
case $input in
[yY][eE][sS] | [yY])
echo "Exit the installation."
exit 0
[yY][eE][sS] | [yY])
echo "Exit the installation."
exit 0
;;
[nN][oO] | [nN])
echo "Continue."
[nN][oO] | [nN])
echo "Continue."
;;
*)
echo "Exit the installation."
exit 0
*)
echo "Exit the installation."
exit 0
;;
esac
fi
@@ -239,19 +256,20 @@ install_dashboard_standalone() {
fi
chmod 777 -R $NZ_DASHBOARD_PATH
modify_dashboard_config_standalone 0
if [[ $# == 0 ]]; then
before_show_menu
fi
}
selinux(){
selinux() {
#Check SELinux
if [ "$os_alpine" != 1 ];then
command -v getenforce >/dev/null 2>&1
if [ $? -eq 0 ]; then
getenforce | grep '[Ee]nfor'
if [ $? -eq 0 ];then
if [ $? -eq 0 ]; then
echo -e "SELinux runningclosing now"
setenforce 0 &>/dev/null
find_key="SELINUX="
@@ -295,8 +313,8 @@ install_agent() {
fi
unzip -qo nezha-agent_linux_${os_arch}.zip &&
mv nezha-agent $NZ_AGENT_PATH &&
rm -rf nezha-agent_linux_${os_arch}.zip README.md
mv nezha-agent $NZ_AGENT_PATH &&
rm -rf nezha-agent_linux_${os_arch}.zip README.md
if [ $# -ge 3 ]; then
modify_agent_config "$@"
@@ -312,7 +330,7 @@ install_agent() {
modify_agent_config() {
echo -e "> Modify Agent Configuration"
if [ "$os_alpine" != 1 ];then
if [ "$os_alpine" != 1 ]; then
wget -t 2 -T 10 -O $NZ_AGENT_SERVICE https://${GITHUB_RAW_URL}/script/nezha-agent.service >/dev/null 2>&1
if [[ $? != 0 ]]; then
echo -e "${red}Fail to download service, please check if the network can link ${GITHUB_RAW_URL}${plain}"
@@ -329,11 +347,11 @@ modify_agent_config() {
if [ $# -lt 3 ]; then
echo "Please add Agent in the admin panel first, record the secret" &&
read -ep "Please enter a domain that resolves to the IP where the panel is located (no CDN sets): " nz_grpc_host &&
read -ep "Please enter the panel RPC port (default 5555): " nz_grpc_port &&
read -ep "Please enter the Agent secret: " nz_client_secret &&
read -ep "Do you want to enable SSL/TLS encryption for the gRPC port (--tls)? Press [y] if yes, the default is not required, and users can press Enter to skip if you don't understand: " nz_grpc_proxy
grep -qiw 'Y' <<< "${nz_grpc_proxy}" && args='--tls'
read -ep "Please enter a domain that resolves to the IP where the panel is located (no CDN sets): " nz_grpc_host &&
read -ep "Please enter the panel RPC port (default 5555): " nz_grpc_port &&
read -ep "Please enter the Agent secret: " nz_client_secret &&
read -ep "Do you want to enable SSL/TLS encryption for the gRPC port (--tls)? Press [y] if yes, the default is not required, and users can press Enter to skip if you don't understand: " nz_grpc_proxy
grep -qiw 'Y' <<<"${nz_grpc_proxy}" && args='--tls'
if [[ -z "${nz_grpc_host}" || -z "${nz_client_secret}" ]]; then
echo -e "${red}All options cannot be empty${plain}"
before_show_menu
@@ -352,7 +370,7 @@ modify_agent_config() {
fi
fi
if [ "$os_alpine" != 1 ];then
if [ "$os_alpine" != 1 ]; then
sed -i "s/nz_grpc_host/${nz_grpc_host}/" ${NZ_AGENT_SERVICE}
sed -i "s/nz_grpc_port/${nz_grpc_port}/" ${NZ_AGENT_SERVICE}
sed -i "s/nz_client_secret/${nz_client_secret}/" ${NZ_AGENT_SERVICE}
@@ -366,7 +384,7 @@ modify_agent_config() {
echo -e "Agent configuration ${green} modified successfully, please wait for agent self-restart to take effect${plain}"
if [ "$os_alpine" != 1 ];then
if [ "$os_alpine" != 1 ]; then
systemctl daemon-reload
systemctl enable nezha-agent
systemctl restart nezha-agent
@@ -397,14 +415,14 @@ modify_dashboard_config() {
fi
echo "About the GitHub Oauth2 application: create it at https://github.com/settings/developers, no review required, and fill in the http(s)://domain_or_IP/oauth2/callback" &&
echo "(Not recommended) About the Gitee Oauth2 application: create it at https://gitee.com/oauth/applications, no auditing required, and fill in the http(s)://domain_or_IP/oauth2/callback" &&
read -ep "Please enter the OAuth2 provider (github/gitlab/jihulab/gitee, default github): " nz_oauth2_type &&
read -ep "Please enter the Client ID of the Oauth2 application: " nz_github_oauth_client_id &&
read -ep "Please enter the Client Secret of the Oauth2 application: " nz_github_oauth_client_secret &&
read -ep "Please enter your GitHub/Gitee login name as the administrator, separated by commas: " nz_admin_logins &&
read -ep "Please enter the site title: " nz_site_title &&
read -ep "Please enter the site access port: (default 8008)" nz_site_port &&
read -ep "Please enter the RPC port to be used for Agent access: (default 5555)" nz_grpc_port
echo "(Not recommended) About the Gitee Oauth2 application: create it at https://gitee.com/oauth/applications, no auditing required, and fill in the http(s)://domain_or_IP/oauth2/callback" &&
read -ep "Please enter the OAuth2 provider (github/gitlab/jihulab/gitee, default github): " nz_oauth2_type &&
read -ep "Please enter the Client ID of the Oauth2 application: " nz_github_oauth_client_id &&
read -ep "Please enter the Client Secret of the Oauth2 application: " nz_github_oauth_client_secret &&
read -ep "Please enter your GitHub/Gitee login name as the administrator, separated by commas: " nz_admin_logins &&
read -ep "Please enter the site title: " nz_site_title &&
read -ep "Please enter the site access port: (default 8008)" nz_site_port &&
read -ep "Please enter the RPC port to be used for Agent access: (default 5555)" nz_grpc_port
if [[ -z "${nz_admin_logins}" || -z "${nz_github_oauth_client_id}" || -z "${nz_github_oauth_client_secret}" || -z "${nz_site_title}" ]]; then
echo -e "${red}All options cannot be empty${plain}"
@@ -448,31 +466,31 @@ modify_dashboard_config() {
modify_dashboard_config_standalone() {
echo -e "> Modify Panel Configuration"
echo -e "Download configuration template"
wget -t 2 -T 10 -O /tmp/nezha-config.yaml https://${GITHUB_RAW_URL}/script/config.yaml >/dev/null 2>&1
if [[ $? != 0 ]]; then
echo -e "${red}File failed to get, please check if the network can link ${GITHUB_RAW_URL}${plain}"
return 0
fi
echo "About the GitHub Oauth2 application: create it at https://github.com/settings/developers, no review required, and fill in the http(s)://domain_or_IP/oauth2/callback" &&
echo "(Not recommended) About the Gitee Oauth2 application: create it at https://gitee.com/oauth/applications, no auditing required, and fill in the http(s)://domain_or_IP/oauth2/callback" &&
read -ep "Please enter the OAuth2 provider (github/gitlab/jihulab/gitee, default github): " nz_oauth2_type &&
read -ep "Please enter the Client ID of the Oauth2 application: " nz_github_oauth_client_id &&
read -ep "Please enter the Client Secret of the Oauth2 application: " nz_github_oauth_client_secret &&
read -ep "Please enter your GitHub/Gitee login name as the administrator, separated by commas: " nz_admin_logins &&
read -ep "Please enter the site title: " nz_site_title &&
read -ep "Please enter the site access port: (default 8008)" nz_site_port &&
read -ep "Please enter the RPC port to be used for Agent access: (default 5555)" nz_grpc_port
echo "(Not recommended) About the Gitee Oauth2 application: create it at https://gitee.com/oauth/applications, no auditing required, and fill in the http(s)://domain_or_IP/oauth2/callback" &&
read -ep "Please enter the OAuth2 provider (github/gitlab/jihulab/gitee, default github): " nz_oauth2_type &&
read -ep "Please enter the Client ID of the Oauth2 application: " nz_github_oauth_client_id &&
read -ep "Please enter the Client Secret of the Oauth2 application: " nz_github_oauth_client_secret &&
read -ep "Please enter your GitHub/Gitee login name as the administrator, separated by commas: " nz_admin_logins &&
read -ep "Please enter the site title: " nz_site_title &&
read -ep "Please enter the site access port: (default 8008)" nz_site_port &&
read -ep "Please enter the RPC port to be used for Agent access: (default 5555)" nz_grpc_port
if [[ -z "${nz_admin_logins}" || -z "${nz_github_oauth_client_id}" || -z "${nz_github_oauth_client_secret}" || -z "${nz_site_title}" ]]; then
echo -e "${red}All options cannot be empty${plain}"
before_show_menu
return 1
fi
if [[ -z "${nz_site_port}" ]]; then
nz_site_port=8008
fi
@@ -482,7 +500,7 @@ modify_dashboard_config_standalone() {
if [[ -z "${nz_oauth2_type}" ]]; then
nz_oauth2_type=github
fi
sed -i "s/nz_oauth2_type/${nz_oauth2_type}/" /tmp/nezha-config.yaml
sed -i "s/nz_admin_logins/${nz_admin_logins}/" /tmp/nezha-config.yaml
sed -i "s/nz_grpc_port/${nz_grpc_port}/" /tmp/nezha-config.yaml
@@ -491,15 +509,15 @@ modify_dashboard_config_standalone() {
sed -i "s/nz_language/zh-CN/" /tmp/nezha-config.yaml
sed -i "s/nz_site_title/${nz_site_title}/" /tmp/nezha-config.yaml
sed -i "s/80/${nz_site_port}/" /tmp/nezha-config.yaml
mkdir -p $NZ_DASHBOARD_PATH/data
mv -f /tmp/nezha-config.yaml ${NZ_DASHBOARD_PATH}/data/config.yaml
echo -e "Downloading service file"
if [ "$os_alpine" != 1 ];then
if [ "$os_alpine" != 1 ]; then
wget -t 2 -T 10 -O $NZ_DASHBOARD_SERVICE https://${GITHUB_RAW_URL}/script/nezha-dashboard.service >/dev/null 2>&1
else
else
wget -t 2 -T 10 -O $NZ_DASHBOARD_SERVICERC https://${GITHUB_RAW_URL}/script/nezha-dashboard >/dev/null 2>&1
chmod +x $NZ_DASHBOARD_SERVICERC
if [[ $? != 0 ]]; then
@@ -508,11 +526,10 @@ modify_dashboard_config_standalone() {
fi
fi
echo -e "Dashboard configuration ${green} modified successfully, please wait for Dashboard self-restart to take effect${plain}"
restart_and_update_standalone
if [[ $# == 0 ]]; then
before_show_menu
fi
@@ -548,7 +565,7 @@ restart_and_update() {
restart_and_update_standalone() {
echo -e "> Restart and Update the Panel"
cd $NZ_DASHBOARD_PATH
if [ "$os_alpine" != 1 ]; then
@@ -557,9 +574,9 @@ restart_and_update_standalone() {
rc-service nezha-dashboard stop
fi
wget -qO app.zip $GITHUB_RELEASE_URL/dashboard-linux-$os_arch.zip >/dev/null 2>&1 && unzip -qq app.zip && mv dist/dashboard-linux-$os_arch app && rm -r app.zip dist
wget -qO app.zip https://${GITHUB_URL}/naiba/nezha/releases/latest/download/dashboard-linux-$os_arch.zip >/dev/null 2>&1 && unzip -qq app.zip && mv dist/dashboard-linux-$os_arch app && rm -r app.zip dist
if [ "$os_alpine" != 1 ];then
if [ "$os_alpine" != 1 ]; then
systemctl daemon-reload
systemctl enable nezha-dashboard
systemctl restart nezha-dashboard
@@ -567,14 +584,14 @@ restart_and_update_standalone() {
rc-update add nezha-dashboard
rc-service nezha-dashboard restart
fi
if [[ $? == 0 ]]; then
echo -e "${green}Nezha Monitoring Restart Successful${plain}"
echo -e "Default panel address: ${yellow}domain:Site_access_port${plain}"
else
echo -e "${red}The restart failed, probably because the boot time exceeded two seconds, please check the log information later${plain}"
fi
if [[ $# == 0 ]]; then
before_show_menu
fi
@@ -603,19 +620,19 @@ start_dashboard() {
start_dashboard_standalone() {
echo -e "> Start Panel"
if [ "$os_alpine" != 1 ]; then
systemctl start nezha-dashboard
else
rc-service nezha-dashboard start
fi
if [[ $? == 0 ]]; then
echo -e "${green}Nezha Monitoring Start Successful${plain}"
else
echo -e "${red}Failed to start, please check the log message later${plain}"
fi
if [[ $# == 0 ]]; then
before_show_menu
fi
@@ -644,19 +661,19 @@ stop_dashboard() {
stop_dashboard_standalone() {
echo -e "> Stop Panel"
if [ "$os_alpine" != 1 ]; then
systemctl stop nezha-dashboard
else
rc-service nezha-dashboard stop
fi
if [[ $? == 0 ]]; then
echo -e "${green}Nezha Monitoring Stop Successful${plain}"
else
echo -e "${red}Failed to stop, please check the log message later${plain}"
fi
if [[ $# == 0 ]]; then
before_show_menu
fi
@@ -679,7 +696,7 @@ show_dashboard_log() {
show_dashboard_log_standalone() {
echo -e "> View Panel Log"
if [ "$os_alpine" != 1 ]; then
journalctl -xf -u nezha-dashboard.service
else
@@ -702,8 +719,8 @@ uninstall_dashboard() {
fi
rm -rf $NZ_DASHBOARD_PATH
docker rmi -f ghcr.io/naiba/nezha-dashboard > /dev/null 2>&1
docker rmi -f registry.cn-shanghai.aliyuncs.com/naibahq/nezha-dashboard > /dev/null 2>&1
docker rmi -f ghcr.io/naiba/nezha-dashboard >/dev/null 2>&1
docker rmi -f registry.cn-shanghai.aliyuncs.com/naibahq/nezha-dashboard >/dev/null 2>&1
clean_all
if [[ $# == 0 ]]; then
@@ -713,7 +730,7 @@ uninstall_dashboard() {
uninstall_dashboard_standalone() {
echo -e "> Uninstall Panel"
rm -rf $NZ_DASHBOARD_PATH
if [ "$os_alpine" != 1 ]; then
@@ -729,7 +746,7 @@ uninstall_dashboard_standalone() {
fi
clean_all
if [[ $# == 0 ]]; then
before_show_menu
fi
@@ -752,7 +769,7 @@ show_agent_log() {
uninstall_agent() {
echo -e "> Uninstall Agent"
if [ "$os_alpine" != 1 ];then
if [ "$os_alpine" != 1 ]; then
systemctl disable nezha-agent.service
systemctl stop nezha-agent.service
rm -rf $NZ_AGENT_SERVICE
@@ -774,7 +791,7 @@ uninstall_agent() {
restart_agent() {
echo -e "> Restart Agent"
if [ "$os_alpine" != 1 ];then
if [ "$os_alpine" != 1 ]; then
systemctl restart nezha-agent.service
else
rc-service nezha-agent restart
@@ -795,14 +812,29 @@ select_version() {
DOCKER_COMPOSE_COMMAND=""
if docker compose version >/dev/null 2>&1; then
DOCKER_COMPOSE_COMMAND="docker compose"
if $DOCKER_COMPOSE_COMMAND ls | grep -qw "$NZ_DASHBOARD_PATH/docker-compose.yaml" >/dev/null 2>&1; then
NEZHA_IMAGES=$(docker images --format "{{.Repository}}:{{.Tag}}" | grep -w "nezha-dashboard")
if [ -n "$NEZHA_IMAGES" ]; then
echo "Docker image with nezha-dashboard repository exists:"
echo "$NEZHA_IMAGES"
IS_DOCKER_NEZHA=1
return
else
echo "No Docker images with the nezha-dashboard repository were found."
fi
fi
elif command -v docker-compose >/dev/null 2>&1; then
DOCKER_COMPOSE_COMMAND="docker-compose"
fi
if [[ -n $DOCKER_COMPOSE_COMMAND ]]; then
if $DOCKER_COMPOSE_COMMAND ls | grep -qw "$NZ_DASHBOARD_PATH/docker-compose.yaml" >/dev/null 2>&1; then
IS_DOCKER_NEZHA=1
return
if $DOCKER_COMPOSE_COMMAND -f "$NZ_DASHBOARD_PATH/docker-compose.yaml" config >/dev/null 2>&1; then
NEZHA_IMAGES=$(docker images --format "{{.Repository}}:{{.Tag}}" | grep -w "nezha-dashboard")
if [ -n "$NEZHA_IMAGES" ]; then
echo "Docker image with nezha-dashboard repository exists:"
echo "$NEZHA_IMAGES"
IS_DOCKER_NEZHA=1
return
else
echo "No Docker images with the nezha-dashboard repository were found."
fi
fi
fi
@@ -813,16 +845,16 @@ select_version() {
while true; do
read -e -r -p "Input a number [1-2]: " option
case "${option}" in
1)
IS_DOCKER_NEZHA=1
break
1)
IS_DOCKER_NEZHA=1
break
;;
2)
IS_DOCKER_NEZHA=0
break
2)
IS_DOCKER_NEZHA=0
break
;;
*)
echo "Wrong input, try again"
*)
echo "Wrong input, try again"
;;
esac
done
@@ -875,98 +907,98 @@ show_menu() {
echo && read -ep "Please enter [0-13]: " num
if [[ $IS_DOCKER_NEZHA == 1 ]]; then
case "${num}" in
0)
exit 0
0)
exit 0
;;
1)
install_dashboard
1)
install_dashboard
;;
2)
modify_dashboard_config
2)
modify_dashboard_config
;;
3)
start_dashboard
3)
start_dashboard
;;
4)
stop_dashboard
4)
stop_dashboard
;;
5)
restart_and_update
5)
restart_and_update
;;
6)
show_dashboard_log
6)
show_dashboard_log
;;
7)
uninstall_dashboard
7)
uninstall_dashboard
;;
8)
install_agent
8)
install_agent
;;
9)
modify_agent_config
9)
modify_agent_config
;;
10)
show_agent_log
10)
show_agent_log
;;
11)
uninstall_agent
11)
uninstall_agent
;;
12)
restart_agent
12)
restart_agent
;;
13)
update_script
13)
update_script
;;
*)
echo -e "${red}Please enter the correct number [0-13]${plain}"
*)
echo -e "${red}Please enter the correct number [0-13]${plain}"
;;
esac
elif [[ $IS_DOCKER_NEZHA == 0 ]]; then
case "${num}" in
0)
exit 0
0)
exit 0
;;
1)
install_dashboard_standalone
1)
install_dashboard_standalone
;;
2)
modify_dashboard_config_standalone
2)
modify_dashboard_config_standalone
;;
3)
start_dashboard_standalone
3)
start_dashboard_standalone
;;
4)
stop_dashboard_standalone
4)
stop_dashboard_standalone
;;
5)
restart_and_update_standalone
5)
restart_and_update_standalone
;;
6)
show_dashboard_log_standalone
6)
show_dashboard_log_standalone
;;
7)
uninstall_dashboard_standalone
7)
uninstall_dashboard_standalone
;;
8)
install_agent
8)
install_agent
;;
9)
modify_agent_config
9)
modify_agent_config
;;
10)
show_agent_log
10)
show_agent_log
;;
11)
uninstall_agent
11)
uninstall_agent
;;
12)
restart_agent
12)
restart_agent
;;
13)
update_script
13)
update_script
;;
*)
echo -e "${red}Please enter the correct number [0-13]${plain}"
*)
echo -e "${red}Please enter the correct number [0-13]${plain}"
;;
esac
fi
@@ -977,129 +1009,129 @@ pre_check
if [[ $# > 0 ]]; then
if [[ $IS_DOCKER_NEZHA == 1 ]]; then
case $1 in
"install_dashboard")
install_dashboard 0
"install_dashboard")
install_dashboard 0
;;
"modify_dashboard_config")
modify_dashboard_config 0
"modify_dashboard_config")
modify_dashboard_config 0
;;
"start_dashboard")
start_dashboard 0
"start_dashboard")
start_dashboard 0
;;
"stop_dashboard")
stop_dashboard 0
"stop_dashboard")
stop_dashboard 0
;;
"restart_and_update")
restart_and_update 0
"restart_and_update")
restart_and_update 0
;;
"show_dashboard_log")
show_dashboard_log 0
"show_dashboard_log")
show_dashboard_log 0
;;
"uninstall_dashboard")
uninstall_dashboard 0
"uninstall_dashboard")
uninstall_dashboard 0
;;
"install_agent")
shift
if [ $# -ge 3 ]; then
install_agent "$@"
else
install_agent 0
fi
"install_agent")
shift
if [ $# -ge 3 ]; then
install_agent "$@"
else
install_agent 0
fi
;;
"modify_agent_config")
modify_agent_config 0
"modify_agent_config")
modify_agent_config 0
;;
"show_agent_log")
show_agent_log 0
"show_agent_log")
show_agent_log 0
;;
"uninstall_agent")
uninstall_agent 0
"uninstall_agent")
uninstall_agent 0
;;
"restart_agent")
restart_agent 0
"restart_agent")
restart_agent 0
;;
"update_script")
update_script 0
"update_script")
update_script 0
;;
*) show_usage ;;
*) show_usage ;;
esac
elif [[ $IS_DOCKER_NEZHA == 0 ]]; then
case $1 in
"install_dashboard")
install_dashboard_standalone 0
"install_dashboard")
install_dashboard_standalone 0
;;
"modify_dashboard_config")
modify_dashboard_config_standalone 0
"modify_dashboard_config")
modify_dashboard_config_standalone 0
;;
"start_dashboard")
start_dashboard_standalone 0
"start_dashboard")
start_dashboard_standalone 0
;;
"stop_dashboard")
stop_dashboard_standalone 0
"stop_dashboard")
stop_dashboard_standalone 0
;;
"restart_and_update")
restart_and_update_standalone 0
"restart_and_update")
restart_and_update_standalone 0
;;
"show_dashboard_log")
show_dashboard_log_standalone 0
"show_dashboard_log")
show_dashboard_log_standalone 0
;;
"uninstall_dashboard")
uninstall_dashboard_standalone 0
"uninstall_dashboard")
uninstall_dashboard_standalone 0
;;
"install_agent")
shift
if [ $# -ge 3 ]; then
install_agent "$@"
else
install_agent 0
fi
"install_agent")
shift
if [ $# -ge 3 ]; then
install_agent "$@"
else
install_agent 0
fi
;;
"modify_agent_config")
modify_agent_config 0
"modify_agent_config")
modify_agent_config 0
;;
"show_agent_log")
show_agent_log 0
"show_agent_log")
show_agent_log 0
;;
"uninstall_agent")
uninstall_agent 0
"uninstall_agent")
uninstall_agent 0
;;
"restart_agent")
restart_agent 0
"restart_agent")
restart_agent 0
;;
"update_script")
update_script 0
"update_script")
update_script 0
;;
*) show_usage ;;
*) show_usage ;;
esac
else
case $1 in
"install_agent")
shift
if [ $# -ge 3 ]; then
install_agent "$@"
else
install_agent 0
fi
"install_agent")
shift
if [ $# -ge 3 ]; then
install_agent "$@"
else
install_agent 0
fi
;;
"modify_agent_config")
modify_agent_config 0
"modify_agent_config")
modify_agent_config 0
;;
"show_agent_log")
show_agent_log 0
"show_agent_log")
show_agent_log 0
;;
"uninstall_agent")
uninstall_agent 0
"uninstall_agent")
uninstall_agent 0
;;
"restart_agent")
restart_agent 0
"restart_agent")
restart_agent 0
;;
"update_script")
update_script 0
"update_script")
update_script 0
;;
*) show_usage ;;
*) show_usage ;;
esac
fi
else
select_version
show_menu
fi
fi
+4 -1
View File
@@ -1,4 +1,7 @@
#!/sbin/openrc-run
supervisor=supervise-daemon
name=nezha-agent
supervise_daemon_args="--stdout /var/log/${name}.log --stderr /var/log/${name}.err"
SERVER="nz_grpc_host:nz_grpc_port" #Dashboard 地址 ip:port
SECRET="nz_client_secret" #SECRET
NZ_BASE_PATH="/opt/nezha"
@@ -56,4 +59,4 @@ start_pre() {
if [ "${RC_CMD}" != "restart" ]; then
checkconfig || return $?
fi
}
}
+32
View File
@@ -3,6 +3,8 @@ package rpc
import (
"context"
"fmt"
"github.com/naiba/nezha/pkg/utils"
"log"
"time"
"github.com/jinzhu/copier"
@@ -110,6 +112,36 @@ func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Rece
host := model.PB2Host(r)
singleton.ServerLock.RLock()
defer singleton.ServerLock.RUnlock()
// 检查并更新DDNS
if singleton.Conf.DDNS.Enable &&
singleton.ServerList[clientID].EnableDDNS &&
singleton.ServerList[clientID].Host != nil &&
host.IP != "" &&
singleton.ServerList[clientID].Host.IP != host.IP {
serverDomain := singleton.ServerList[clientID].DDNSDomain
provider, err := singleton.GetDDNSProviderFromString(singleton.Conf.DDNS.Provider)
if err == nil && serverDomain != "" {
ipv4, ipv6, _ := utils.SplitIPAddr(host.IP)
maxRetries := int(singleton.Conf.DDNS.MaxRetries)
config := &singleton.DDNSDomainConfig{
EnableIPv4: true,
EnableIpv6: true,
FullDomain: serverDomain,
Ipv4Addr: ipv4,
Ipv6Addr: ipv6,
}
go singleton.RetryableUpdateDomain(provider, config, maxRetries)
} else {
// 虽然会在启动时panic, 可以断言不会走这个分支, 但是考虑到动态加载配置或者其它情况, 这里输出一下方便检查奇奇怪怪的BUG
log.Printf("NEZHA>> 未找到对应的DDNS提供者(%s), 请前往config.yml检查你的设置\n", singleton.Conf.DDNS.Provider)
}
}
// 发送IP变动通知
if singleton.Conf.EnableIPChangeNotification &&
((singleton.Conf.Cover == model.ConfigCoverAll && !singleton.Conf.IgnoredIPNotificationServerIDs[clientID]) ||
(singleton.Conf.Cover == model.ConfigCoverIgnoreAll && singleton.Conf.IgnoredIPNotificationServerIDs[clientID])) &&
+66 -2
View File
@@ -1,9 +1,11 @@
package singleton
import (
"sync"
"time"
"github.com/naiba/nezha/model"
"github.com/naiba/nezha/pkg/utils"
"sync"
)
var (
@@ -11,7 +13,10 @@ var (
UserIDToApiTokenList = make(map[uint64][]string)
ApiLock sync.RWMutex
ServerAPI = &ServerAPIService{}
ServerAPI = &ServerAPIService{}
MonitorAPI = &MonitorAPIService{}
once = &sync.Once{}
)
type ServerAPIService struct{}
@@ -51,6 +56,23 @@ type ServerInfoResponse struct {
Result []*CommonServerInfo `json:"result"`
}
type MonitorAPIService struct {
}
type MonitorInfoResponse struct {
CommonResponse
Result []*MonitorInfo `json:"result"`
}
type MonitorInfo struct {
MonitorID uint64 `json:"monitor_id"`
ServerID uint64 `json:"server_id"`
MonitorName string `json:"monitor_name"`
ServerName string `json:"server_name"`
CreatedAt []int64 `json:"created_at"`
AvgDelay []float32 `json:"avg_delay"`
}
func InitAPI() {
ApiTokenList = make(map[string]*model.ApiToken)
UserIDToApiTokenList = make(map[uint64][]string)
@@ -203,3 +225,45 @@ func (s *ServerAPIService) GetAllList() *ServerInfoResponse {
}
return res
}
func (m *MonitorAPIService) GetMonitorHistories(query map[string]any) *MonitorInfoResponse {
var (
resultMap = make(map[uint64]*MonitorInfo)
monitorHistories []*model.MonitorHistory
sortedMonitorIDs []uint64
)
res := &MonitorInfoResponse{
CommonResponse: CommonResponse{
Code: 0,
Message: "success",
},
}
if err := DB.Model(&model.MonitorHistory{}).Select("monitor_id, created_at, server_id, avg_delay").
Where(query).Where("created_at >= ?", time.Now().Add(-24*time.Hour)).Order("monitor_id, created_at").
Scan(&monitorHistories).Error; err != nil {
res.CommonResponse = CommonResponse{
Code: 500,
Message: err.Error(),
}
} else {
for _, history := range monitorHistories {
infos, ok := resultMap[history.MonitorID]
if !ok {
infos = &MonitorInfo{
MonitorID: history.MonitorID,
ServerID: history.ServerID,
MonitorName: ServiceSentinelShared.monitors[history.MonitorID].Name,
ServerName: ServerList[history.ServerID].Name,
}
resultMap[history.MonitorID] = infos
sortedMonitorIDs = append(sortedMonitorIDs, history.MonitorID)
}
infos.CreatedAt = append(infos.CreatedAt, history.CreatedAt.Truncate(time.Minute).Unix()*1000)
infos.AvgDelay = append(infos.AvgDelay, history.AvgDelay)
}
for _, monitorID := range sortedMonitorIDs {
res.Result = append(res.Result, resultMap[monitorID])
}
}
return res
}
+302
View File
@@ -0,0 +1,302 @@
package singleton
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"strings"
)
type DDNSDomainConfig struct {
EnableIPv4 bool
EnableIpv6 bool
FullDomain string
Ipv4Addr string
Ipv6Addr string
}
type DDNSProvider interface {
// UpdateDomain Return is updated
UpdateDomain(domainConfig *DDNSDomainConfig) bool
}
type DDNSProviderWebHook struct {
URL string
RequestMethod string
RequestBody string
RequestHeader string
}
func (provider DDNSProviderWebHook) UpdateDomain(domainConfig *DDNSDomainConfig) bool {
if domainConfig == nil {
return false
}
if domainConfig.FullDomain == "" {
log.Println("NEZHA>> Failed to update an empty domain")
return false
}
updated := false
client := &http.Client{}
if domainConfig.EnableIPv4 && domainConfig.Ipv4Addr != "" {
url := provider.FormatWebhookString(provider.URL, domainConfig, "ipv4")
body := provider.FormatWebhookString(provider.RequestBody, domainConfig, "ipv4")
header := provider.FormatWebhookString(provider.RequestHeader, domainConfig, "ipv4")
headers := strings.Split(header, "\n")
req, err := http.NewRequest(provider.RequestMethod, url, bytes.NewBufferString(body))
if err == nil && req != nil {
SetStringHeadersToRequest(req, headers)
if _, err := client.Do(req); err != nil {
log.Printf("NEZHA>> Failed to update a domain: %s. Cause by: %s\n", domainConfig.FullDomain, err.Error())
}
updated = true
}
}
if domainConfig.EnableIpv6 && domainConfig.Ipv6Addr != "" {
url := provider.FormatWebhookString(provider.URL, domainConfig, "ipv6")
body := provider.FormatWebhookString(provider.RequestBody, domainConfig, "ipv6")
header := provider.FormatWebhookString(provider.RequestHeader, domainConfig, "ipv6")
headers := strings.Split(header, "\n")
req, err := http.NewRequest(provider.RequestMethod, url, bytes.NewBufferString(body))
if err == nil && req != nil {
SetStringHeadersToRequest(req, headers)
if _, err := client.Do(req); err != nil {
log.Printf("NEZHA>> Failed to update a domain: %s. Cause by: %s\n", domainConfig.FullDomain, err.Error())
}
updated = true
}
}
return updated
}
type DDNSProviderDummy struct{}
func (provider DDNSProviderDummy) UpdateDomain(domainConfig *DDNSDomainConfig) bool {
return false
}
type DDNSProviderCloudflare struct {
Secret string
}
func (provider DDNSProviderCloudflare) UpdateDomain(domainConfig *DDNSDomainConfig) bool {
if domainConfig == nil {
return false
}
zoneID, err := provider.getZoneID(domainConfig.FullDomain)
if err != nil {
log.Printf("无法获取 zone ID: %s\n", err)
return false
}
record, err := provider.findDNSRecord(zoneID, domainConfig.FullDomain)
if err != nil {
log.Printf("查找 DNS 记录时出错: %s\n", err)
return false
}
if record == nil {
// 添加 DNS 记录
return provider.createDNSRecord(zoneID, domainConfig)
} else {
// 更新 DNS 记录
return provider.updateDNSRecord(zoneID, record["id"].(string), domainConfig)
}
}
func (provider DDNSProviderCloudflare) getZoneID(domain string) (string, error) {
_, realDomain := SplitDomain(domain)
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones?name=%s", realDomain)
body, err := provider.sendRequest("GET", url, nil)
if err != nil {
return "", err
}
var res map[string]interface{}
err = json.Unmarshal(body, &res)
if err != nil {
return "", err
}
result := res["result"].([]interface{})
if len(result) > 0 {
zoneID := result[0].(map[string]interface{})["id"].(string)
return zoneID, nil
}
return "", fmt.Errorf("找不到 Zone ID")
}
func (provider DDNSProviderCloudflare) findDNSRecord(zoneID string, domain string) (map[string]interface{}, error) {
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records?type=A&name=%s", zoneID, domain)
body, err := provider.sendRequest("GET", url, nil)
if err != nil {
return nil, err
}
var res map[string]interface{}
err = json.Unmarshal(body, &res)
if err != nil {
return nil, err
}
result := res["result"].([]interface{})
if len(result) > 0 {
return result[0].(map[string]interface{}), nil
}
return nil, nil // 没有找到 DNS 记录
}
func (provider DDNSProviderCloudflare) createDNSRecord(zoneID string, domainConfig *DDNSDomainConfig) bool {
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records", zoneID)
data := map[string]interface{}{
"type": "A",
"name": domainConfig.FullDomain,
"content": domainConfig.Ipv4Addr,
"ttl": 3600,
"proxied": false,
}
jsonData, _ := json.Marshal(data)
_, err := provider.sendRequest("POST", url, jsonData)
return err == nil
}
func (provider DDNSProviderCloudflare) updateDNSRecord(zoneID string, recordID string, domainConfig *DDNSDomainConfig) bool {
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records/%s", zoneID, recordID)
data := map[string]interface{}{
"type": "A",
"name": domainConfig.FullDomain,
"content": domainConfig.Ipv4Addr,
"ttl": 3600,
"proxied": false,
}
jsonData, _ := json.Marshal(data)
_, err := provider.sendRequest("PATCH", url, jsonData)
return err == nil
}
// 以下为辅助方法,如发送 HTTP 请求等
func (provider DDNSProviderCloudflare) sendRequest(method string, url string, data []byte) ([]byte, error) {
client := &http.Client{}
req, err := http.NewRequest(method, url, bytes.NewBuffer(data))
if err != nil {
return nil, err
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", provider.Secret))
req.Header.Add("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Printf("NEZHA>> 无法关闭HTTP响应体流: %s\n", err.Error())
}
}(resp.Body)
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return body, nil
}
func (provider DDNSProviderWebHook) FormatWebhookString(s string, config *DDNSDomainConfig, ipType string) string {
if config == nil {
return s
}
result := strings.TrimSpace(s)
result = strings.Replace(s, "{ip}", config.Ipv4Addr, -1)
result = strings.Replace(result, "{domain}", config.FullDomain, -1)
result = strings.Replace(result, "{type}", ipType, -1)
result = strings.Replace(result, "{access_id}", Conf.DDNS.AccessID, -1)
result = strings.Replace(result, "{access_secret}", Conf.DDNS.AccessSecret, -1)
// remove \r
result = strings.Replace(result, "\r", "", -1)
return result
}
func SetStringHeadersToRequest(req *http.Request, headers []string) {
if req == nil {
return
}
for _, element := range headers {
kv := strings.SplitN(element, ":", 2)
if len(kv) == 2 {
req.Header.Add(kv[0], kv[1])
}
}
}
// SplitDomain 分割域名为前缀和一级域名
func SplitDomain(domain string) (prefix string, topLevelDomain string) {
// 带有二级TLD的一些常见例子,需要特别处理
secondLevelTLDs := map[string]bool{
".co.uk": true, ".com.cn": true, ".gov.cn": true, ".net.cn": true, ".org.cn": true,
}
// 分割域名为"."的各部分
parts := strings.Split(domain, ".")
// 处理特殊情况,例如 ".co.uk"
for i := len(parts) - 2; i > 0; i-- {
potentialTLD := fmt.Sprintf(".%s.%s", parts[i], parts[i+1])
if secondLevelTLDs[potentialTLD] {
if i > 1 {
return strings.Join(parts[:i-1], "."), strings.Join(parts[i-1:], ".")
}
return "", domain // 当域名仅为二级TLD时,无前缀
}
}
// 常规处理,查找最后一个"."前的所有内容作为前缀
if len(parts) > 2 {
return strings.Join(parts[:len(parts)-2], "."), strings.Join(parts[len(parts)-2:], ".")
}
return "", domain // 当域名不包含子域名时,无前缀
}
func RetryableUpdateDomain(provider DDNSProvider, config *DDNSDomainConfig, maxRetries int) bool {
if nil == config {
return false
}
for retries := 0; retries < maxRetries; retries++ {
log.Printf("NEZHA>> 正在尝试更新域名(%s)DDNS(%d/%d)\n", config.FullDomain, retries+1, maxRetries)
if provider.UpdateDomain(config) {
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS成功\n", config.FullDomain)
return true
}
}
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS失败\n", config.FullDomain)
return false
}
func GetDDNSProviderFromString(provider string) (DDNSProvider, error) {
switch provider {
case "webhook":
return DDNSProviderWebHook{
URL: Conf.DDNS.WebhookURL,
RequestMethod: Conf.DDNS.WebhookMethod,
RequestBody: Conf.DDNS.WebhookRequestBody,
RequestHeader: Conf.DDNS.WebhookHeaders,
}, nil
case "dummy":
return DDNSProviderDummy{}, nil
case "cloudflare":
return DDNSProviderCloudflare{
Secret: Conf.DDNS.AccessSecret,
}, nil
}
return DDNSProviderDummy{}, errors.New(fmt.Sprintf("无法找到配置的DDNS提供者%s", Conf.DDNS.Provider))
}
+62 -7
View File
@@ -8,9 +8,10 @@ import (
"sync"
"time"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/naiba/nezha/model"
pb "github.com/naiba/nezha/proto"
"github.com/nicksnyder/go-i18n/v2/i18n"
)
const (
@@ -36,12 +37,13 @@ func NewServiceSentinel(serviceSentinelDispatchBus chan<- model.Monitor) {
ServiceSentinelShared = &ServiceSentinel{
serviceReportChannel: make(chan ReportData, 200),
serviceStatusToday: make(map[uint64]*_TodayStatsOfMonitor),
serviceCurrentStatusIndex: make(map[uint64]int),
serviceCurrentStatusIndex: make(map[uint64]*indexStore),
serviceCurrentStatusData: make(map[uint64][]*pb.TaskResult),
lastStatus: make(map[uint64]int),
serviceResponseDataStoreCurrentUp: make(map[uint64]uint64),
serviceResponseDataStoreCurrentDown: make(map[uint64]uint64),
serviceResponseDataStoreCurrentAvgDelay: make(map[uint64]float32),
serviceResponsePing: make(map[uint64]map[uint64]*pingStore),
monitors: make(map[uint64]*model.Monitor),
sslCertCache: make(map[uint64]string),
// 30天数据缓存
@@ -95,11 +97,12 @@ type ServiceSentinel struct {
serviceResponseDataStoreLock sync.RWMutex
serviceStatusToday map[uint64]*_TodayStatsOfMonitor // [monitor_id] -> _TodayStatsOfMonitor
serviceCurrentStatusIndex map[uint64]int // [monitor_id] -> 该监控ID对应的 serviceCurrentStatusData 的最新索引下标
serviceCurrentStatusIndex map[uint64]*indexStore // [monitor_id] -> 该监控ID对应的 serviceCurrentStatusData 的最新索引下标
serviceCurrentStatusData map[uint64][]*pb.TaskResult // [monitor_id] -> []model.MonitorHistory
serviceResponseDataStoreCurrentUp map[uint64]uint64 // [monitor_id] -> 当前服务在线计数
serviceResponseDataStoreCurrentDown map[uint64]uint64 // [monitor_id] -> 当前服务离线计数
serviceResponseDataStoreCurrentAvgDelay map[uint64]float32 // [monitor_id] -> 当前服务离线计数
serviceResponsePing map[uint64]map[uint64]*pingStore // [monitor_id] -> ClientID -> delay
lastStatus map[uint64]int
sslCertCache map[uint64]string
@@ -111,6 +114,16 @@ type ServiceSentinel struct {
monthlyStatus map[uint64]*model.ServiceItemResponse // [monitor_id] -> model.ServiceItemResponse
}
type indexStore struct {
index int
t time.Time
}
type pingStore struct {
count int
ping float32
}
func (ss *ServiceSentinel) refreshMonthlyServiceStatus() {
// 刷新数据防止无人访问
ss.LoadStats()
@@ -326,6 +339,34 @@ func (ss *ServiceSentinel) worker() {
continue
}
mh := r.Data
if mh.Type == model.TaskTypeTCPPing || mh.Type == model.TaskTypeICMPPing {
monitorTcpMap, ok := ss.serviceResponsePing[mh.GetId()]
if !ok {
monitorTcpMap = make(map[uint64]*pingStore)
ss.serviceResponsePing[mh.GetId()] = monitorTcpMap
}
ts, ok := monitorTcpMap[r.Reporter]
if !ok {
ts = &pingStore{}
}
ts.count++
ts.ping = (ts.ping*float32(ts.count-1) + mh.Delay) / float32(ts.count)
if ts.count == Conf.AvgPingCount {
if ts.ping > float32(Conf.MaxTCPPingValue) {
ts.ping = float32(Conf.MaxTCPPingValue)
}
ts.count = 0
if err := DB.Create(&model.MonitorHistory{
MonitorID: mh.GetId(),
AvgDelay: ts.ping,
Data: mh.Data,
ServerID: r.Reporter,
}).Error; err != nil {
log.Println("NEZHA>> 服务监控数据持久化失败:", err)
}
}
monitorTcpMap[r.Reporter] = ts
}
ss.serviceResponseDataStoreLock.Lock()
// 写入当天状态
if mh.Successful {
@@ -336,9 +377,20 @@ func (ss *ServiceSentinel) worker() {
} else {
ss.serviceStatusToday[mh.GetId()].Down++
}
currentTime := time.Now()
if ss.serviceCurrentStatusIndex[mh.GetId()] == nil {
ss.serviceCurrentStatusIndex[mh.GetId()] = &indexStore{
t: currentTime,
index: 0,
}
}
// 写入当前数据
ss.serviceCurrentStatusData[mh.GetId()][ss.serviceCurrentStatusIndex[mh.GetId()]] = mh
ss.serviceCurrentStatusIndex[mh.GetId()]++
if ss.serviceCurrentStatusIndex[mh.GetId()].t.Before(currentTime) {
ss.serviceCurrentStatusIndex[mh.GetId()].t = currentTime.Add(30 * time.Second)
ss.serviceCurrentStatusData[mh.GetId()][ss.serviceCurrentStatusIndex[mh.GetId()].index] = mh
ss.serviceCurrentStatusIndex[mh.GetId()].index++
}
// 更新当前状态
ss.serviceResponseDataStoreCurrentUp[mh.GetId()] = 0
@@ -365,8 +417,11 @@ func (ss *ServiceSentinel) worker() {
stateCode := GetStatusCode(upPercent)
// 数据持久化
if ss.serviceCurrentStatusIndex[mh.GetId()] == _CurrentStatusSize {
ss.serviceCurrentStatusIndex[mh.GetId()] = 0
if ss.serviceCurrentStatusIndex[mh.GetId()].index == _CurrentStatusSize {
ss.serviceCurrentStatusIndex[mh.GetId()] = &indexStore{
index: 0,
t: currentTime,
}
if err := DB.Create(&model.MonitorHistory{
MonitorID: mh.GetId(),
AvgDelay: ss.serviceResponseDataStoreCurrentAvgDelay[mh.GetId()],
+20
View File
@@ -1,6 +1,7 @@
package singleton
import (
"fmt"
"log"
"time"
@@ -46,6 +47,21 @@ func InitConfigFromPath(path string) {
if err != nil {
panic(err)
}
ValidateConfig()
}
// ValidateConfig 验证配置文件有效性
func ValidateConfig() {
// 如果DDNS启用则检查Provider是否存在, 不存在直接退出
if Conf.DDNS.Enable {
_, err := GetDDNSProviderFromString(Conf.DDNS.Provider)
if err != nil {
panic(err)
}
if Conf.DDNS.MaxRetries < 1 || Conf.DDNS.MaxRetries > 10 {
panic(fmt.Errorf("DDNS.MaxRetries值域为[1, 10]的整数, 当前为 %d", Conf.DDNS.MaxRetries))
}
}
}
// InitDBFromPath 从给出的文件路径中加载数据库
@@ -99,6 +115,10 @@ func RecordTransferHourlyUsage() {
func CleanMonitorHistory() {
// 清理已被删除的服务器的监控记录与流量记录
DB.Unscoped().Delete(&model.MonitorHistory{}, "created_at < ? OR monitor_id NOT IN (SELECT `id` FROM monitors)", time.Now().AddDate(0, 0, -30))
// 由于网络监控记录的数据较多,并且前端仅使用了 1 天的数据
// 考虑到 sqlite 数据量问题,仅保留一天数据,
// server_id = 0 的数据会用于/service页面的可用性展示
DB.Unscoped().Delete(&model.MonitorHistory{}, "(created_at < ? AND server_id != 0) OR monitor_id NOT IN (SELECT `id` FROM monitors)", time.Now().AddDate(0, 0, -1))
DB.Unscoped().Delete(&model.Transfer{}, "server_id NOT IN (SELECT `id` FROM servers)")
// 计算可清理流量记录的时长
var allServerKeep time.Time