A 新增主机批量导入功能

pull/103/head
vapao 2020-05-27 13:02:42 +08:00
commit 3dce00995e
9 changed files with 167 additions and 1 deletions

View File

@ -7,4 +7,5 @@ from .views import *
urlpatterns = [ urlpatterns = [
path('', HostView.as_view()), path('', HostView.as_view()),
path('import/', post_import),
] ]

View File

@ -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')

View File

@ -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

34
spug_api/test.py Normal file
View File

@ -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.

View File

@ -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

View File

@ -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>
) )
} }

View File

@ -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>

View File

@ -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;