upgrade host module

pull/330/head
vapao 2021-04-12 22:33:10 +08:00
parent 19150210a2
commit e9c0a04218
24 changed files with 527 additions and 21 deletions

59
spug_api/apps/host/add.py Normal file
View File

@ -0,0 +1,59 @@
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from libs import json_response, JsonParser, Argument
from apps.host.models import Host, HostExtend, Group
from apps.host import utils
import json
def get_regions(request):
form, error = JsonParser(
Argument('type', filter=lambda x: x in ('ali', 'tencent'), help='参数错误'),
Argument('ak', help='请输入AccessKey ID'),
Argument('ac', help='请输入AccessKey Secret'),
).parse(request.GET)
if error is None:
response = []
if form.type == 'ali':
for item in utils.fetch_ali_regions(form.ak, form.ac):
response.append({'id': item['RegionId'], 'name': item['LocalName']})
else:
for item in utils.fetch_tencent_regions(form.ak, form.ac):
response.append({'id': item['Region'], 'name': item['RegionName']})
return json_response(response)
return json_response(error=error)
def cloud_import(request):
form, error = JsonParser(
Argument('type', filter=lambda x: x in ('ali', 'tencent'), help='参数错误'),
Argument('ak', help='请输入AccessKey ID'),
Argument('ac', help='请输入AccessKey Secret'),
Argument('region_id', help='请选择区域'),
Argument('group_id', type=int, help='请选择分组')
).parse(request.body)
if error is None:
group = Group.objects.filter(pk=form.group_id).first()
if not group:
return json_response(error='未找到指定分组')
if form.type == 'ali':
instances = utils.fetch_ali_instances(form.ak, form.ac, form.region_id)
else:
instances = utils.fetch_tencent_instances(form.ak, form.ac, form.region_id)
host_add_ids = []
for item in instances:
instance_id = item['instance_id']
item['public_ip_address'] = json.dumps(item['public_ip_address'] or [])
item['private_ip_address'] = json.dumps(item['private_ip_address'] or [])
if HostExtend.objects.filter(instance_id=instance_id).exists():
HostExtend.objects.filter(instance_id=instance_id).update(**item)
else:
host = Host.objects.create(name=instance_id, created_by=request.user)
HostExtend.objects.create(host=host, **item)
host_add_ids.append(host.id)
if host_add_ids:
group.hosts.add(*host_add_ids)
return json_response(len(instances))
return json_response(error=error)

View File

@ -6,15 +6,17 @@ from libs import ModelMixin, human_datetime
from apps.account.models import User from apps.account.models import User
from apps.setting.utils import AppSetting from apps.setting.utils import AppSetting
from libs.ssh import SSH from libs.ssh import SSH
import json
class Host(models.Model, ModelMixin): class Host(models.Model, ModelMixin):
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
hostname = models.CharField(max_length=50) hostname = models.CharField(max_length=50)
port = models.IntegerField() port = models.IntegerField(null=True)
username = models.CharField(max_length=50) username = models.CharField(max_length=50)
pkey = models.TextField(null=True) pkey = models.TextField(null=True)
desc = models.CharField(max_length=255, null=True) desc = models.CharField(max_length=255, null=True)
is_verified = models.BooleanField(default=False)
created_at = models.CharField(max_length=20, default=human_datetime) created_at = models.CharField(max_length=20, default=human_datetime)
created_by = models.ForeignKey(User, models.PROTECT, related_name='+') created_by = models.ForeignKey(User, models.PROTECT, related_name='+')
@ -28,6 +30,8 @@ class Host(models.Model, ModelMixin):
def to_view(self): def to_view(self):
tmp = self.to_dict() tmp = self.to_dict()
if hasattr(self, 'hostextend'):
tmp.update(self.hostextend.to_view())
tmp['group_ids'] = [] tmp['group_ids'] = []
return tmp return tmp
@ -55,7 +59,7 @@ class HostExtend(models.Model, ModelMixin):
zone_id = models.CharField(max_length=30) zone_id = models.CharField(max_length=30)
cpu = models.IntegerField() cpu = models.IntegerField()
memory = models.FloatField() memory = models.FloatField()
disk = models.CharField(max_length=255) disk = models.CharField(max_length=255, default='[]')
os_name = models.CharField(max_length=50) os_name = models.CharField(max_length=50)
os_type = models.CharField(max_length=20) os_type = models.CharField(max_length=20)
private_ip_address = models.CharField(max_length=255) private_ip_address = models.CharField(max_length=255)
@ -66,6 +70,15 @@ class HostExtend(models.Model, ModelMixin):
expired_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) updated_at = models.CharField(max_length=20, default=human_datetime)
def to_view(self):
tmp = self.to_dict(excludes=('id',))
tmp['disk'] = json.loads(self.disk)
tmp['private_ip_address'] = json.loads(self.private_ip_address)
tmp['public_ip_address'] = json.loads(self.public_ip_address)
tmp['instance_charge_type_alias'] = self.get_instance_charge_type_display()
tmp['internet_charge_type_alisa'] = self.get_internet_charge_type_display()
return tmp
class Meta: class Meta:
db_table = 'host_extend' db_table = 'host_extend'

