mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-01-08 08:04:37 +08:00
parent
4e6f872c02
commit
5a8ddde495
8 changed files with 211 additions and 53 deletions
|
|
@ -2,6 +2,10 @@ package v2
|
|||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
|
|
@ -201,7 +205,7 @@ func (b *BaseApi) DeleteRootCert(c *gin.Context) {
|
|||
// @Summary Load host SSH logs
|
||||
// @Accept json
|
||||
// @Param request body dto.SearchSSHLog true "request"
|
||||
// @Success 200 {object} dto.SSHLog
|
||||
// @Success 200 {object} dto.PageResult
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/ssh/log [post]
|
||||
|
|
@ -211,12 +215,49 @@ func (b *BaseApi) LoadSSHLogs(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
data, err := sshService.LoadLog(c, req)
|
||||
total, data, err := sshService.LoadLog(c, req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, data)
|
||||
|
||||
helper.SuccessWithData(c, dto.PageResult{
|
||||
Total: total,
|
||||
Items: data,
|
||||
})
|
||||
}
|
||||
|
||||
// @Tags SSH
|
||||
// @Summary Export host SSH logs
|
||||
// @Accept json
|
||||
// @Param request body dto.SearchSSHLog true "request"
|
||||
// @Success 200 {object} dto.PageResult
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/ssh/log/export [post]
|
||||
func (b *BaseApi) ExportSSHLogs(c *gin.Context) {
|
||||
var req dto.SearchSSHLog
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
tmpFile, err := sshService.ExportLog(c, req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
file, err := os.Open(tmpFile)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
_ = os.RemoveAll(tmpFile)
|
||||
}()
|
||||
info, _ := file.Stat()
|
||||
c.Header("Content-Length", strconv.FormatInt(info.Size(), 10))
|
||||
c.Header("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(info.Name()))
|
||||
http.ServeContent(c.Writer, c.Request, info.Name(), info.ModTime(), file)
|
||||
}
|
||||
|
||||
// @Tags SSH
|
||||
|
|
|
|||
|
|
@ -54,12 +54,6 @@ type SearchSSHLog struct {
|
|||
Info string `json:"info"`
|
||||
Status string `json:"Status" validate:"required,oneof=Success Failed All"`
|
||||
}
|
||||
type SSHLog struct {
|
||||
Logs []SSHHistory `json:"logs"`
|
||||
TotalCount int `json:"totalCount"`
|
||||
SuccessfulCount int `json:"successfulCount"`
|
||||
FailedCount int `json:"failedCount"`
|
||||
}
|
||||
|
||||
type SSHHistory struct {
|
||||
Date time.Time `json:"date"`
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/copier"
|
||||
csvexport "github.com/1Panel-dev/1Panel/agent/utils/csv_export"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/geo"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
|
@ -38,9 +39,11 @@ type ISSHService interface {
|
|||
OperateSSH(operation string) error
|
||||
UpdateByFile(value string) error
|
||||
Update(req dto.SSHUpdate) error
|
||||
LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (*dto.SSHLog, error)
|
||||
LoadSSHConf() (string, error)
|
||||
|
||||
LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []dto.SSHHistory, error)
|
||||
ExportLog(ctx *gin.Context, req dto.SearchSSHLog) (string, error)
|
||||
|
||||
SyncRootCert() error
|
||||
CreateRootCert(req dto.CreateRootCert) error
|
||||
SearchRootCerts(req dto.SearchWithPage) (int64, interface{}, error)
|
||||
|
|
@ -392,13 +395,13 @@ type sshFileItem struct {
|
|||
Year int
|
||||
}
|
||||
|
||||
func (u *SSHService) LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (*dto.SSHLog, error) {
|
||||
func (u *SSHService) LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []dto.SSHHistory, error) {
|
||||
var fileList []sshFileItem
|
||||
var data dto.SSHLog
|
||||
var data []dto.SSHHistory
|
||||
baseDir := "/var/log"
|
||||
fileItems, err := os.ReadDir(baseDir)
|
||||
if err != nil {
|
||||
return &data, err
|
||||
return 0, data, err
|
||||
}
|
||||
for _, item := range fileItems {
|
||||
if item.IsDir() || (!strings.HasPrefix(item.Name(), "secure") && !strings.HasPrefix(item.Name(), "auth")) {
|
||||
|
|
@ -427,6 +430,7 @@ func (u *SSHService) LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (*dto.SSHLo
|
|||
showCountFrom := (req.Page - 1) * req.PageSize
|
||||
showCountTo := req.Page * req.PageSize
|
||||
nyc, _ := time.LoadLocation(common.LoadTimeZoneByCmd())
|
||||
itemFailed, itemTotal := 0, 0
|
||||
for _, file := range fileList {
|
||||
commandItem := ""
|
||||
if strings.HasPrefix(path.Base(file.Name), "secure") {
|
||||
|
|
@ -450,15 +454,38 @@ func (u *SSHService) LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (*dto.SSHLo
|
|||
}
|
||||
}
|
||||
dataItem, successCount, failedCount := loadSSHData(ctx, commandItem, showCountFrom, showCountTo, file.Year, nyc)
|
||||
data.FailedCount += failedCount
|
||||
data.TotalCount += successCount + failedCount
|
||||
itemFailed += failedCount
|
||||
itemTotal += successCount + failedCount
|
||||
showCountFrom = showCountFrom - (successCount + failedCount)
|
||||
showCountTo = showCountTo - (successCount + failedCount)
|
||||
data.Logs = append(data.Logs, dataItem...)
|
||||
if showCountTo != -1 {
|
||||
showCountTo = showCountTo - (successCount + failedCount)
|
||||
}
|
||||
data = append(data, dataItem...)
|
||||
}
|
||||
|
||||
data.SuccessfulCount = data.TotalCount - data.FailedCount
|
||||
return &data, nil
|
||||
total := itemTotal
|
||||
if req.Status == constant.StatusFailed {
|
||||
total = itemFailed
|
||||
}
|
||||
if req.Status == constant.StatusSuccess {
|
||||
total = itemTotal - itemFailed
|
||||
}
|
||||
return int64(total), data, nil
|
||||
}
|
||||
|
||||
func (u *SSHService) ExportLog(ctx *gin.Context, req dto.SearchSSHLog) (string, error) {
|
||||
_, logs, err := u.LoadLog(ctx, req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
tmpFileName := path.Join(global.Dir.TmpDir, "export/ssh-log", fmt.Sprintf("1panel-ssh-log-%s.csv", time.Now().Format(constant.DateTimeSlimLayout)))
|
||||
if _, err := os.Stat(path.Dir(tmpFileName)); err != nil {
|
||||
_ = os.MkdirAll(path.Dir(tmpFileName), constant.DirPerm)
|
||||
}
|
||||
if err := csvexport.ExportSSHLogs(tmpFileName, logs); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return tmpFileName, nil
|
||||
}
|
||||
|
||||
func (u *SSHService) LoadSSHConf() (string, error) {
|
||||
|
|
@ -543,7 +570,7 @@ func loadSSHData(ctx *gin.Context, command string, showCountFrom, showCountTo, c
|
|||
case strings.Contains(lines[i], "Failed password for"):
|
||||
itemData = loadFailedSecureDatas(lines[i])
|
||||
if checkIsStandard(itemData) {
|
||||
if successCount+failedCount >= showCountFrom && successCount+failedCount < showCountTo {
|
||||
if successCount+failedCount >= showCountFrom && (showCountTo == -1 || successCount+failedCount < showCountTo) {
|
||||
itemData.Area, _ = geo.GetIPLocation(getLoc, itemData.Address, common.GetLang(ctx))
|
||||
itemData.Date = loadDate(currentYear, itemData.DateStr, nyc)
|
||||
datas = append(datas, itemData)
|
||||
|
|
@ -553,7 +580,7 @@ func loadSSHData(ctx *gin.Context, command string, showCountFrom, showCountTo, c
|
|||
case strings.Contains(lines[i], "Connection closed by authenticating user"):
|
||||
itemData = loadFailedAuthDatas(lines[i])
|
||||
if checkIsStandard(itemData) {
|
||||
if successCount+failedCount >= showCountFrom && successCount+failedCount < showCountTo {
|
||||
if successCount+failedCount >= showCountFrom && (showCountTo == -1 || successCount+failedCount < showCountTo) {
|
||||
itemData.Area, _ = geo.GetIPLocation(getLoc, itemData.Address, common.GetLang(ctx))
|
||||
itemData.Date = loadDate(currentYear, itemData.DateStr, nyc)
|
||||
datas = append(datas, itemData)
|
||||
|
|
@ -563,7 +590,7 @@ func loadSSHData(ctx *gin.Context, command string, showCountFrom, showCountTo, c
|
|||
case strings.Contains(lines[i], "Accepted "):
|
||||
itemData = loadSuccessDatas(lines[i])
|
||||
if checkIsStandard(itemData) {
|
||||
if successCount+failedCount >= showCountFrom && successCount+failedCount < showCountTo {
|
||||
if successCount+failedCount >= showCountFrom && (showCountTo == -1 || successCount+failedCount < showCountTo) {
|
||||
itemData.Area, _ = geo.GetIPLocation(getLoc, itemData.Address, common.GetLang(ctx))
|
||||
itemData.Date = loadDate(currentYear, itemData.DateStr, nyc)
|
||||
datas = append(datas, itemData)
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
|
|||
hostRouter.POST("/ssh/search", baseApi.GetSSHInfo)
|
||||
hostRouter.POST("/ssh/update", baseApi.UpdateSSH)
|
||||
hostRouter.POST("/ssh/log", baseApi.LoadSSHLogs)
|
||||
hostRouter.POST("/ssh/log/export", baseApi.ExportSSHLogs)
|
||||
hostRouter.POST("/ssh/conffile/update", baseApi.UpdateSSHByfile)
|
||||
hostRouter.POST("/ssh/operate", baseApi.OperateSSH)
|
||||
|
||||
|
|
|
|||
41
agent/utils/csv_export/ssh_log.go
Normal file
41
agent/utils/csv_export/ssh_log.go
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
package csvexport
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"os"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
)
|
||||
|
||||
func ExportSSHLogs(filename string, logs []dto.SSHHistory) error {
|
||||
file, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
writer := csv.NewWriter(file)
|
||||
defer writer.Flush()
|
||||
|
||||
if err := writer.Write([]string{"IP", "Area", "Port", "AuthMode", "User", "Status", "Date"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, log := range logs {
|
||||
record := []string{
|
||||
log.Address,
|
||||
log.Area,
|
||||
log.Port,
|
||||
log.AuthMode,
|
||||
log.User,
|
||||
log.Status,
|
||||
log.Date.Format(constant.DateTimeLayout),
|
||||
}
|
||||
if err := writer.Write(record); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -194,24 +194,6 @@ export namespace Host {
|
|||
export interface analysisSSHLog extends ReqPage {
|
||||
orderBy: string;
|
||||
}
|
||||
export interface logAnalysisRes {
|
||||
total: number;
|
||||
items: Array<logAnalysis>;
|
||||
successfulCount: number;
|
||||
failedCount: number;
|
||||
}
|
||||
export interface sshLog {
|
||||
logs: Array<sshHistory>;
|
||||
successfulCount: number;
|
||||
failedCount: number;
|
||||
}
|
||||
export interface logAnalysis {
|
||||
address: string;
|
||||
area: string;
|
||||
successfulCount: number;
|
||||
failedCount: number;
|
||||
status: string;
|
||||
}
|
||||
export interface sshHistory {
|
||||
date: Date;
|
||||
area: string;
|
||||
|
|
|
|||
|
|
@ -96,5 +96,11 @@ export const syncCert = () => {
|
|||
return http.post(`/hosts/ssh/cert/sync`);
|
||||
};
|
||||
export const loadSSHLogs = (params: Host.searchSSHLog) => {
|
||||
return http.post<Host.sshLog>(`/hosts/ssh/log`, params);
|
||||
return http.post<ResPage<Host.sshHistory>>(`/hosts/ssh/log`, params);
|
||||
};
|
||||
export const exportSSHLogs = (params: Host.searchSSHLog) => {
|
||||
return http.download<BlobPart>('/hosts/ssh/log/export', params, {
|
||||
responseType: 'blob',
|
||||
timeout: TimeoutEnum.T_40S,
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@
|
|||
<el-alert type="info" :title="$t('ssh.sshAlert2')" :closable="false" />
|
||||
<div class="mt-2"><el-alert type="info" :title="$t('ssh.sshAlert')" :closable="false" /></div>
|
||||
</template>
|
||||
<template #leftToolBar>
|
||||
<el-button type="primary" @click="onExport">
|
||||
{{ $t('commons.button.export') }}
|
||||
</el-button>
|
||||
</template>
|
||||
<template #rightToolBar>
|
||||
<el-select v-model="searchStatus" @change="search()" class="p-w-200">
|
||||
<template #prefix>{{ $t('commons.table.status') }}</template>
|
||||
|
|
@ -49,13 +54,46 @@
|
|||
</ComplexTable>
|
||||
</template>
|
||||
</LayoutContent>
|
||||
|
||||
<DialogPro v-model="open" :title="$t('commons.button.export')" size="mini">
|
||||
<el-form class="mt-5" ref="backupForm" @submit.prevent v-loading="loading">
|
||||
<el-form-item :label="$t('commons.table.status')">
|
||||
<el-select v-model="exportConfig.status" class="w-full">
|
||||
<el-option :label="$t('commons.table.all')" value="All"></el-option>
|
||||
<el-option :label="$t('commons.status.success')" value="Success"></el-option>
|
||||
<el-option :label="$t('commons.status.failed')" value="Failed"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('container.lines')">
|
||||
<el-select class="tailClass" v-model.number="exportConfig.count">
|
||||
<el-option :value="-1" :label="$t('commons.table.all')" />
|
||||
<el-option :value="100" :label="100" />
|
||||
<el-option :value="200" :label="200" />
|
||||
<el-option :value="500" :label="500" />
|
||||
<el-option :value="1000" :label="1000" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="open = false" :disabled="loading">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
<el-button type="primary" @click="onSubmitExport" :disabled="loading">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DialogPro>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { dateFormat } from '@/utils/util';
|
||||
import { dateFormat, getDateStr } from '@/utils/util';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { loadSSHLogs } from '@/api/modules/host';
|
||||
import { exportSSHLogs, loadSSHLogs } from '@/api/modules/host';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import i18n from '@/lang';
|
||||
|
||||
const loading = ref();
|
||||
const data = ref();
|
||||
|
|
@ -65,6 +103,12 @@ const paginationConfig = reactive({
|
|||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const open = ref();
|
||||
const exportConfig = reactive({
|
||||
count: 100,
|
||||
status: 'All',
|
||||
});
|
||||
const searchInfo = ref();
|
||||
const searchStatus = ref('All');
|
||||
|
||||
|
|
@ -79,16 +123,38 @@ const search = async () => {
|
|||
await loadSSHLogs(params)
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
data.value = res.data?.logs || [];
|
||||
if (searchStatus.value === 'Success') {
|
||||
paginationConfig.total = res.data.successfulCount;
|
||||
}
|
||||
if (searchStatus.value === 'Failed') {
|
||||
paginationConfig.total = res.data.failedCount;
|
||||
}
|
||||
if (searchStatus.value === 'All') {
|
||||
paginationConfig.total = res.data.failedCount + res.data.successfulCount;
|
||||
}
|
||||
data.value = res.data.items || [];
|
||||
paginationConfig.total = res.data.total;
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const onExport = async () => {
|
||||
open.value = true;
|
||||
exportConfig.status = 'All';
|
||||
exportConfig.count = -1;
|
||||
};
|
||||
|
||||
const onSubmitExport = async () => {
|
||||
let params = {
|
||||
info: '',
|
||||
status: exportConfig.status,
|
||||
page: 1,
|
||||
pageSize: exportConfig.count,
|
||||
};
|
||||
await exportSSHLogs(params)
|
||||
.then((res) => {
|
||||
const downloadUrl = window.URL.createObjectURL(new Blob([res]));
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
a.href = downloadUrl;
|
||||
a.download = '1panel-ssh-log-' + getDateStr() + '.csv';
|
||||
const event = new MouseEvent('click');
|
||||
a.dispatchEvent(event);
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
open.value = false;
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue