mirror of https://github.com/openspug/spug
				
				
				
			upgrade host module
							parent
							
								
									e9c0a04218
								
							
						
					
					
						commit
						ef84cf6bff
					
				| 
						 | 
				
			
			@ -0,0 +1,81 @@
 | 
			
		|||
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
 | 
			
		||||
# Copyright: (c) <spug.dev@gmail.com>
 | 
			
		||||
# Released under the AGPL-3.0 License.
 | 
			
		||||
from django.views.generic import View
 | 
			
		||||
from libs import json_response, JsonParser, Argument, human_datetime
 | 
			
		||||
from apps.host.models import Host, HostExtend
 | 
			
		||||
from apps.host.utils import check_os_type
 | 
			
		||||
import ipaddress
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ExtendView(View):
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        form, error = JsonParser(
 | 
			
		||||
            Argument('host_id', type=int, help='参数错误')
 | 
			
		||||
        ).parse(request.GET)
 | 
			
		||||
        if error is None:
 | 
			
		||||
            host = Host.objects.filter(pk=form.host_id).first()
 | 
			
		||||
            if not host:
 | 
			
		||||
                return json_response(error='未找到指定主机')
 | 
			
		||||
            if not host.is_verified:
 | 
			
		||||
                return json_response(error='该主机还未验证')
 | 
			
		||||
            cli = host.get_ssh()
 | 
			
		||||
            commands = [
 | 
			
		||||
                "lscpu | grep '^CPU(s)' | awk '{print $2}'",
 | 
			
		||||
                "free -m | awk 'NR==2{print $2}'",
 | 
			
		||||
                "hostname -I",
 | 
			
		||||
                "cat /etc/os-release | grep PRETTY_NAME | awk -F \\\" '{print $2}'",
 | 
			
		||||
                "fdisk -l | grep '^Disk /' | awk '{print $5}'"
 | 
			
		||||
            ]
 | 
			
		||||
            code, out = cli.exec_command(';'.join(commands))
 | 
			
		||||
            if code != 0:
 | 
			
		||||
                return json_response(error=f'Exception: {out}')
 | 
			
		||||
            response = {'disk': [], 'public_ip_address': [], 'private_ip_address': []}
 | 
			
		||||
            for index, line in enumerate(out.strip().split('\n')):
 | 
			
		||||
                if index == 0:
 | 
			
		||||
                    response['cpu'] = int(line)
 | 
			
		||||
                elif index == 1:
 | 
			
		||||
                    response['memory'] = round(int(line) / 1000, 1)
 | 
			
		||||
                elif index == 2:
 | 
			
		||||
                    for ip in line.split():
 | 
			
		||||
                        if ipaddress.ip_address(ip).is_global:
 | 
			
		||||
                            response['public_ip_address'].append(ip)
 | 
			
		||||
                        else:
 | 
			
		||||
                            response['private_ip_address'].append(ip)
 | 
			
		||||
                elif index == 3:
 | 
			
		||||
                    response['os_name'] = line
 | 
			
		||||
                else:
 | 
			
		||||
                    response['disk'].append(round(int(line) / 1024 / 1024 / 1024, 0))
 | 
			
		||||
            return json_response(response)
 | 
			
		||||
        return json_response(error=error)
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        form, error = JsonParser(
 | 
			
		||||
            Argument('host_id', type=int, help='参数错误'),
 | 
			
		||||
            Argument('instance_id', required=False),
 | 
			
		||||
            Argument('os_name', help='请输入操作系统'),
 | 
			
		||||
            Argument('cpu', type=int, help='请输入CPU核心数'),
 | 
			
		||||
            Argument('memory', type=float, help='请输入内存大小'),
 | 
			
		||||
            Argument('disk', type=list, filter=lambda x: len(x), help='请添加磁盘'),
 | 
			
		||||
            Argument('private_ip_address', type=list, filter=lambda x: len(x), help='请添加内网IP'),
 | 
			
		||||
            Argument('public_ip_address', type=list, required=False),
 | 
			
		||||
            Argument('instance_charge_type', default='Other'),
 | 
			
		||||
            Argument('internet_charge_type', default='Other'),
 | 
			
		||||
            Argument('created_time', required=False),
 | 
			
		||||
            Argument('expired_time', required=False)
 | 
			
		||||
        ).parse(request.body)
 | 
			
		||||
        if error is None:
 | 
			
		||||
            host = Host.objects.filter(pk=form.host_id).first()
 | 
			
		||||
            form.disk = json.dumps(form.disk)
 | 
			
		||||
            form.public_ip_address = json.dumps(form.public_ip_address) if form.public_ip_address else '[]'
 | 
			
		||||
            form.private_ip_address = json.dumps(form.private_ip_address)
 | 
			
		||||
            form.updated_at = human_datetime()
 | 
			
		||||
            form.os_type = check_os_type(form.os_name)
 | 
			
		||||
            if hasattr(host, 'hostextend'):
 | 
			
		||||
                extend = host.hostextend
 | 
			
		||||
                extend.update_by_dict(form)
 | 
			
		||||
            else:
 | 
			
		||||
                extend = HostExtend.objects.create(host=host, **form)
 | 
			
		||||
            return json_response(extend.to_view())
 | 
			
		||||
        return json_response(error=error)
 | 
			
		||||
| 
						 | 
				
			
			@ -46,7 +46,7 @@ class Host(models.Model, ModelMixin):
 | 
			
		|||
class HostExtend(models.Model, ModelMixin):
 | 
			
		||||
    INSTANCE_CHARGE_TYPES = (
 | 
			
		||||
        ('PrePaid', '包年包月'),
 | 
			
		||||
        ('PostPaid', '按量付费'),
 | 
			
		||||
        ('PostPaid', '按量计费'),
 | 
			
		||||
        ('Other', '其他')
 | 
			
		||||
    )
 | 
			
		||||
    INTERNET_CHARGE_TYPES = (
 | 
			
		||||
| 
						 | 
				
			
			@ -55,8 +55,8 @@ class HostExtend(models.Model, ModelMixin):
 | 
			
		|||
        ('Other', '其他')
 | 
			
		||||
    )
 | 
			
		||||
    host = models.OneToOneField(Host, on_delete=models.CASCADE)
 | 
			
		||||
    instance_id = models.CharField(max_length=64)
 | 
			
		||||
    zone_id = models.CharField(max_length=30)
 | 
			
		||||
    instance_id = models.CharField(max_length=64, null=True)
 | 
			
		||||
    zone_id = models.CharField(max_length=30, null=True)
 | 
			
		||||
    cpu = models.IntegerField()
 | 
			
		||||
    memory = models.FloatField()
 | 
			
		||||
    disk = models.CharField(max_length=255, default='[]')
 | 
			
		||||
| 
						 | 
				
			
			@ -66,7 +66,7 @@ class HostExtend(models.Model, ModelMixin):
 | 
			
		|||
    public_ip_address = models.CharField(max_length=255)
 | 
			
		||||
    instance_charge_type = models.CharField(max_length=20, choices=INSTANCE_CHARGE_TYPES)
 | 
			
		||||
    internet_charge_type = models.CharField(max_length=20, choices=INTERNET_CHARGE_TYPES)
 | 
			
		||||
    created_time = models.CharField(max_length=20)
 | 
			
		||||
    created_time = models.CharField(max_length=20, null=True)
 | 
			
		||||
    expired_time = models.CharField(max_length=20, null=True)
 | 
			
		||||
    updated_at = models.CharField(max_length=20, default=human_datetime)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,10 +5,12 @@ from django.urls import path
 | 
			
		|||
 | 
			
		||||
from apps.host.views import *
 | 
			
		||||
from apps.host.group import GroupView
 | 
			
		||||
from apps.host.extend import ExtendView
 | 
			
		||||
from apps.host.add import get_regions, cloud_import
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path('', HostView.as_view()),
 | 
			
		||||
    path('extend/', ExtendView.as_view()),
 | 
			
		||||
    path('group/', GroupView.as_view()),
 | 
			
		||||
    path('import/', post_import),
 | 
			
		||||
    path('import/cloud/', cloud_import),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -51,11 +51,14 @@ class HostView(View):
 | 
			
		|||
            if other and (not form.id or other.id != form.id):
 | 
			
		||||
                return json_response(error=f'已存在的主机名称【{form.name}】')
 | 
			
		||||
            if form.id:
 | 
			
		||||
                Host.objects.filter(pk=form.id).update(**form)
 | 
			
		||||
                Host.objects.filter(pk=form.id).update(is_verified=True, **form)
 | 
			
		||||
                host = Host.objects.get(pk=form.id)
 | 
			
		||||
            else:
 | 
			
		||||
                host = Host.objects.create(created_by=request.user, **form)
 | 
			
		||||
                host = Host.objects.create(created_by=request.user, is_verified=True, **form)
 | 
			
		||||
            host.groups.set(group_ids)
 | 
			
		||||
            response = host.to_view()
 | 
			
		||||
            response['group_ids'] = group_ids
 | 
			
		||||
            return json_response(response)
 | 
			
		||||
        return json_response(error=error)
 | 
			
		||||
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
| 
						 | 
				
			
			@ -91,7 +94,6 @@ class HostView(View):
 | 
			
		|||
            if detection:
 | 
			
		||||
                return json_response(error=f'监控中心的任务【{detection.name}】关联了该主机,请解除关联后再尝试删除该主机')
 | 
			
		||||
            Host.objects.filter(pk=form.id).delete()
 | 
			
		||||
            print('pk: ', form.id)
 | 
			
		||||
        return json_response(error=error)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -143,13 +145,7 @@ def post_import(request):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
def valid_ssh(hostname, port, username, password=None, pkey=None, with_expect=True):
 | 
			
		||||
    try:
 | 
			
		||||
        private_key = AppSetting.get('private_key')
 | 
			
		||||
        public_key = AppSetting.get('public_key')
 | 
			
		||||
    except KeyError:
 | 
			
		||||
        private_key, public_key = SSH.generate_key()
 | 
			
		||||
        AppSetting.set('private_key', private_key, 'ssh private key')
 | 
			
		||||
        AppSetting.set('public_key', public_key, 'ssh public key')
 | 
			
		||||
    private_key, public_key = AppSetting.get_ssh_key()
 | 
			
		||||
    if password:
 | 
			
		||||
        _cli = SSH(hostname, port, username, password=str(password))
 | 
			
		||||
        _cli.add_public_key(public_key)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,7 @@
 | 
			
		|||
# Released under the AGPL-3.0 License.
 | 
			
		||||
from functools import lru_cache
 | 
			
		||||
from apps.setting.models import Setting, KEYS_DEFAULT
 | 
			
		||||
from libs.ssh import SSH
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -29,3 +30,13 @@ class AppSetting:
 | 
			
		|||
            Setting.objects.update_or_create(key=key, defaults={'value': value, 'desc': desc})
 | 
			
		||||
        else:
 | 
			
		||||
            raise KeyError('invalid key')
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def get_ssh_key(cls):
 | 
			
		||||
        public_key = cls.get_default('public_key')
 | 
			
		||||
        private_key = cls.get_default('private_key')
 | 
			
		||||
        if not private_key or not public_key:
 | 
			
		||||
            private_key, public_key = SSH.generate_key()
 | 
			
		||||
            cls.set('private_key', private_key)
 | 
			
		||||
            cls.set('public_key', public_key)
 | 
			
		||||
        return private_key, public_key
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,6 +18,11 @@ class ModelMixin(object):
 | 
			
		|||
        else:
 | 
			
		||||
            return {f.attname: getattr(self, f.attname) for f in self._meta.fields}
 | 
			
		||||
 | 
			
		||||
    def update_by_dict(self, data):
 | 
			
		||||
        for key, value in data.items():
 | 
			
		||||
            setattr(self, key, value)
 | 
			
		||||
        self.save()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# 使用该混入类,需要request.user对象实现has_perms方法
 | 
			
		||||
class PermissionMixin(object):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,19 +1,117 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import React, { useState, useEffect, useRef } from 'react';
 | 
			
		||||
import { observer } from 'mobx-react';
 | 
			
		||||
import { Drawer, Descriptions, List } from 'antd';
 | 
			
		||||
import { Drawer, Descriptions, List, Button, Input, Select, DatePicker, Tag, message } from 'antd';
 | 
			
		||||
import { EditOutlined, SaveOutlined, PlusOutlined, SyncOutlined } from '@ant-design/icons';
 | 
			
		||||
import { http } from 'libs';
 | 
			
		||||
import store from './store';
 | 
			
		||||
import lds from 'lodash';
 | 
			
		||||
