diff --git a/spug_api/apps/host/urls.py b/spug_api/apps/host/urls.py index 939195d5..3aaa2157 100644 --- a/spug_api/apps/host/urls.py +++ b/spug_api/apps/host/urls.py @@ -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), ] diff --git a/spug_api/apps/host/utils.py b/spug_api/apps/host/utils.py index 2dec2667..e9dff0ec 100644 --- a/spug_api/apps/host/utils.py +++ b/spug_api/apps/host/utils.py @@ -1,20 +1,22 @@ # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug # Copyright: (c) # 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): @@ -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: @@ -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 [] diff --git a/spug_api/apps/host/views.py b/spug_api/apps/host/views.py index db5036e9..dd77baad 100644 --- a/spug_api/apps/host/views.py +++ b/spug_api/apps/host/views.py @@ -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 @@ -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) + diff --git a/spug_web/src/pages/host/Detail.js b/spug_web/src/pages/host/Detail.js index 5e99d9a8..22092abb 100644 --- a/spug_web/src/pages/host/Detail.js +++ b/spug_web/src/pages/host/Detail.js @@ -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 ( 基本信息} - column={1}> + // column={1} + > {host.name} {host.username}@{host.hostname} {host.port} @@ -137,9 +139,9 @@ export default observer(function () { } onClick={handleFetch}>同步, @@ -270,6 +272,16 @@ export default observer(function () { {host.updated_at} + {host.id !== undefined && store.detailVisible ? ( + + + + + + + + + ) : null} ) }) \ No newline at end of file diff --git a/spug_web/src/pages/host/Ports.js b/spug_web/src/pages/host/Ports.js new file mode 100644 index 00000000..39bcd131 --- /dev/null +++ b/spug_web/src/pages/host/Ports.js @@ -0,0 +1,71 @@ +/** + * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug + * Copyright (c) + * 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 ( 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, + }} + > + + + a.connections.localeCompare(b.connections)} + sortDirections={['descend']} + defaultSortOrder="descend" + /> + + ) +}) \ No newline at end of file diff --git a/spug_web/src/pages/host/Processes.js b/spug_web/src/pages/host/Processes.js new file mode 100644 index 00000000..3cfb3cef --- /dev/null +++ b/spug_web/src/pages/host/Processes.js @@ -0,0 +1,111 @@ +/** + * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug + * Copyright (c) + * 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 ( 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, + }} + > + `${info.name} / ${info.pid} / ${info.ppid}`}/> + `${info.username} / ${info.uid}`}/> + `${TimestampConverter(info.start_time)}`}/> + `${info.cpu_usage}%`} + sorter={(a, b) => a.cpu_usage.localeCompare(b.cpu_usage)} + defaultSortOrder="descend" + sortDirections={['descend']} + /> + { + return formatMemory(info.memory) + `(${info.memory_usage}%)` + }} + sorter={(a, b) => a.memory_usage.localeCompare(b.memory_usage)} + defaultSortOrder="descend" + sortDirections={['descend']} + /> + `${info.command}`} + ellipsis + /> + ) +}) \ No newline at end of file