View File

@ -5,10 +5,13 @@ from django.urls import path
from apps.host.views import * from apps.host.views import *
from apps.host.group import GroupView from apps.host.group import GroupView
from apps.host.add import get_regions, cloud_import
urlpatterns = [ urlpatterns = [
path('', HostView.as_view()), path('', HostView.as_view()),
path('group/', GroupView.as_view()), path('group/', GroupView.as_view()),
path('import/', post_import), path('import/', post_import),
path('import/cloud/', cloud_import),
path('import/region/', get_regions),
path('parse/', post_parse), path('parse/', post_parse),
] ]

174
spug_api/apps/host/utils.py Normal file
View File

@ -0,0 +1,174 @@
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from libs.helper import make_ali_request, make_tencent_request
from collections import defaultdict
from datetime import datetime
def check_os_type(os_name):
os_name = os_name.lower()
types = ('centos', 'coreos', 'debian', 'suse', 'ubuntu', 'windows', 'freebsd', 'tencent', 'alibaba')
for t in types:
if t in os_name:
return t
return 'unknown'
def check_instance_charge_type(value, supplier):
if supplier == 'ali':
if value in ('PrePaid', 'PostPaid'):
return value
else:
return 'Other'
if supplier == 'tencent':
if value == 'PREPAID':
return 'PrePaid'
if value == 'POSTPAID_BY_HOUR':
return 'PostPaid'
return 'Other'
def check_internet_charge_type(value, supplier):
if supplier == 'ali':
if value in ('PayByTraffic', 'PayByBandwidth'):
return value
else:
return 'Other'
if supplier == 'tencent':
if value == 'TRAFFIC_POSTPAID_BY_HOUR':
return 'PayByTraffic'
if value in ('BANDWIDTH_PREPAID', 'BANDWIDTH_POSTPAID_BY_HOUR'):
return 'PayByBandwidth'
return 'Other'
def parse_utc_date(value):
if not value:
return None
s_format = '%Y-%m-%dT%H:%M:%S%z'
if len(value) == 17:
s_format = '%Y-%m-%dT%H:%M%z'
date = datetime.strptime(value, s_format)
return date.astimezone().strftime('%Y-%m-%d %H:%M:%S')
def parse__date(value, supplier):
if not value:
return None
date = datetime.strptime(value, '%Y-%m-%dT%H:%M%z')
return date.astimezone().strftime('%Y-%m-%d %H:%M:%S')
def fetch_ali_regions(ak, ac):
params = dict(Action='DescribeRegions')
res = make_ali_request(ak, ac, 'http://ecs.aliyuncs.com', params)
if 'Regions' in res:
return res['Regions']['Region']
else:
raise Exception(res)
def fetch_ali_disks(ak, ac, region_id, page_number=1):
data, page_size = defaultdict(list), 20
params = dict(
Action='DescribeDisks',
RegionId=region_id,
PageNumber=page_number,
PageSize=page_size
)
res = make_ali_request(ak, ac, 'http://ecs.aliyuncs.com', params)
if 'Disks' in res:
for item in res['Disks']['Disk']:
data[item['InstanceId']].append(item['Size'])
if len(res['Disks']['Disk']) == page_size:
page_number += 1
new_data = fetch_ali_disks(ak, ac, 'http://ecs.aliyuncs.com', page_number)
data.update(new_data)
return data
else:
raise Exception(res)
def fetch_ali_instances(ak, ac, region_id, page_number=1):
data, page_size = {}, 20
params = dict(
Action='DescribeInstances',
RegionId=region_id,
PageNumber=page_number,
PageSize=page_size
)
res = make_ali_request(ak, ac, 'http://ecs.aliyuncs.com', params)
if 'Instances' not in res:
raise Exception(res)
for item in res['Instances']['Instance']:
network_interface = item['NetworkInterfaces']['NetworkInterface']
data[item['InstanceId']] = dict(
instance_id=item['InstanceId'],
os_name=item['OSName'],
os_type=check_os_type(item['OSName']),
cpu=item['Cpu'],
memory=item['Memory'] / 1024,
created_time=parse_utc_date(item['CreationTime']),
expired_time=parse_utc_date(item['ExpiredTime']),
instance_charge_type=check_instance_charge_type(item['InstanceChargeType'], 'ali'),
internet_charge_type=check_internet_charge_type(item['InternetChargeType'], 'ali'),
public_ip_address=item['PublicIpAddress']['IpAddress'],
private_ip_address=list(map(lambda x: x['PrimaryIpAddress'], network_interface)),
zone_id=item['ZoneId']
)
if len(res['Instances']['Instance']) == page_size:
page_number += 1
new_data = fetch_ali_instances(ak, ac, region_id, page_number)
data.update(new_data)
if page_number != 1:
return data
for instance_id, disk in fetch_ali_disks(ak, ac, region_id).items():
if instance_id in data:
data[instance_id]['disk'] = disk
return list(data.values())
def fetch_tencent_regions(ak, ac):
params = dict(Action='DescribeRegions')
res = make_tencent_request(ak, ac, 'cvm.tencentcloudapi.com', params)
if 'RegionSet' in res['Response']:
return res['Response']['RegionSet']
else:
raise Exception(res)
def fetch_tencent_instances(ak, ac, region_id, page_number=1):
data, page_size = [], 20
params = dict(
Action='DescribeInstances',
Region=region_id,
Offset=(page_number - 1) * page_size,
Limit=page_size
)
res = make_tencent_request(ak, ac, 'cvm.tencentcloudapi.com', params)
if 'InstanceSet' not in res['Response']:
raise Exception(res)
for item in res['Response']['InstanceSet']:
data_disks = list(map(lambda x: x['DiskSize'], item['DataDisks']))
internet_charge_type = item['InternetAccessible']['InternetChargeType']
data.append(dict(
instance_id=item['InstanceId'],
os_name=item['OsName'],
os_type=check_os_type(item['OsName']),
cpu=item['CPU'],
memory=item['Memory'],
disk=[item['SystemDisk']['DiskSize']] + data_disks,
created_time=parse_utc_date(item['CreatedTime']),
expired_time=parse_utc_date(item['ExpiredTime']),
instance_charge_type=check_instance_charge_type(item['InstanceChargeType'], 'tencent'),
internet_charge_type=check_internet_charge_type(internet_charge_type, 'tencent'),
public_ip_address=item['PublicIpAddresses'],
private_ip_address=item['PrivateIpAddresses'],
zone_id=item['Placement']['Zone']
))
if len(res['Response']['InstanceSet']) == page_size:
page_number += 1
new_data = fetch_tencent_instances(ak, ac, region_id, page_number)
data.extend(new_data)
return data

View File

@ -10,7 +10,6 @@ from apps.host.models import Host, Group
from apps.app.models import Deploy 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 apps.account.models import Role
from libs.ssh import SSH, AuthenticationException from libs.ssh import SSH, AuthenticationException
from paramiko.ssh_exception import BadAuthenticationType from paramiko.ssh_exception import BadAuthenticationType
from libs import AttrDict from libs import AttrDict
@ -25,7 +24,7 @@ class HostView(View):
if not request.user.has_host_perm(host_id): if not request.user.has_host_perm(host_id):
return json_response(error='无权访问该主机,请联系管理员') return json_response(error='无权访问该主机,请联系管理员')
return json_response(Host.objects.get(pk=host_id)) return json_response(Host.objects.get(pk=host_id))
hosts = {x.id: x.to_view() for x in Host.objects.all()} hosts = {x.id: x.to_view() for x in Host.objects.select_related('hostextend').all()}
for rel in Group.hosts.through.objects.all(): for rel in Group.hosts.through.objects.all():
hosts[rel.host_id]['group_ids'].append(rel.group_id) hosts[rel.host_id]['group_ids'].append(rel.group_id)
return json_response(list(hosts.values())) return json_response(list(hosts.values()))
@ -91,10 +90,8 @@ class HostView(View):
detection = Detection.objects.filter(type__in=('3', '4'), addr=form.id).first() detection = Detection.objects.filter(type__in=('3', '4'), addr=form.id).first()
if detection: if detection:
return json_response(error=f'监控中心的任务【{detection.name}】关联了该主机,请解除关联后再尝试删除该主机') return json_response(error=f'监控中心的任务【{detection.name}】关联了该主机,请解除关联后再尝试删除该主机')
role = Role.objects.filter(host_perms__regex=fr'[^0-9]{form.id}[^0-9]').first()
if role:
return json_response(error=f'角色【{role.name}】的主机权限关联了该主机,请解除关联后再尝试删除该主机')
Host.objects.filter(pk=form.id).delete() Host.objects.filter(pk=form.id).delete()
print('pk: ', form.id)
return json_response(error=error) return json_response(error=error)