import moment from 'moment';
 | 
			
		||||
import styles from './index.module.less';
 | 
			
		||||
 | 
			
		||||
export default observer(function () {
 | 
			
		||||
  const host = store.record;
 | 
			
		||||
  const group_ids = host.group_ids || [];
 | 
			
		||||
  const [edit, setEdit] = useState(false);
 | 
			
		||||
  const [host, setHost] = useState(store.record);
 | 
			
		||||
  const diskInput = useRef();
 | 
			
		||||
  const sipInput = useRef();
 | 
			
		||||
  const gipInput = useRef();
 | 
			
		||||
  const [tag, setTag] = useState();
 | 
			
		||||
  const [inputVisible, setInputVisible] = useState(null);
 | 
			
		||||
  const [loading, setLoading] = useState(false);
 | 
			
		||||
  const [fetching, setFetching] = useState(false);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (store.detailVisible) {
 | 
			
		||||
      setHost(lds.cloneDeep(store.record))
 | 
			
		||||
    }
 | 
			
		||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
  }, [store.detailVisible])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (inputVisible === 'disk') {
 | 
			
		||||
      diskInput.current.focus()
 | 
			
		||||
    } else if (inputVisible === 'sip') {
 | 
			
		||||
      sipInput.current.focus()
 | 
			
		||||
    } else if (inputVisible === 'gip') {
 | 
			
		||||
      gipInput.current.focus()
 | 
			
		||||
    }
 | 
			
		||||
  }, [inputVisible])
 | 
			
		||||
 | 
			
		||||
  function handleSubmit() {
 | 
			
		||||
    setLoading(true)
 | 
			
		||||
    if (host.created_time) host.created_time = moment(host.created_time).format('YYYY-MM-DD HH:mm:ss')
 | 
			
		||||
    if (host.expired_time) host.expired_time = moment(host.expired_time).format('YYYY-MM-DD HH:mm:ss')
 | 
			
		||||
    http.post('/api/host/extend/', {host_id: host.id, ...host})
 | 
			
		||||
      .then(res => {
 | 
			
		||||
        Object.assign(host, res);
 | 
			
		||||
        setEdit(false);
 | 
			
		||||
        setHost(lds.cloneDeep(host));
 | 
			
		||||
        store.fetchRecords()
 | 
			
		||||
      })
 | 
			
		||||
      .finally(() => setLoading(false))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function handleFetch() {
 | 
			
		||||
    setFetching(true);
 | 
			
		||||
    http.get('/api/host/extend/', {params: {host_id: host.id}})
 | 
			
		||||
      .then(res => {
 | 
			
		||||
        Object.assign(host, res);
 | 
			
		||||
        setHost(lds.cloneDeep(host));
 | 
			
		||||
        message.success('同步成功')
 | 
			
		||||
      })
 | 
			
		||||
      .finally(() => setFetching(false))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function handleChange(e, key) {
 | 
			
		||||
    host[key] = e && e.target ? e.target.value : e;
 | 
			
		||||
    setHost({...host})
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function handleClose() {
 | 
			
		||||
    store.detailVisible = false;
 | 
			
		||||
    setEdit(false)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function handleTagConfirm(key) {
 | 
			
		||||
    if (tag) {
 | 
			
		||||
      if (key === 'disk') {
 | 
			
		||||
        const value = Number(tag);
 | 
			
		||||
        if (lds.isNaN(value)) return message.error('请输入数字');
 | 
			
		||||
        host.disk ? host.disk.push(value) : host.disk = [value]
 | 
			
		||||
      } else if (key === 'sip') {
 | 
			
		||||
        host.private_ip_address ? host.private_ip_address.push(tag) : host.private_ip_address = [tag]
 | 
			
		||||
      } else if (key === 'gip') {
 | 
			
		||||
        host.public_ip_address ? host.public_ip_address.push(tag) : host.public_ip_address = [tag]
 | 
			
		||||
      }
 | 
			
		||||
      setHost(lds.cloneDeep(host))
 | 
			
		||||
    }
 | 
			
		||||
    setTag(undefined);
 | 
			
		||||
    setInputVisible(false)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function handleTagRemove(key, index) {
 | 
			
		||||
    if (key === 'disk') {
 | 
			
		||||
      host.disk.splice(index, 1)
 | 
			
		||||
    } else if (key === 'sip') {
 | 
			
		||||
      host.private_ip_address.splice(index, 1)
 | 
			
		||||
    } else if (key === 'gip') {
 | 
			
		||||
      host.public_ip_address.splice(index, 1)
 | 
			
		||||
    }
 | 
			
		||||
    setHost(lds.cloneDeep(host))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Drawer
 | 
			
		||||
      width={500}
 | 
			
		||||
      width={550}
 | 
			
		||||
      title={host.name}
 | 
			
		||||
      placement="right"
 | 
			
		||||
      onClose={() => store.detailVisible = false}
 | 
			
		||||
      onClose={handleClose}
 | 
			
		||||
      visible={store.detailVisible}>
 | 
			
		||||
      <Descriptions bordered size="small" title={<span style={{fontWeight: 500}}>基本信息</span>} column={1}>
 | 
			
		||||
      <Descriptions
 | 
			
		||||
        bordered
 | 
			
		||||
        size="small"
 | 
			
		||||
        labelStyle={{width: 150}}
 | 
			
		||||
        title={<span style={{fontWeight: 500}}>基本信息</span>}
 | 
			
		||||
        column={1}>
 | 
			
		||||
        <Descriptions.Item label="主机名称">{host.name}</Descriptions.Item>
 | 
			
		||||
        <Descriptions.Item label="连接地址">{host.username}@{host.hostname}</Descriptions.Item>
 | 
			
		||||
        <Descriptions.Item label="连接端口">{host.port}</Descriptions.Item>
 | 
			
		||||
| 
						 | 
				
			
			@ -21,34 +119,148 @@ export default observer(function () {
 | 
			
		|||
        <Descriptions.Item label="描述信息">{host.desc}</Descriptions.Item>
 | 
			
		||||
        <Descriptions.Item label="所属分组">
 | 
			
		||||
          <List>
 | 
			
		||||
            {group_ids.map(g_id => (
 | 
			
		||||
            {lds.get(host, 'group_ids', []).map(g_id => (
 | 
			
		||||
              <List.Item key={g_id} style={{padding: '6px 0'}}>{store.groups[g_id]}</List.Item>
 | 
			
		||||
            ))}
 | 
			
		||||
          </List>
 | 
			
		||||
        </Descriptions.Item>
 | 
			
		||||
      </Descriptions>
 | 
			
		||||
      {host.id ? (
 | 
			
		||||
        <Descriptions
 | 
			
		||||
          bordered
 | 
			
		||||
          size="small"
 | 
			
		||||
          column={1}
 | 
			
		||||
          style={{marginTop: 24}}
 | 
			
		||||
          title={<span style={{fontWeight: 500}}>扩展信息</span>}>
 | 
			
		||||
          <Descriptions.Item label="实例ID">{host.instance_id}</Descriptions.Item>
 | 
			
		||||
          <Descriptions.Item label="操作系统">{host.os_name}</Descriptions.Item>
 | 
			
		||||
          <Descriptions.Item label="CPU">{host.cpu}核</Descriptions.Item>
 | 
			
		||||
          <Descriptions.Item label="内存">{host.memory}GB</Descriptions.Item>
 | 
			
		||||
          <Descriptions.Item label="磁盘">{host.disk.map(x => `${x}GB`).join(', ')}</Descriptions.Item>
 | 
			
		||||
          <Descriptions.Item label="内网IP">{host.private_ip_address.join(', ')}</Descriptions.Item>
 | 
			
		||||
          <Descriptions.Item label="公网IP">{host.public_ip_address.join(', ')}</Descriptions.Item>
 | 
			
		||||
          <Descriptions.Item label="实例付费方式">{host.instance_charge_type_alias}</Descriptions.Item>
 | 
			
		||||
          <Descriptions.Item label="网络付费方式">{host.internet_charge_type_alisa}</Descriptions.Item>
 | 
			
		||||
          <Descriptions.Item label="创建时间">{host.created_time}</Descriptions.Item>
 | 
			
		||||
          <Descriptions.Item label="到期时间">{host.expired_time || 'N/A'}</Descriptions.Item>
 | 
			
		||||
          <Descriptions.Item label="更新时间">{host.updated_at}</Descriptions.Item>
 | 
			
		||||
        </Descriptions>
 | 
			
		||||
      ) : null}
 | 
			
		||||
 | 
			
		||||
      <Descriptions
 | 
			
		||||
        bordered
 | 
			
		||||
        size="small"
 | 
			
		||||
        column={1}
 | 
			
		||||
        className={edit ? styles.hostExtendEdit : null}
 | 
			
		||||
        labelStyle={{width: 150}}
 | 
			
		||||
        style={{marginTop: 24}}
 | 
			
		||||
        extra={edit ? ([
 | 
			
		||||
          <Button key="1" type="link" loading={fetching} icon={<SyncOutlined/>} onClick={handleFetch}>同步</Button>,
 | 
			
		||||
          <Button key="2" type="link" loading={loading} icon={<SaveOutlined/>} onClick={handleSubmit}>保存</Button>
 | 
			
		||||
        ]) : (
 | 
			
		||||
          <Button type="link" icon={<EditOutlined/>} onClick={() => setEdit(true)}>编辑</Button>
 | 
			
		||||
        )}
 | 
			
		||||
        title={<span style={{fontWeight: 500}}>扩展信息</span>}>
 | 
			
		||||
        <Descriptions.Item label="实例ID">
 | 
			
		||||
          {edit ? (
 | 
			
		||||
            <Input value={host.instance_id} onChange={e => handleChange(e, 'instance_id')} placeholder="选填"/>
 | 
			
		||||
          ) : host.instance_id}
 | 
			
		||||
        </Descriptions.Item>
 | 
			
		||||
        <Descriptions.Item label="操作系统">
 | 
			
		||||
          {edit ? (
 | 
			
		||||
            <Input value={host.os_name} onChange={e => handleChange(e, 'os_name')}
 | 
			
		||||
                   placeholder="例如:Ubuntu Server 16.04.1 LTS"/>
 | 
			
		||||
          ) : host.os_name}
 | 
			
		||||
        </Descriptions.Item>
 | 
			
		||||
        <Descriptions.Item label="CPU">
 | 
			
		||||
          {edit ? (
 | 
			
		||||
            <Input suffix="核" style={{width: 100}} value={host.cpu} onChange={e => handleChange(e, 'cpu')}
 | 
			
		||||
                   placeholder="数字"/>
 | 
			
		||||
          ) : host.cpu ? `${host.cpu}核` : null}
 | 
			
		||||
        </Descriptions.Item>
 | 
			
		||||
        <Descriptions.Item label="内存">
 | 
			
		||||
          {edit ? (
 | 
			
		||||
            <Input suffix="GB" style={{width: 100}} value={host.memory} onChange={e => handleChange(e, 'memory')}
 | 
			
		||||
                   placeholder="数字"/>
 | 
			
		||||
          ) : host.memory ? `${host.memory}GB` : null}
 | 
			
		||||
        </Descriptions.Item>
 | 
			
		||||
        <Descriptions.Item label="磁盘">
 | 
			
		||||
          {lds.get(host, 'disk', []).map((item, index) => (
 | 
			
		||||
            <Tag visible closable={edit} key={index} onClose={() => handleTagRemove('disk', index)}>{item}GB</Tag>
 | 
			
		||||
          ))}
 | 
			
		||||
          {edit && (inputVisible === 'disk' ? (
 | 
			
		||||
            <Input
 | 
			
		||||
              ref={diskInput}
 | 
			
		||||
              type="text"
 | 
			
		||||
              size="small"
 | 
			
		||||
              value={tag}
 | 
			
		||||
              className={styles.tagNumberInput}
 | 
			
		||||
              onChange={e => setTag(e.target.value)}
 | 
			
		||||
              onBlur={() => handleTagConfirm('disk')}
 | 
			
		||||
              onPressEnter={() => handleTagConfirm('disk')}
 | 
			
		||||
            />
 | 
			
		||||
          ) : (
 | 
			
		||||
            <Tag className={styles.tagAdd} onClick={() => setInputVisible('disk')}><PlusOutlined/> 新建</Tag>
 | 
			
		||||
          ))}
 | 
			
		||||
        </Descriptions.Item>
 | 
			
		||||
        <Descriptions.Item label="内网IP">
 | 
			
		||||
          {lds.get(host, 'private_ip_address', []).map((item, index) => (
 | 
			
		||||
            <Tag visible closable={edit} key={index} onClose={() => handleTagRemove('sip', index)}>{item}</Tag>
 | 
			
		||||
          ))}
 | 
			
		||||
          {edit && (inputVisible === 'sip' ? (
 | 
			
		||||
            <Input
 | 
			
		||||
              ref={sipInput}
 | 
			
		||||
              type="text"
 | 
			
		||||
              size="small"
 | 
			
		||||
              value={tag}
 | 
			
		||||
              className={styles.tagInput}
 | 
			
		||||
              onChange={e => setTag(e.target.value)}
 | 
			
		||||
              onBlur={() => handleTagConfirm('sip')}
 | 
			
		||||
              onPressEnter={() => handleTagConfirm('sip')}
 | 
			
		||||
            />
 | 
			
		||||
          ) : (
 | 
			
		||||
            <Tag className={styles.tagAdd} onClick={() => setInputVisible('sip')}><PlusOutlined/> 新建</Tag>
 | 
			
		||||
          ))}
 | 
			
		||||
        </Descriptions.Item>
 | 
			
		||||
        <Descriptions.Item label="公网IP">
 | 
			
		||||
          {lds.get(host, 'public_ip_address', []).map((item, index) => (
 | 
			
		||||
            <Tag visible closable={edit} key={index} onClose={() => handleTagRemove('gip', index)}>{item}</Tag>
 | 
			
		||||
          ))}
 | 
			
		||||
          {edit && (inputVisible === 'gip' ? (
 | 
			
		||||
            <Input
 | 
			
		||||
              ref={gipInput}
 | 
			
		||||
              type="text"
 | 
			
		||||
              size="small"
 | 
			
		||||
              value={tag}
 | 
			
		||||
              className={styles.tagInput}
 | 
			
		||||
              onChange={e => setTag(e.target.value)}
 | 
			
		||||
              onBlur={() => handleTagConfirm('gip')}
 | 
			
		||||
              onPressEnter={() => handleTagConfirm('gip')}
 | 
			
		||||
            />
 | 
			
		||||
          ) : (
 | 
			
		||||
            <Tag className={styles.tagAdd} onClick={() => setInputVisible('gip')}><PlusOutlined/> 新建</Tag>
 | 
			
		||||
          ))}
 | 
			
		||||
        </Descriptions.Item>
 | 
			
		||||
        <Descriptions.Item label="实例计费方式">
 | 
			
		||||
          {edit ? (
 | 
			
		||||
            <Select
 | 
			
		||||
              style={{width: 150}}
 | 
			
		||||
              value={host.instance_charge_type}
 | 
			
		||||
              placeholder="请选择"
 | 
			
		||||
              onChange={v => handleChange(v, 'instance_charge_type')}>
 | 
			
		||||
              <Select.Option value="PrePaid">包年包月</Select.Option>
 | 
			
		||||
              <Select.Option value="PostPaid">按量计费</Select.Option>
 | 
			
		||||
              <Select.Option value="Other">其他</Select.Option>
 | 
			
		||||
            </Select>
 | 
			
		||||
          ) : host.instance_charge_type_alias}
 | 
			
		||||
        </Descriptions.Item>
 | 
			
		||||
        <Descriptions.Item label="网络计费方式">
 | 
			
		||||
          {edit ? (
 | 
			
		||||
            <Select
 | 
			
		||||
              style={{width: 150}}
 | 
			
		||||
              value={host.internet_charge_type}
 | 
			
		||||
              placeholder="请选择"
 | 
			
		||||
              onChange={v => handleChange(v, 'internet_charge_type')}>
 | 
			
		||||
              <Select.Option value="PayByBandwidth">按带宽计费</Select.Option>
 | 
			
		||||
              <Select.Option value="PayByTraffic">按流量计费</Select.Option>
 | 
			
		||||
              <Select.Option value="Other">其他</Select.Option>
 | 
			
		||||
            </Select>
 | 
			
		||||
          ) : host.internet_charge_type_alisa}
 | 
			
		||||
        </Descriptions.Item>
 | 
			
		||||
        <Descriptions.Item label="创建时间">
 | 
			
		||||
          {edit ? (
 | 
			
		||||
            <DatePicker
 | 
			
		||||
              value={host.created_time ? moment(host.created_time) : undefined}
 | 
			
		||||
              onChange={v => handleChange(v, 'created_time')}/>
 | 
			
		||||
          ) : host.created_time}
 | 
			
		||||
        </Descriptions.Item>
 | 
			
		||||
        <Descriptions.Item label="到期时间">
 | 
			
		||||
          {edit ? (
 | 
			
		||||
            <DatePicker
 | 
			
		||||
              value={host.expired_time ? moment(host.expired_time) : undefined}
 | 
			
		||||
              onChange={v => handleChange(v, 'expired_time')}/>
 | 
			
		||||
          ) : host.expired_time}
 | 
			
		||||
        </Descriptions.Item>
 | 
			
		||||
        <Descriptions.Item label="更新时间">{host.updated_at}</Descriptions.Item>
 | 
			
		||||
      </Descriptions>
 | 
			
		||||
    </Drawer>
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -46,7 +46,8 @@ export default observer(function () {
 | 
			
		|||
        } else {
 | 
			
		||||
          message.success('操作成功');
 | 
			
		||||
          store.formVisible = false;
 | 
			
		||||
          store.fetchRecords()
 | 
			
		||||
          store.fetchRecords();
 | 
			
		||||
          if (!store.record.id) handleNext(res)
 | 
			
		||||
        }
 | 
			
		||||
      }, () => setLoading(false))
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -57,12 +58,24 @@ export default observer(function () {
 | 
			
		|||
      return http.post('/api/host/', formData).then(res => {
 | 
			
		||||
        message.success('验证成功');
 | 
			
		||||
        store.formVisible = false;
 | 
			
		||||
        store.fetchRecords()
 | 
			
		||||
        store.fetchRecords();
 | 
			
		||||
        if (!store.record.id) handleNext(res)
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
    message.error('请输入授权密码')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function handleNext(res) {
 | 
			
		||||
    Modal.confirm({
 | 
			
		||||
      title: '提示信息',
 | 
			
		||||
      content: '是否继续完善主机的扩展信息?',
 | 
			
		||||
      onOk: () => {
 | 
			
		||||
        store.record = res;
 | 
			
		||||
        store.detailVisible = true
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const ConfirmForm = (props) => (
 | 
			
		||||
    <Form layout="vertical" style={{marginTop: 24}}>
 | 
			
		||||
      <Form.Item required label="授权密码" help={`用户 ${props.username} 的密码, 该密码仅做首次验证使用,不会存储该密码。`}>
 | 
			
		||||
| 
						 | 
				
			
			@ -94,14 +107,14 @@ export default observer(function () {
 | 
			
		|||
  return (
 | 
			
		||||
    <Modal
 | 
			
		||||
      visible
 | 
			
		||||
      width={800}
 | 
			
		||||
      width={700}
 | 
			
		||||
      maskClosable={false}
 | 
			
		||||
      title={store.record.id ? '编辑主机' : '新建主机'}
 | 
			
		||||
      okText="验证"
 | 
			
		||||
      onCancel={() => store.formVisible = false}
 | 
			
		||||
      confirmLoading={loading}
 | 
			
		||||
      onOk={handleSubmit}>
 | 
			
		||||
      <Form form={form} labelCol={{span: 6}} wrapperCol={{span: 14}} initialValues={info}>
 | 
			
		||||
      <Form form={form} labelCol={{span: 5}} wrapperCol={{span: 17}} initialValues={info}>
 | 
			
		||||
        <Form.Item required name="group_ids" label="主机分组">
 | 
			
		||||
          <TreeSelect
 | 
			
		||||
            multiple
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,3 +2,26 @@
 | 
			
		|||
    width: 350px;
 | 
			
		||||
    margin: 0 auto 30px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tagAdd {
 | 
			
		||||
    background: #fff;
 | 
			
		||||
    border-style: dashed;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tagNumberInput {
 | 
			
		||||
  width: 78px;
 | 
			
		||||
  margin-right: 8px;
 | 
			
		||||
  vertical-align: top;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tagInput {
 | 
			
		||||
  width: 140px;
 | 
			
		||||
  margin-right: 8px;
 | 
			
		||||
  vertical-align: top;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.hostExtendEdit {
 | 
			
		||||
  :global(.ant-descriptions-item-content) {
 | 
			
		||||
    padding: 4px 16px !important;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue