mirror of https://github.com/openspug/spug
A 新增主机批量导入功能
commit
3dce00995e
|
@ -7,4 +7,5 @@ from .views import *
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', HostView.as_view()),
|
path('', HostView.as_view()),
|
||||||
|
path('import/', post_import),
|
||||||
]
|
]
|
||||||
|
|
|
@ -10,7 +10,8 @@ from apps.app.models import Deploy
|
||||||
from apps.schedule.models import Task
|
from apps.schedule.models import Task
|
||||||
from apps.monitor.models import Detection
|
from apps.monitor.models import Detection
|
||||||
from libs.ssh import SSH, AuthenticationException
|
from libs.ssh import SSH, AuthenticationException
|
||||||
from libs import human_datetime
|
from libs import human_datetime, AttrDict
|
||||||
|
from openpyxl import load_workbook
|
||||||
|
|
||||||
|
|
||||||
class HostView(View):
|
class HostView(View):
|
||||||
|
@ -69,6 +70,38 @@ class HostView(View):
|
||||||
return json_response(error=error)
|
return json_response(error=error)
|
||||||
|
|
||||||
|
|
||||||
|
def post_import(request):
|
||||||
|
password = request.POST.get('password')
|
||||||
|
file = request.FILES['file']
|
||||||
|
ws = load_workbook(file, read_only=True)['Sheet1']
|
||||||
|
summary = {'invalid': [], 'skip': [], 'fail': [], 'success': []}
|
||||||
|
for i, row in enumerate(ws.rows):
|
||||||
|
if i == 0: # 第1行是表头 略过
|
||||||
|
continue
|
||||||
|
if not all([row[x].value for x in range(5)]):
|
||||||
|
summary['invalid'].append(i)
|
||||||
|
continue
|
||||||
|
data = AttrDict(
|
||||||
|
zone=row[0].value,
|
||||||
|
name=row[1].value,
|
||||||
|
hostname=row[2].value,
|
||||||
|
port=row[3].value,
|
||||||
|
username=row[4].value,
|
||||||
|
password=row[5].value,
|
||||||
|
desc=row[6].value
|
||||||
|
)
|
||||||
|
if Host.objects.filter(hostname=data.hostname, port=data.port, username=data.username,
|
||||||
|
deleted_by_id__isnull=True).exists():
|
||||||
|
summary['skip'].append(i)
|
||||||
|
continue
|
||||||
|
if valid_ssh(data.hostname, data.port, data.username, data.pop('password') or password) is False:
|
||||||
|
summary['fail'].append(i)
|
||||||
|
continue
|
||||||
|
Host.objects.create(created_by=request.user, **data)
|
||||||
|
summary['success'].append(i)
|
||||||
|
return json_response(summary)
|
||||||
|
|
||||||
|
|
||||||
def valid_ssh(hostname, port, username, password):
|
def valid_ssh(hostname, port, username, password):
|
||||||
try:
|
try:
|
||||||
private_key = AppSetting.get('private_key')
|
private_key = AppSetting.get('private_key')
|
||||||
|
|
|
@ -7,3 +7,4 @@ django-redis==4.10.0
|
||||||
requests==2.22.0
|
requests==2.22.0
|
||||||
GitPython==3.0.8
|
GitPython==3.0.8
|
||||||
python-ldap==3.2.0
|
python-ldap==3.2.0
|
||||||
|
openpyxl==3.0.3
|
|
@ -0,0 +1,34 @@
|
||||||
|
from email.message import EmailMessage
|
||||||
|
from email.utils import formataddr
|
||||||
|
import smtplib
|
||||||
|
|
||||||
|
|
||||||
|
class Mail:
|
||||||
|
def __init__(self, server, port, username, password, nickname=None):
|
||||||
|
self.host = server
|
||||||
|
self.port = port
|
||||||
|
self.user = username
|
||||||
|
self.password = password
|
||||||
|
self.nickname = nickname
|
||||||
|
|
||||||
|
def _get_server(self):
|
||||||
|
print(self.host, self.port, self.user, self.password)
|
||||||
|
server = smtplib.SMTP_SSL(self.host, self.port)
|
||||||
|
server.login(self.user, self.password)
|
||||||
|
return server
|
||||||
|
|
||||||
|
def send_text_mail(self, to_addrs, subject, body):
|
||||||
|
if isinstance(to_addrs, (list, tuple)):
|
||||||
|
to_addrs = ', '.join(to_addrs)
|
||||||
|
server = self._get_server()
|
||||||
|
msg = EmailMessage()
|
||||||
|
msg.set_content(body)
|
||||||
|
msg['Subject'] = subject
|
||||||
|
msg['From'] = formataddr((self.nickname, self.user)) if self.nickname else self.user
|
||||||
|
msg['To'] = to_addrs
|
||||||
|
server.send_message(msg)
|
||||||
|
server.quit()
|
||||||
|
|
||||||
|
mail_service = {'server': 'smtp.163.com', 'port': '465', 'username': 'leiem1989@163.com', 'password': 'FOCCUIGOCTJGCBOB', 'nickname': 'spug'}
|
||||||
|
mail = Mail(**mail_service)
|
||||||
|
mail.send_text_mail({'live1989@foxmail.com'}, '恢复-官网检测', f'恢复-官网检测\r\n\r\n自动发送,请勿回复。')
|
Binary file not shown.
|
@ -0,0 +1,92 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
|
||||||
|
* Copyright (c) <spug.dev@gmail.com>
|
||||||
|
* Released under the MIT License.
|
||||||
|
*/
|
||||||
|
import React from 'react';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import { Modal, Form, Input, Upload, Icon, Button, Tooltip, Alert } from 'antd';
|
||||||
|
import http from 'libs/http';
|
||||||
|
import store from './store';
|
||||||
|
|
||||||
|
@observer
|
||||||
|
class ComImport extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
loading: false,
|
||||||
|
password: null,
|
||||||
|
fileList: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit = () => {
|
||||||
|
this.setState({loading: true});
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', this.state.fileList[0]);
|
||||||
|
formData.append('password', this.state.password);
|
||||||
|
http.post('/api/host/import/', formData)
|
||||||
|
.then(res => {
|
||||||
|
Modal.info({
|
||||||
|
title: '导入结果',
|
||||||
|
content: <Form labelCol={{span: 5}} wrapperCol={{span: 14}}>
|
||||||
|
<Form.Item style={{margin: 0}} label="导入成功">{res.success.length}</Form.Item>
|
||||||
|
{res['fail'].length > 0 && <Form.Item style={{margin: 0, color: '#1890ff'}} label="验证失败">
|
||||||
|
<Tooltip title={`相关行:${res['fail'].join(', ')}`}>{res['fail'].length}</Tooltip>
|
||||||
|
</Form.Item>}
|
||||||
|
{res['skip'].length > 0 && <Form.Item style={{margin: 0, color: '#1890ff'}} label="重复数据">
|
||||||
|
<Tooltip title={`相关行:${res['skip'].join(', ')}`}>{res['skip'].length}</Tooltip>
|
||||||
|
</Form.Item>}
|
||||||
|
{res['invalid'].length > 0 && <Form.Item style={{margin: 0, color: '#1890ff'}} label="无效数据">
|
||||||
|
<Tooltip title={`相关行:${res['invalid'].join(', ')}`}>{res['invalid'].length}</Tooltip>
|
||||||
|
</Form.Item>}
|
||||||
|
</Form>
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.finally(() => this.setState({loading: false}))
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeUpload = (file) => {
|
||||||
|
this.setState({fileList: [file]});
|
||||||
|
return false
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible
|
||||||
|
width={800}
|
||||||
|
maskClosable={false}
|
||||||
|
title="批量导入"
|
||||||
|
okText="导入"
|
||||||
|
onCancel={() => store.importVisible = false}
|
||||||
|
confirmLoading={this.state.loading}
|
||||||
|
okButtonProps={{disabled: !this.state.fileList.length}}
|
||||||
|
onOk={this.handleSubmit}>
|
||||||
|
<Alert closable showIcon type="info" message={null}
|
||||||
|
style={{width: 600, margin: '0 auto 20px', color: '#31708f !important'}}
|
||||||
|
description="导入或输入的密码仅作首次验证使用,并不会存储密码。"/>
|
||||||
|
<Form labelCol={{span: 6}} wrapperCol={{span: 14}}>
|
||||||
|
<Form.Item label="模板下载" help="请下载使用该模板填充数据后导入">
|
||||||
|
<a href="/resource/主机导入模板.xlsx">主机导入模板.xlsx</a>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="默认密码" help="如果excel中密码为空则使用该密码">
|
||||||
|
<Input
|
||||||
|
value={this.state.password}
|
||||||
|
onChange={e => this.setState({password: e.target.value})}
|
||||||
|
placeholder="请输入默认主机密码"/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item required label="导入数据">
|
||||||
|
<Upload name="file" accept=".xls, .xlsx" fileList={this.state.fileList} beforeUpload={this.beforeUpload}>
|
||||||
|
<Button>
|
||||||
|
<Icon type="upload"/> 点击上传
|
||||||
|
</Button>
|
||||||
|
</Upload>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ComImport
|
|
@ -8,6 +8,7 @@ import { observer } from 'mobx-react';
|
||||||
import { Table, Divider, Modal, message } from 'antd';
|
import { Table, Divider, Modal, message } from 'antd';
|
||||||
import { LinkButton } from 'components';
|
import { LinkButton } from 'components';
|
||||||
import ComForm from './Form';
|
import ComForm from './Form';
|
||||||
|
import ComImport from './Import';
|
||||||
import http from 'libs/http';
|
import http from 'libs/http';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
|
|
||||||
|
@ -85,6 +86,7 @@ class ComTable extends React.Component {
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Table rowKey="id" loading={store.isFetching} dataSource={data} columns={this.columns}/>
|
<Table rowKey="id" loading={store.isFetching} dataSource={data} columns={this.columns}/>
|
||||||
{store.formVisible && <ComForm/>}
|
{store.formVisible && <ComForm/>}
|
||||||
|
{store.importVisible && <ComImport/>}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,8 @@ export default observer(function () {
|
||||||
</SearchForm>
|
</SearchForm>
|
||||||
<AuthDiv auth="host.host.add" style={{marginBottom: 16}}>
|
<AuthDiv auth="host.host.add" style={{marginBottom: 16}}>
|
||||||
<Button type="primary" icon="plus" onClick={() => store.showForm()}>新建</Button>
|
<Button type="primary" icon="plus" onClick={() => store.showForm()}>新建</Button>
|
||||||
|
<Button style={{marginLeft: 20}} type="primary" icon="import"
|
||||||
|
onClick={() => store.importVisible = true}>批量导入</Button>
|
||||||
</AuthDiv>
|
</AuthDiv>
|
||||||
<ComTable/>
|
<ComTable/>
|
||||||
</AuthCard>
|
</AuthCard>
|
||||||
|
|
|
@ -13,6 +13,7 @@ class Store {
|
||||||
@observable idMap = {};
|
@observable idMap = {};
|
||||||
@observable isFetching = false;
|
@observable isFetching = false;
|
||||||
@observable formVisible = false;
|
@observable formVisible = false;
|
||||||
|
@observable importVisible = false;
|
||||||
|
|
||||||
@observable f_name;
|
@observable f_name;
|
||||||
@observable f_zone;
|
@observable f_zone;
|
||||||
|
|
Loading…
Reference in New Issue