60
spug_api/libs/helper.py Normal file
View File

@ -0,0 +1,60 @@
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from urllib.parse import quote, urlencode
from datetime import datetime
from pytz import timezone
import requests
import hashlib
import base64
import random
import time
import hmac
import uuid
def _special_url_encode(value) -> str:
if isinstance(value, (str, bytes)):
rst = quote(value)
else:
rst = urlencode(value)
return rst.replace('+', '%20').replace('*', '%2A').replace('%7E', '~')
def _make_ali_signature(key: str, params: dict) -> bytes:
sorted_str = _special_url_encode(dict(sorted(params.items())))
sign_str = 'GET&%2F&' + _special_url_encode(sorted_str)
sign_digest = hmac.new(key.encode(), sign_str.encode(), hashlib.sha1).digest()
return base64.encodebytes(sign_digest).strip()
def _make_tencent_signature(endpoint: str, key: str, params: dict) -> bytes:
sorted_str = '&'.join(f'{k}={v}' for k, v in sorted(params.items()))
sign_str = f'POST{endpoint}/?{sorted_str}'
sign_digest = hmac.new(key.encode(), sign_str.encode(), hashlib.sha1).digest()
return base64.encodebytes(sign_digest).strip()
def make_ali_request(ak, ac, endpoint, params):
params.update(
AccessKeyId=ak,
Format='JSON',
SignatureMethod='HMAC-SHA1',
SignatureNonce=uuid.uuid4().hex,
SignatureVersion='1.0',
Timestamp=datetime.now(tz=timezone('UTC')).strftime('%Y-%m-%dT%H:%M:%SZ'),
Version='2014-05-26'
)
params['Signature'] = _make_ali_signature(ac + '&', params)
return requests.get(endpoint, params).json()
def make_tencent_request(ak, ac, endpoint, params):
params.update(
Nonce=int(random.random() * 10000),
SecretId=ak,
Timestamp=int(time.time()),
Version='2017-03-12'
)
params['Signature'] = _make_tencent_signature(endpoint, ac, params)
return requests.post(f'https://{endpoint}', data=params).json()

