diff --git a/spug_api/apps/host/utils.py b/spug_api/apps/host/utils.py index 463a33c..d550e95 100644 --- a/spug_api/apps/host/utils.py +++ b/spug_api/apps/host/utils.py @@ -248,12 +248,14 @@ def fetch_host_extend(ssh): return response -def batch_sync_host(token, hosts, password): +def batch_sync_host(token, hosts, password=None): private_key, public_key = AppSetting.get_ssh_key() threads, latest_exception, rds = [], None, get_redis_connection() max_workers = max(10, os.cpu_count() * 5) with futures.ThreadPoolExecutor(max_workers=max_workers) as executor: for host in hosts: + if hasattr(host, 'password'): + password = host.password t = executor.submit(_sync_host_extend, host, private_key, public_key, password) t.host = host threads.append(t) diff --git a/spug_api/apps/host/views.py b/spug_api/apps/host/views.py index db7c963..8fb6e36 100644 --- a/spug_api/apps/host/views.py +++ b/spug_api/apps/host/views.py @@ -129,31 +129,40 @@ class HostView(View): def post_import(request): group_id = request.POST.get('group_id') file = request.FILES['file'] + hosts = [] ws = load_workbook(file, read_only=True)['Sheet1'] - summary = {'invalid': [], 'skip': [], 'repeat': [], 'success': []} - for i, row in enumerate(ws.rows): - if i == 0: # 第1行是表头 略过 + summary = {'fail': 0, 'success': 0, 'invalid': [], 'skip': [], 'repeat': []} + for i, row in enumerate(ws.rows, start=1): + if i == 1: # 第1行是表头 略过 continue if not all([row[x].value for x in range(4)]): summary['invalid'].append(i) + summary['fail'] += 1 continue data = AttrDict( name=row[0].value, hostname=row[1].value, port=row[2].value, username=row[3].value, - desc=row[4].value + desc=row[5].value ) if Host.objects.filter(hostname=data.hostname, port=data.port, username=data.username).exists(): summary['skip'].append(i) + summary['fail'] += 1 continue if Host.objects.filter(name=data.name).exists(): summary['repeat'].append(i) + summary['fail'] += 1 continue host = Host.objects.create(created_by=request.user, **data) host.groups.add(group_id) - summary['success'].append(i) - return json_response(summary) + summary['success'] += 1 + host.password = row[4].value + hosts.append(host) + token = uuid.uuid4().hex + if hosts: + Thread(target=batch_sync_host, args=(token, hosts)).start() + return json_response({'summary': summary, 'token': token, 'hosts': {x.id: {'name': x.name} for x in hosts}}) @auth('host.host.add') diff --git a/spug_web/public/resource/主机导入模板.xlsx b/spug_web/public/resource/主机导入模板.xlsx index 415e78a..4ca6e2f 100644 Binary files a/spug_web/public/resource/主机导入模板.xlsx and b/spug_web/public/resource/主机导入模板.xlsx differ diff --git a/spug_web/src/pages/host/BatchSync.js b/spug_web/src/pages/host/BatchSync.js index 8332c95..8c32b81 100644 --- a/spug_web/src/pages/host/BatchSync.js +++ b/spug_web/src/pages/host/BatchSync.js @@ -3,42 +3,20 @@ * Copyright (c) * Released under the AGPL-3.0 License. */ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { observer } from 'mobx-react'; import { Modal, Form, Input, Button, Radio } from 'antd'; -import { LoadingOutlined } from '@ant-design/icons'; -import { http, X_TOKEN } from 'libs'; +import Sync from './Sync'; +import { http } from 'libs'; import store from './store'; export default observer(function () { const [loading, setLoading] = useState(false); const [password, setPassword] = useState(); const [range, setRange] = useState('2'); - const [hosts, setHosts] = useState({}); + const [hosts, setHosts] = useState(); const [token, setToken] = useState(); - useEffect(() => { - if (token) { - let index = 0; - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const socket = new WebSocket(`${protocol}//${window.location.host}/api/ws/host/${token}/?x-token=${X_TOKEN}`); - socket.onopen = () => socket.send(String(index)); - socket.onmessage = e => { - if (e.data === 'pong') { - socket.send(String(index)) - } else { - index += 1; - const {key, status, message} = JSON.parse(e.data); - hosts[key]['status'] = status; - hosts[key]['message'] = message; - setHosts({...hosts}) - } - } - return () => socket && socket.close() - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [token]) - function handleSubmit() { setLoading(true); http.post('/api/host/valid/', {password, range}) @@ -82,15 +60,9 @@ export default observer(function () { - + {token && hosts ? ( + + ) : null} ); }) diff --git a/spug_web/src/pages/host/Import.js b/spug_web/src/pages/host/Import.js index d58635f..e77c3e6 100644 --- a/spug_web/src/pages/host/Import.js +++ b/spug_web/src/pages/host/Import.js @@ -5,8 +5,9 @@ */ import React, { useState } from 'react'; import { observer } from 'mobx-react'; +import { Modal, Form, Upload, Button, Tooltip, Divider, Cascader, message } from 'antd'; import { UploadOutlined } from '@ant-design/icons'; -import { Modal, Form, Upload, Button, Tooltip, Alert, Cascader, message } from 'antd'; +import Sync from './Sync'; import http from 'libs/http'; import store from './store'; @@ -14,6 +15,9 @@ export default observer(function () { const [loading, setLoading] = useState(false); const [fileList, setFileList] = useState([]); const [groupId, setGroupId] = useState([]); + const [summary, setSummary] = useState({}); + const [token, setToken] = useState(); + const [hosts, setHosts] = useState(); function handleSubmit() { if (groupId.length === 0) return message.error('请选择要导入的分组'); @@ -23,25 +27,9 @@ export default observer(function () { formData.append('group_id', groupId[groupId.length - 1]); http.post('/api/host/import/', formData, {timeout: 120000}) .then(res => { - Modal.info({ - title: '导入结果', - content:
- {res.success.length} - {res['skip'].length > 0 && - {res['skip'].length} - } - {res['invalid'].length > 0 && - {res['invalid'].length} - } - {res['repeat'].length > 0 && - {res['repeat'].length} - } -
, - onOk: () => { - store.fetchRecords(); - store.importVisible = false - } - }) + setToken(res.token) + setHosts(res.hosts) + setSummary(res.summary) }) .finally(() => setLoading(false)) } @@ -54,22 +42,20 @@ export default observer(function () { } } + function handleClose() { + store.importVisible = false; + store.fetchRecords() + } + return ( store.importVisible = false} - confirmLoading={loading} - okButtonProps={{disabled: !fileList.length}} - onOk={handleSubmit}> - -
+ onCancel={handleClose} + footer={null}> + 主机导入模板.xlsx @@ -81,7 +67,7 @@ export default observer(function () { fieldNames={{label: 'title'}} placeholder="请选择"/> - + + + + + + {token && hosts ? ( +
+ 导入结果 +
+
成功:{summary.success}
+
失败:{summary.fail > 0 ? ( + + {summary.skip.map(x =>
第 {x} 行,重复的服务器信息
)} + {summary.repeat.map(x =>
第 {x} 行,重复的主机名称
)} + {summary.invalid.map(x =>
第 {x} 行,无效的数据
)} +
+ )}>{summary.fail} + ) : 0}
+
+ {Object.keys(hosts).length > 0 && ( + <> + 验证及同步 + + + )} + + ) : null}
); }) diff --git a/spug_web/src/pages/host/Sync.js b/spug_web/src/pages/host/Sync.js new file mode 100644 index 0000000..1d7d13c --- /dev/null +++ b/spug_web/src/pages/host/Sync.js @@ -0,0 +1,46 @@ +/** + * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug + * Copyright (c) + * Released under the AGPL-3.0 License. + */ +import React, { useState, useEffect } from 'react'; +import { Form } from 'antd'; +import { LoadingOutlined } from '@ant-design/icons'; +import { X_TOKEN } from 'libs'; +import styles from './index.module.less'; + +export default function (props) { + const [hosts, setHosts] = useState(props.hosts); + + useEffect(() => { + let index = 0; + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const socket = new WebSocket(`${protocol}//${window.location.host}/api/ws/host/${props.token}/?x-token=${X_TOKEN}`); + socket.onopen = () => socket.send(String(index)); + socket.onmessage = e => { + if (e.data === 'pong') { + socket.send(String(index)) + } else { + index += 1; + const {key, status, message} = JSON.parse(e.data); + hosts[key]['status'] = status; + hosts[key]['message'] = message; + setHosts({...hosts}) + } + } + return () => socket && socket.close() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + return ( +
+ {Object.entries(hosts).map(([key, item]) => ( + + {item.status === 'ok' && 成功} + {item.status === 'fail' && 失败} + {item.status === undefined && } + + ))} +
+ ) +} diff --git a/spug_web/src/pages/host/index.module.less b/spug_web/src/pages/host/index.module.less index 30d4517..27c2ccd 100644 --- a/spug_web/src/pages/host/index.module.less +++ b/spug_web/src/pages/host/index.module.less @@ -68,4 +68,17 @@ text-overflow: ellipsis; white-space: nowrap; } +} + +.batchSync { + max-height: calc(100vh - 300px); + overflow: auto; + + :global(.ant-form-item) { + margin-bottom: 4px; + } + + :global(.ant-form-item-extra) { + padding-top: 0; + } } \ No newline at end of file