mirror of https://github.com/openspug/spug
Merge c1cb8e525d
into 0e7b5ec77e
commit
7f73b45eec
|
@ -17,4 +17,6 @@ urlpatterns = [
|
|||
path('import/region/', get_regions),
|
||||
path('parse/', post_parse),
|
||||
path('valid/', batch_valid),
|
||||
path('processes/', get_processes),
|
||||
path('ports/', get_ports),
|
||||
]
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
|
||||
# Copyright: (c) <spug.dev@gmail.com>
|
||||
# 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 json
|
||||
import math
|
||||
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):
|
||||
|
@ -301,3 +303,51 @@ def _get_ssh(kwargs, pkey=None, private_key=None, public_key=None, password=None
|
|||
ssh.add_public_key(public_key)
|
||||
return _get_ssh(kwargs, private_key)
|
||||
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 []
|
||||
|
|
|
@ -6,7 +6,7 @@ from django.db.models import F
|
|||
from django.http.response import HttpResponseBadRequest
|
||||
from libs import json_response, JsonParser, Argument, AttrDict, auth
|
||||
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.utils import batch_sync_host, _sync_host_extend
|
||||
from apps.exec.models import ExecTemplate
|
||||
|
@ -230,3 +230,45 @@ def _do_host_verify(form):
|
|||
except socket.timeout:
|
||||
raise Exception('连接主机超时,请检查网络')
|
||||
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)
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
import React, { useState, useEffect, useRef } from '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 { AuthButton } from 'components';
|
||||
import { http } from 'libs';
|
||||
|
@ -13,7 +13,8 @@ import store from './store';
|
|||
import lds from 'lodash';
|
||||
import moment from 'moment';
|
||||
import styles from './index.module.less';
|
||||
|
||||
import ProcessesTable from "./Processes";
|
||||
import PortsTable from "./Ports";
|
||||
export default observer(function () {
|
||||
const [edit, setEdit] = useState(false);
|
||||
const [host, setHost] = useState(store.record);
|
||||
|
@ -110,7 +111,7 @@ export default observer(function () {
|
|||
|
||||
return (
|
||||
<Drawer
|
||||
width={550}
|
||||
width={1500}
|
||||
title={host.name}
|
||||
placement="right"
|
||||
onClose={handleClose}
|
||||
|
@ -118,9 +119,10 @@ export default observer(function () {
|
|||
<Descriptions
|
||||
bordered
|
||||
size="small"
|
||||
labelStyle={{width: 150}}
|
||||
// labelStyle={{width: 150}}
|
||||
title={<span style={{fontWeight: 500}}>基本信息</span>}
|
||||
column={1}>
|
||||
// column={1}
|
||||
>
|
||||
<Descriptions.Item label="主机名称">{host.name}</Descriptions.Item>
|
||||
<Descriptions.Item label="连接地址">{host.username}@{host.hostname}</Descriptions.Item>
|
||||
<Descriptions.Item label="连接端口">{host.port}</Descriptions.Item>
|
||||
|
@ -137,9 +139,9 @@ export default observer(function () {
|
|||
<Descriptions
|
||||
bordered
|
||||
size="small"
|
||||
column={1}
|
||||
// column={1}
|
||||
className={edit ? styles.hostExtendEdit : null}
|
||||
labelStyle={{width: 150}}
|
||||
// labelStyle={{width: 150}}
|
||||
style={{marginTop: 24}}
|
||||
extra={edit ? ([
|
||||
<Button key="1" type="link" loading={fetching} icon={<SyncOutlined/>} onClick={handleFetch}>同步</Button>,
|
||||
|
@ -270,6 +272,16 @@ export default observer(function () {
|
|||
</Descriptions.Item>
|
||||
<Descriptions.Item label="更新时间">{host.updated_at}</Descriptions.Item>
|
||||
</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>
|
||||
)
|
||||
})
|
|
@ -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>)
|
||||
})
|
|
@ -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>)
|
||||
})
|
Loading…
Reference in New Issue