pull/639/merge
JrDw0 2025-04-18 09:21:05 +08:00 committed by GitHub
commit 7f73b45eec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 307 additions and 19 deletions

View File

@ -17,4 +17,6 @@ urlpatterns = [
path('import/region/', get_regions), path('import/region/', get_regions),
path('parse/', post_parse), path('parse/', post_parse),
path('valid/', batch_valid), path('valid/', batch_valid),
path('processes/', get_processes),
path('ports/', get_ports),
] ]

View File

@ -1,20 +1,22 @@
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com> # Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License. # Released under the AGPL-3.0 License.
from django_redis import get_redis_connection
from libs.helper import make_ali_request, make_tencent_request
from libs.ssh import SSH, AuthenticationException
from libs.utils import AttrDict, human_datetime
from libs.validators import ip_validator
from apps.host.models import HostExtend
from apps.setting.utils import AppSetting
from collections import defaultdict
from datetime import datetime, timezone
from concurrent import futures
import ipaddress import ipaddress
import json import json
import math import math
import os import os
from collections import defaultdict
from concurrent import futures
from datetime import datetime, timezone
from django_redis import get_redis_connection
from apps.host.models import HostExtend
from apps.setting.utils import AppSetting
from libs.helper import make_ali_request, make_tencent_request
from libs.ssh import SSH, AuthenticationException
from libs.utils import AttrDict, human_datetime
from libs.validators import ip_validator
def check_os_type(os_name): def check_os_type(os_name):
@ -201,7 +203,7 @@ def fetch_host_extend(ssh):
code, out = ssh.exec_command_raw('hostname -I') code, out = ssh.exec_command_raw('hostname -I')
if code == 0: if code == 0:
for ip in out.strip().split(): for ip in out.strip().split():
if len(ip) > 15: # ignore ipv6 if len(ip) > 15: # ignore ipv6
continue continue
if ipaddress.ip_address(ip).is_global: if ipaddress.ip_address(ip).is_global:
if len(public_ip_address) < 10: if len(public_ip_address) < 10:
@ -301,3 +303,51 @@ def _get_ssh(kwargs, pkey=None, private_key=None, public_key=None, password=None
ssh.add_public_key(public_key) ssh.add_public_key(public_key)
return _get_ssh(kwargs, private_key) return _get_ssh(kwargs, private_key)
raise e raise e
def _sync_host_process(host, private_key=None, public_key=None, password=None, ssh=None):
if not ssh:
kwargs = host.to_dict(selects=('hostname', 'port', 'username'))
with _get_ssh(kwargs, host.pkey, private_key, public_key, password) as ssh:
return _sync_host_process(host, ssh=ssh)
process_list = fetch_host_processes(ssh)
return process_list
def fetch_host_processes(ssh):
command = '''ps_info=$(ps -e -o pid=,comm=,ppid=,user=,%cpu=,%mem=,rss,uid= --no-headers); echo -n '['; while read -r line; do read pid name ppid username cpu_usage memory_usage memory uid <<<$(echo $line); if [ -e \"/proc/${pid}/cmdline\" ]; then command=$(tr '\\0' ' ' <\"/proc/${pid}/cmdline\" | awk '{$1=$1};1'| sed 's/\\\\/\\\\\\\\/g' | sed 's/\"/\\\\\"/g' 2>/dev/null); start_time=$(stat -c %Y \"/proc/${pid}\" 2>/dev/null); echo \"{\\\"name\\\":\\\"${name}\\\",\\\"pid\\\":${pid},\\\"ppid\\\":${ppid},\\\"username\\\":\\\"${username}\\\",\\\"uid\\\":${uid},\\\"start_time\\\":${start_time},\\\"cpu_usage\\\":\\\"${cpu_usage}\\\",\\\"memory_usage\\\":\\\"${memory_usage}\\\",\\\"memory\\\":\\\"${memory}\\\",\\\"command\\\":\\\"${command}\\\"},\"; fi; done <<<\"$ps_info\" | sed '$s/,$/]/';'''
code, out = ssh.exec_command(command)
if code == 0:
try:
_j = json.loads(out.strip())
return _j
except Exception as e:
print(e)
print(out)
elif code != 0:
print(code, out)
return []
def _sync_host_ports(host, private_key=None, public_key=None, password=None, ssh=None):
if not ssh:
kwargs = host.to_dict(selects=('hostname', 'port', 'username'))
with _get_ssh(kwargs, host.pkey, private_key, public_key, password) as ssh:
return _sync_host_ports(host, ssh=ssh)
ports_list = fetch_host_ports(ssh)
return ports_list
def fetch_host_ports(ssh):
command = '''netstat -nltp | awk 'NR>2 {cmd=\"netstat -n | grep -c \\\"\"$4\"\\\"\"; cmd | getline conn_count; close(cmd); printf \"{\\\"protocol\\\":\\\"%s\\\",\\\"listen\\\":\\\"%s\\\",\\\"pid\\\":\\\"%s\\\",\\\"connections\\\":\\\"%s\\\"},\", $1, $4, $7, conn_count}' | sed 's/,$/]/' | awk 'BEGIN {printf\"[\"} {print}' '''
code, out = ssh.exec_command(command)
if code == 0:
try:
_j = json.loads(out.strip())
return _j
except Exception as e:
print(e)
print(out)
elif code != 0:
print(code, out)
return []

View File

@ -6,7 +6,7 @@ from django.db.models import F
from django.http.response import HttpResponseBadRequest from django.http.response import HttpResponseBadRequest
from libs import json_response, JsonParser, Argument, AttrDict, auth from libs import json_response, JsonParser, Argument, AttrDict, auth
from apps.setting.utils import AppSetting from apps.setting.utils import AppSetting
from apps.account.utils import get_host_perms from apps.account.utils import get_host_perms, has_host_perm
from apps.host.models import Host, Group from apps.host.models import Host, Group
from apps.host.utils import batch_sync_host, _sync_host_extend from apps.host.utils import batch_sync_host, _sync_host_extend
from apps.exec.models import ExecTemplate from apps.exec.models import ExecTemplate
@ -230,3 +230,45 @@ def _do_host_verify(form):
except socket.timeout: except socket.timeout:
raise Exception('连接主机超时,请检查网络') raise Exception('连接主机超时,请检查网络')
return True return True
@auth('host.host.view')
def get_processes(request):
form, error = JsonParser(
Argument('host_id', type=int, help='参数错误'),
).parse(request.body)
if error is None:
if not has_host_perm(request.user, form.host_id):
return json_response(error='无权访问主机,请联系管理员')
private_key, public_key = AppSetting.get_ssh_key()
host = Host.objects.filter(id=form.host_id).first()
if host.is_verified:
try:
result = _sync_host_process(host=host, private_key=private_key)
except socket.timeout:
return json_response(error='连接主机超时,请检查网络')
return json_response(result)
else:
return json_response(error='该主机未验证,请先验证')
return json_response(error=error)
@auth('host.host.view')
def get_ports(request):
form, error = JsonParser(
Argument('host_id', type=int, help='参数错误'),
).parse(request.body)
if error is None:
if not has_host_perm(request.user, form.host_id):
return json_response(error='无权访问主机,请联系管理员')
private_key, public_key = AppSetting.get_ssh_key()
host = Host.objects.filter(id=form.host_id).first()
if host.is_verified:
try:
result = _sync_host_ports(host=host, private_key=private_key)
except socket.timeout:
return json_response(error='连接主机超时,请检查网络')
return json_response(result)
else:
return json_response(error='该主机未验证,请先验证')
return json_response(error=error)

View File

@ -5,7 +5,7 @@
*/ */
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Drawer, Descriptions, List, Button, Input, Select, DatePicker, Tag, message } from 'antd'; import {Drawer, Descriptions, List, Button, Input, Select, DatePicker, Tag, message, Tabs} from 'antd';
import { EditOutlined, SaveOutlined, PlusOutlined, SyncOutlined } from '@ant-design/icons'; import { EditOutlined, SaveOutlined, PlusOutlined, SyncOutlined } from '@ant-design/icons';
import { AuthButton } from 'components'; import { AuthButton } from 'components';
import { http } from 'libs'; import { http } from 'libs';
@ -13,7 +13,8 @@ import store from './store';
import lds from 'lodash'; import lds from 'lodash';
import moment from 'moment'; import moment from 'moment';
import styles from './index.module.less'; import styles from './index.module.less';
import ProcessesTable from "./Processes";
import PortsTable from "./Ports";
export default observer(function () { export default observer(function () {
const [edit, setEdit] = useState(false); const [edit, setEdit] = useState(false);
const [host, setHost] = useState(store.record); const [host, setHost] = useState(store.record);
@ -110,7 +111,7 @@ export default observer(function () {
return ( return (
<Drawer <Drawer
width={550} width={1500}
title={host.name} title={host.name}
placement="right" placement="right"
onClose={handleClose} onClose={handleClose}
@ -118,9 +119,10 @@ export default observer(function () {
<Descriptions <Descriptions
bordered bordered
size="small" size="small"
labelStyle={{width: 150}} // labelStyle={{width: 150}}
title={<span style={{fontWeight: 500}}>基本信息</span>} title={<span style={{fontWeight: 500}}>基本信息</span>}
column={1}> // column={1}
>
<Descriptions.Item label="主机名称">{host.name}</Descriptions.Item> <Descriptions.Item label="主机名称">{host.name}</Descriptions.Item>
<Descriptions.Item label="连接地址">{host.username}@{host.hostname}</Descriptions.Item> <Descriptions.Item label="连接地址">{host.username}@{host.hostname}</Descriptions.Item>
<Descriptions.Item label="连接端口">{host.port}</Descriptions.Item> <Descriptions.Item label="连接端口">{host.port}</Descriptions.Item>
@ -137,9 +139,9 @@ export default observer(function () {
<Descriptions <Descriptions
bordered bordered
size="small" size="small"
column={1} // column={1}
className={edit ? styles.hostExtendEdit : null} className={edit ? styles.hostExtendEdit : null}
labelStyle={{width: 150}} // labelStyle={{width: 150}}
style={{marginTop: 24}} style={{marginTop: 24}}
extra={edit ? ([ extra={edit ? ([
<Button key="1" type="link" loading={fetching} icon={<SyncOutlined/>} onClick={handleFetch}>同步</Button>, <Button key="1" type="link" loading={fetching} icon={<SyncOutlined/>} onClick={handleFetch}>同步</Button>,
@ -270,6 +272,16 @@ export default observer(function () {
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="更新时间">{host.updated_at}</Descriptions.Item> <Descriptions.Item label="更新时间">{host.updated_at}</Descriptions.Item>
</Descriptions> </Descriptions>
{host.id !== undefined && store.detailVisible ? (
<Tabs>
<Tabs.TabPane tab="进程清单" key="item-1">
<ProcessesTable host_id={store.record.id}/>
</Tabs.TabPane>
<Tabs.TabPane tab="网络端口" key="item-2">
<PortsTable host_id={store.record.id}/>
</Tabs.TabPane>
</Tabs>
) : null}
</Drawer> </Drawer>
) )
}) })

View File

@ -0,0 +1,71 @@
/**
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
* Copyright (c) <spug.dev@gmail.com>
* Released under the AGPL-3.0 License.
*/
import React, {useEffect, useState} from 'react';
import {Input, Table} from 'antd';
import {TableCard} from 'components';
import {observer} from "mobx-react";
import {http} from "../../libs";
export default observer(function PortsTable(value) {
let [portFetching, setPortFetching] = useState(false)
let [dataSource, setDataSource] = useState([])
let [searchText, setSearchText] = useState(''); // 新增搜索文本状态
useEffect(() => {
fetchPorts(value.host_id)
}, [])
function fetchPorts() {
console.log("host_id:" + value.host_id, "portFetching:" + portFetching)
setPortFetching(true);
return http.post('/api/host/ports/', {
'host_id': value.host_id
})
.then(res => {
setDataSource(res)
})
.finally(() => setPortFetching(false))
}
function handleSearch(value) {
setSearchText(value);
}
const filteredDataSource = dataSource.filter(item =>
item.listen.toLowerCase().includes(searchText.toLowerCase()) ||
item.pid.toLowerCase().includes(searchText.toLowerCase())
);
return (<TableCard
tKey="mi"
rowKey="id"
title={<Input.Search allowClear value={searchText} placeholder="输入端口/PID检索" style={{maxWidth: 250}}
onChange={e => handleSearch(e.target.value)}/>}
loading={portFetching}
dataSource={filteredDataSource}
onReload={fetchPorts}
pagination={{
showSizeChanger: true,
showLessItems: true,
showTotal: total => `${total}`,
defaultPageSize: 50,
pageSizeOptions: ['50', '100']
}}
scroll={{
y: 240,
}}
>
<Table.Column title="协议" dataIndex="protocol"/>
<Table.Column title="监听地址" dataIndex="listen"/>
<Table.Column title="连接数" dataIndex="connections"
sorter={(a, b) => a.connections.localeCompare(b.connections)}
sortDirections={['descend']}
defaultSortOrder="descend"
/>
<Table.Column title="PID/进程名" dataIndex="pid"
/>
</TableCard>)
})

View File

@ -0,0 +1,111 @@
/**
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
* Copyright (c) <spug.dev@gmail.com>
* Released under the AGPL-3.0 License.
*/
import React, {useEffect, useState} from 'react';
import {Input, Table} from 'antd';
import {TableCard} from 'components';
import {observer} from "mobx-react";
import {http} from "../../libs";
export default observer(function ProcessesTable(value) {
let [processFetching, setProcessFetching] = useState(false)
let [dataSource, setDataSource] = useState([])
let [searchText, setSearchText] = useState(''); // 新增搜索文本状态
useEffect(() => {
fetchProcesses(value.host_id)
}, [])
function TimestampConverter(timestampInSeconds) {
const date = new Date(timestampInSeconds * 1000);
const year = date.getFullYear();
const month = ('0' + (date.getMonth() + 1)).slice(-2);
const day = ('0' + date.getDate()).slice(-2);
const hours = ('0' + date.getHours()).slice(-2);
const minutes = ('0' + date.getMinutes()).slice(-2);
const seconds = ('0' + date.getSeconds()).slice(-2);
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
function formatMemory(memoryUsed) {
if (memoryUsed >= 1024) {
return (memoryUsed / 1024).toFixed(2) + ' MB';
} else if (memoryUsed >= 0) {
return (memoryUsed) + ' KB';
}
}
function fetchProcesses() {
console.log("host_id:" + value.host_id, "processFetching:" + processFetching)
setProcessFetching(true);
return http.post('/api/host/processes/', {
'host_id': value.host_id
})
.then(res => {
setDataSource(res)
})
.finally(() => setProcessFetching(false))
}
function handleSearch(value) {
setSearchText(value);
}
const filteredDataSource = dataSource.filter(item =>
item.name.toLowerCase().includes(searchText.toLowerCase()) ||
item.pid.toString().includes(searchText.toLowerCase()) ||
item.command.toLowerCase().includes(searchText.toLowerCase())
);
return (<TableCard
tKey="mi"
rowKey="id"
title={<Input.Search allowClear value={searchText} placeholder="输入进程名/PID/命令检索" style={{maxWidth: 250}}
onChange={e => handleSearch(e.target.value)}/>}
loading={processFetching}
dataSource={filteredDataSource}
onReload={fetchProcesses}
pagination={{
showSizeChanger: true,
showLessItems: true,
showTotal: total => `${total}`,
defaultPageSize: 50,
pageSizeOptions: ['50', '100']
}}
scroll={{
y: 240,
}}
>
<Table.Column title="进程名 / PID / PPID" width={200}
render={info => `${info.name} / ${info.pid} / ${info.ppid}`}/>
<Table.Column title="用户 / UID" width={100}
render={info => `${info.username} / ${info.uid}`}/>
<Table.Column title="启动时间" width={200}
render={info => `${TimestampConverter(info.start_time)}`}/>
<Table.Column title="CPU" width={100}
render={info => `${info.cpu_usage}%`}
sorter={(a, b) => a.cpu_usage.localeCompare(b.cpu_usage)}
defaultSortOrder="descend"
sortDirections={['descend']}
/>
<Table.Column title="内存" width={200}
render={info => {
return formatMemory(info.memory) + `(${info.memory_usage}%)`
}}
sorter={(a, b) => a.memory_usage.localeCompare(b.memory_usage)}
defaultSortOrder="descend"
sortDirections={['descend']}
/>
<Table.Column title="命令参数"
render={info => `${info.command}`}
ellipsis
/>
</TableCard>)
})