View File

@ -0,0 +1,92 @@
/**
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
* Copyright (c) <spug.dev@gmail.com>
* Released under the AGPL-3.0 License.
*/
import React, { useState } from 'react';
import { observer } from 'mobx-react';
import { Modal, Form, Input, Select, Button, Steps, Cascader, message } from 'antd';
import http from 'libs/http';
import store from './store';
import styles from './index.module.less';
export default observer(function () {
const [loading, setLoading] = useState(false);
const [step, setStep] = useState(0);
const [ak, setAK] = useState();
const [ac, setAC] = useState();
const [regionId, setRegionId] = useState();
const [groupId, setGroupId] = useState([]);
const [regions, setRegions] = useState([]);
function handleSubmit() {
setLoading(true);
const formData = {ak, ac, type: store.cloudImport, region_id: regionId, group_id: groupId[groupId.length - 1]};
http.post('/api/host/import/cloud/', formData, {timeout: 120000})
.then(res => {
message.success(`已同步/导入 ${res} 台主机`);
store.cloudImport = null;
store.fetchRecords()
}, () => setLoading(false))
}
function fetchRegions() {
setLoading(true);
http.get('/api/host/import/region/', {params: {ak, ac, type: store.cloudImport}})
.then(res => {
setRegions(res)
setStep(1)
})
.finally(() => setLoading(false))
}
return (
<Modal
visible
maskClosable={false}
title="批量导入"
footer={null}
onCancel={() => store.cloudImport = null}>
<Steps current={step} className={styles.steps}>
<Steps.Step key={0} title="访问凭据"/>
<Steps.Step key={1} title="导入确认"/>
</Steps>
<Form labelCol={{span: 8}} wrapperCol={{span: 14}}>
<Form.Item hidden={step === 1} required label="AccessKey ID">
<Input value={ak} onChange={e => setAK(e.target.value)} placeholder="请输入"/>
</Form.Item>
<Form.Item hidden={step === 1} required label="AccessKey Secret">
<Input value={ac} onChange={e => setAC(e.target.value)} placeholder="请输入"/>
</Form.Item>
<Form.Item hidden={step === 0} required label="选择区域">
<Select placeholder="请选择" value={regionId} onChange={setRegionId}>
{regions.map(item => (
<Select.Option key={item.id} value={item.id}>{item.name}</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item hidden={step === 0} required label="选择分组">
<Cascader
value={groupId}
onChange={setGroupId}
options={store.treeData}
fieldNames={{label: 'title'}}
placeholder="请选择"/>
</Form.Item>
<Form.Item wrapperCol={{span: 14, offset: 8}}>
{step === 0 ? (
<Button type="primary" loading={loading} disabled={!ak || !ac} onClick={fetchRegions}>下一步</Button>
) : ([
<Button
key="1"
type="primary"
loading={loading}
disabled={!regionId || !groupId}
onClick={handleSubmit}>同步导入</Button>,
<Button key="2" style={{marginLeft: 24}} onClick={() => setStep(0)}>上一步</Button>
])}
</Form.Item>
</Form>
</Modal>
);
})

View File

@ -27,6 +27,28 @@ export default observer(function () {
</List> </List>
</Descriptions.Item> </Descriptions.Item>
</Descriptions> </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}
</Drawer> </Drawer>
) )
}) })

View File

