mirror of https://github.com/openspug/spug
				
				
				
			A 新增主机批量导入功能
						commit
						3dce00995e
					
				| 
						 | 
				
			
			@ -7,4 +7,5 @@ from .views import *
 | 
			
		|||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    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.monitor.models import Detection
 | 
			
		||||
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):
 | 
			
		||||
| 
						 | 
				
			
			@ -69,6 +70,38 @@ class HostView(View):
 | 
			
		|||
        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):
 | 
			
		||||
    try:
 | 
			
		||||
        private_key = AppSetting.get('private_key')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,3 +7,4 @@ django-redis==4.10.0
 | 
			
		|||
requests==2.22.0
 | 
			
		||||
GitPython==3.0.8
 | 
			
		||||
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 { LinkButton } from 'components';
 | 
			
		||||
import ComForm from './Form';
 | 
			
		||||
import ComImport from './Import';
 | 
			
		||||
import http from 'libs/http';
 | 
			
		||||
import store from './store';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -85,6 +86,7 @@ class ComTable extends React.Component {
 | 
			
		|||
      <React.Fragment>
 | 
			
		||||
        <Table rowKey="id" loading={store.isFetching} dataSource={data} columns={this.columns}/>
 | 
			
		||||
        {store.formVisible && <ComForm/>}
 | 
			
		||||
        {store.importVisible && <ComImport/>}
 | 
			
		||||
      </React.Fragment>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -33,6 +33,8 @@ export default observer(function () {
 | 
			
		|||
      </SearchForm>
 | 
			
		||||
      <AuthDiv auth="host.host.add" style={{marginBottom: 16}}>
 | 
			
		||||
        <Button type="primary" icon="plus" onClick={() => store.showForm()}>新建</Button>
 | 
			
		||||
        <Button style={{marginLeft: 20}} type="primary" icon="import"
 | 
			
		||||
                onClick={() => store.importVisible = true}>批量导入</Button>
 | 
			
		||||
      </AuthDiv>
 | 
			
		||||
      <ComTable/>
 | 
			
		||||
    </AuthCard>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,6 +13,7 @@ class Store {
 | 
			
		|||
  @observable idMap = {};
 | 
			
		||||
  @observable isFetching = false;
 | 
			
		||||
  @observable formVisible = false;
 | 
			
		||||
  @observable importVisible = false;
 | 
			
		||||
 | 
			
		||||
  @observable f_name;
 | 
			
		||||
  @observable f_zone;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue