Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

主机详情增加进程清单、网络端口 #639

Open
wants to merge 1 commit into
base: 3.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions spug_api/apps/host/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@
path('import/region/', get_regions),
path('parse/', post_parse),
path('valid/', batch_valid),
path('processes/', get_processes),
path('ports/', get_ports),
]
70 changes: 60 additions & 10 deletions spug_api/apps/host/utils.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <[email protected]>
# Released under the AGPL-3.0 License.
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
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


def check_os_type(os_name):
Expand Down Expand Up @@ -201,7 +203,7 @@ def fetch_host_extend(ssh):
code, out = ssh.exec_command_raw('hostname -I')
if code == 0:
for ip in out.strip().split():
if len(ip) > 15: # ignore ipv6
if len(ip) > 15: # ignore ipv6
continue
if ipaddress.ip_address(ip).is_global:
if len(public_ip_address) < 10:
Expand Down Expand Up @@ -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 []
44 changes: 43 additions & 1 deletion spug_api/apps/host/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
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
Expand Down Expand Up @@ -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)

26 changes: 19 additions & 7 deletions spug_web/src/pages/host/Detail.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@
*/
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';
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);
Expand Down Expand Up @@ -110,17 +111,18 @@ export default observer(function () {

return (
<Drawer
width={550}
width={1500}
title={host.name}
placement="right"
onClose={handleClose}
visible={store.detailVisible}>
<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>
Expand All @@ -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>,
Expand Down Expand Up @@ -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>
)
})
71 changes: 71 additions & 0 deletions spug_web/src/pages/host/Ports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
* Copyright (c) <[email protected]>
* 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>)
})
Loading