@ -124,7 +124,7 @@ export default observer(function () {
<Input addonBefore="-p" placeholder="端口"/> <Input addonBefore="-p" placeholder="端口"/>
</Form.Item> </Form.Item>
</Form.Item> </Form.Item>
<Form.Item label="独立密钥" extra="默认使用全局密钥,如果上传了独立密钥则优先使用该密钥。"> <Form.Item label="独立密钥" extra="默认使用全局密钥,如果上传了独立密钥(私钥)则优先使用该密钥。">
<Upload name="file" fileList={fileList} headers={{'X-Token': X_TOKEN}} beforeUpload={handleUpload} <Upload name="file" fileList={fileList} headers={{'X-Token': X_TOKEN}} beforeUpload={handleUpload}
onChange={handleUploadChange}> onChange={handleUploadChange}>
{fileList.length === 0 ? <Button loading={uploading} icon={<UploadOutlined/>}>点击上传</Button> : null} {fileList.length === 0 ? <Button loading={uploading} icon={<UploadOutlined/>}>点击上传</Button> : null}

View File

@ -5,11 +5,12 @@
*/ */
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Table, Modal, message } from 'antd'; import { Table, Modal, Dropdown, Button, Menu, Avatar, Tooltip, Space, Tag, message } from 'antd';
import { PlusOutlined, ImportOutlined } from '@ant-design/icons'; import { PlusOutlined, DownOutlined } from '@ant-design/icons';
import { Action, TableCard, AuthButton } from 'components'; import { Action, TableCard, AuthButton, AuthFragment } from 'components';
import { http, hasPermission } from 'libs'; import { http, hasPermission } from 'libs';
import store from './store'; import store from './store';
import icons from './icons';
function ComTable() { function ComTable() {
useEffect(() => { useEffect(() => {
@ -30,6 +31,24 @@ function ComTable() {
}) })
} }
function handleImport(menu) {
if (menu.key === 'excel') {
store.importVisible = true
} else {
store.cloudImport = menu.key
}
}
function IpAddress(props) {
if (props.ip && props.ip.length > 0) {
return (
<div>{props.ip[0]}<span style={{color: '#999'}}>{props.isPublic ? '公' : '私有'}</span></div>
)
} else {
return null
}
}
return ( return (
<TableCard <TableCard
rowKey="id" rowKey="id"
@ -43,11 +62,32 @@ function ComTable() {
type="primary" type="primary"
icon={<PlusOutlined/>} icon={<PlusOutlined/>}
onClick={() => store.showForm()}>新建</AuthButton>, onClick={() => store.showForm()}>新建</AuthButton>,
<AuthButton <AuthFragment auth="host.host.add">
auth="host.host.add" <Dropdown overlay={(
type="primary" <Menu onClick={handleImport}>
icon={<ImportOutlined/>} <Menu.Item key="excel">
onClick={() => store.importVisible = true}>批量导入</AuthButton> <Space>
<Avatar shape="square" size={20} src={icons.excel}/>
<span>Excel</span>
</Space>
</Menu.Item>
<Menu.Item key="ali">
<Space>
<Avatar shape="square" size={20} src={icons.alibaba}/>
<span>阿里云</span>
</Space>
</Menu.Item>
<Menu.Item key="tencent">
<Space>
<Avatar shape="square" size={20} src={icons.tencent}/>
<span>腾讯云</span>
</Space>
</Menu.Item>
</Menu>
)}>
<Button type="primary">批量导入 <DownOutlined/></Button>
</Dropdown>
</AuthFragment>
]} ]}
pagination={{ pagination={{
showSizeChanger: true, showSizeChanger: true,
@ -61,14 +101,30 @@ function ComTable() {
title="主机名称" title="主机名称"
render={info => <Action.Button onClick={() => store.showDetail(info)}>{info.name}</Action.Button>} render={info => <Action.Button onClick={() => store.showDetail(info)}>{info.name}</Action.Button>}
sorter={(a, b) => a.name.localeCompare(b.name)}/> sorter={(a, b) => a.name.localeCompare(b.name)}/>
<Table.Column title="连接地址" dataIndex="hostname" sorter={(a, b) => a.name.localeCompare(b.name)}/> <Table.Column title="IP地址" render={info => (
<Table.Column hide width={100} title="端口" dataIndex="port"/> <div>
<Table.Column title="备注信息" dataIndex="desc"/> <IpAddress ip={info.public_ip_address} isPublic/>
<IpAddress ip={info.private_ip_address}/>
</div>
)}/>
<Table.Column title="配置信息" render={info => (
<Space>
<Tooltip title={info.os_name}>
<Avatar shape="square" size={16} src={icons[info.os_type]}/>
</Tooltip>
<span>{info.cpu} {info.memory}GB</span>
</Space>
)}/>
<Table.Column hide title="备注信息" dataIndex="desc"/>
<Table.Column
title="状态"
dataIndex="is_verified"
render={v => v ? <Tag color="green">已验证</Tag> : <Tag color="orange"></Tag>}/>
{hasPermission('host.host.edit|host.host.del|host.host.console') && ( {hasPermission('host.host.edit|host.host.del|host.host.console') && (
<Table.Column width={160} title="操作" render={info => ( <Table.Column width={160} title="操作" render={info => (
<Action> <Action>
<Action.Button auth="host.host.edit" onClick={() => store.showForm(info)}>编辑</Action.Button> <Action.Button auth="host.host.edit" onClick={() => store.showForm(info)}>编辑</Action.Button>
<Action.Button auth="host.host.del" onClick={() => handleDelete(info)}>删除</Action.Button> <Action.Button danger auth="host.host.del" onClick={() => handleDelete(info)}>删除</Action.Button>
</Action> </Action>
)}/> )}/>
)} )}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,23 @@
import iconExcel from './excel.png';
import iconCentos from './centos.png';
import iconAlibaba from './alibaba.png';
import iconCoreos from './coreos.png';
import iconDebian from './debian.png';
import iconFreebsd from './freebsd.png';
import iconSuse from './suse.png';
import iconTencent from './tencent.png';
import iconUbuntu from './ubuntu.png';
import iconWindows from './windows.png';
export default {
excel: iconExcel,
alibaba: iconAlibaba,
centos: iconCentos,
coreos: iconCoreos,
debian: iconDebian,
freebsd: iconFreebsd,
suse: iconSuse,
tencent: iconTencent,
ubuntu: iconUbuntu,
windows: iconWindows,
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -12,6 +12,7 @@ import Group from './Group';
import ComTable from './Table'; import ComTable from './Table';
import ComForm from './Form'; import ComForm from './Form';
import ComImport from './Import'; import ComImport from './Import';
import CloudImport from './CloudImport';
import Detail from './Detail'; import Detail from './Detail';
import Selector from './Selector'; import Selector from './Selector';
import store from './store'; import store from './store';
@ -40,6 +41,7 @@ export default observer(function () {
<Detail/> <Detail/>
{store.formVisible && <ComForm/>} {store.formVisible && <ComForm/>}
{store.importVisible && <ComImport/>} {store.importVisible && <ComImport/>}
{store.cloudImport && <CloudImport/>}
{store.selectorVisible && {store.selectorVisible &&
<Selector oneGroup={!store.addByCopy} onCancel={() => store.selectorVisible = false} onOk={store.updateGroup}/>} <Selector oneGroup={!store.addByCopy} onCancel={() => store.selectorVisible = false} onOk={store.updateGroup}/>}
</AuthDiv> </AuthDiv>

View File

@ -0,0 +1,4 @@
.steps {
width: 350px;
margin: 0 auto 30px;
}

View File

@ -20,6 +20,7 @@ class Store {
@observable isFetching = false; @observable isFetching = false;
@observable formVisible = false; @observable formVisible = false;
@observable importVisible = false; @observable importVisible = false;
@observable cloudImport = null;
@observable detailVisible = false; @observable detailVisible = false;
@observable selectorVisible = false; @observable selectorVisible = false;