upgrade host module
|
@ -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)
|
|
@ -6,15 +6,17 @@ from libs import ModelMixin, human_datetime
|
|||
from apps.account.models import User
|
||||
from apps.setting.utils import AppSetting
|
||||
from libs.ssh import SSH
|
||||
import json
|
||||
|
||||
|
||||
class Host(models.Model, ModelMixin):
|
||||
name = 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)
|
||||
pkey = models.TextField(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_by = models.ForeignKey(User, models.PROTECT, related_name='+')
|
||||
|
||||
|
@ -28,6 +30,8 @@ class Host(models.Model, ModelMixin):
|
|||
|
||||
def to_view(self):
|
||||
tmp = self.to_dict()
|
||||
if hasattr(self, 'hostextend'):
|
||||
tmp.update(self.hostextend.to_view())
|
||||
tmp['group_ids'] = []
|
||||
return tmp
|
||||
|
||||
|
@ -55,7 +59,7 @@ class HostExtend(models.Model, ModelMixin):
|
|||
zone_id = models.CharField(max_length=30)
|
||||
cpu = models.IntegerField()
|
||||
memory = models.FloatField()
|
||||
disk = models.CharField(max_length=255)
|
||||
disk = models.CharField(max_length=255, default='[]')
|
||||
os_name = models.CharField(max_length=50)
|
||||
os_type = models.CharField(max_length=20)
|
||||
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)
|
||||
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:
|
||||
db_table = 'host_extend'
|
||||
|
||||
|
|
|
@ -5,10 +5,13 @@ from django.urls import path
|
|||
|
||||
from apps.host.views import *
|
||||
from apps.host.group import GroupView
|
||||
from apps.host.add import get_regions, cloud_import
|
||||
|
||||
urlpatterns = [
|
||||
path('', HostView.as_view()),
|
||||
path('group/', GroupView.as_view()),
|
||||
path('import/', post_import),
|
||||
path('import/cloud/', cloud_import),
|
||||
path('import/region/', get_regions),
|
||||
path('parse/', post_parse),
|
||||
]
|
||||
|
|
|
@ -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
|
|
@ -10,7 +10,6 @@ from apps.host.models import Host, Group
|
|||
from apps.app.models import Deploy
|
||||
from apps.schedule.models import Task
|
||||
from apps.monitor.models import Detection
|
||||
from apps.account.models import Role
|
||||
from libs.ssh import SSH, AuthenticationException
|
||||
from paramiko.ssh_exception import BadAuthenticationType
|
||||
from libs import AttrDict
|
||||
|
@ -25,7 +24,7 @@ class HostView(View):
|
|||
if not request.user.has_host_perm(host_id):
|
||||
return json_response(error='无权访问该主机,请联系管理员')
|
||||
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():
|
||||
hosts[rel.host_id]['group_ids'].append(rel.group_id)
|
||||
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()
|
||||
if detection:
|
||||
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()
|
||||
print('pk: ', form.id)
|
||||
return json_response(error=error)
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
|
@ -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>
|
||||
);
|
||||
})
|
|
@ -20,13 +20,35 @@ export default observer(function () {
|
|||
<Descriptions.Item label="独立密钥">{host.pkey ? '是' : '否'}</Descriptions.Item>
|
||||
<Descriptions.Item label="描述信息">{host.desc}</Descriptions.Item>
|
||||
<Descriptions.Item label="所属分组">
|
||||
<List >
|
||||
<List>
|
||||
{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}
|
||||
|
||||
</Drawer>
|
||||
)
|
||||
})
|
|
@ -124,7 +124,7 @@ export default observer(function () {
|
|||
<Input addonBefore="-p" placeholder="端口"/>
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
<Form.Item label="独立密钥" extra="默认使用全局密钥,如果上传了独立密钥则优先使用该密钥。">
|
||||
<Form.Item label="独立密钥" extra="默认使用全局密钥,如果上传了独立密钥(私钥)则优先使用该密钥。">
|
||||
<Upload name="file" fileList={fileList} headers={{'X-Token': X_TOKEN}} beforeUpload={handleUpload}
|
||||
onChange={handleUploadChange}>
|
||||
{fileList.length === 0 ? <Button loading={uploading} icon={<UploadOutlined/>}>点击上传</Button> : null}
|
||||
|
|
|
@ -5,11 +5,12 @@
|
|||
*/
|
||||
import React, { useEffect } from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Table, Modal, message } from 'antd';
|
||||
import { PlusOutlined, ImportOutlined } from '@ant-design/icons';
|
||||
import { Action, TableCard, AuthButton } from 'components';
|
||||
import { Table, Modal, Dropdown, Button, Menu, Avatar, Tooltip, Space, Tag, message } from 'antd';
|
||||
import { PlusOutlined, DownOutlined } from '@ant-design/icons';
|
||||
import { Action, TableCard, AuthButton, AuthFragment } from 'components';
|
||||
import { http, hasPermission } from 'libs';
|
||||
import store from './store';
|
||||
import icons from './icons';
|
||||
|
||||
function ComTable() {
|
||||
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 (
|
||||
<TableCard
|
||||
rowKey="id"
|
||||
|
@ -43,11 +62,32 @@ function ComTable() {
|
|||
type="primary"
|
||||
icon={<PlusOutlined/>}
|
||||
onClick={() => store.showForm()}>新建</AuthButton>,
|
||||
<AuthButton
|
||||
auth="host.host.add"
|
||||
type="primary"
|
||||
icon={<ImportOutlined/>}
|
||||
onClick={() => store.importVisible = true}>批量导入</AuthButton>
|
||||
<AuthFragment auth="host.host.add">
|
||||
<Dropdown overlay={(
|
||||
<Menu onClick={handleImport}>
|
||||
<Menu.Item key="excel">
|
||||
<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={{
|
||||
showSizeChanger: true,
|
||||
|
@ -61,14 +101,30 @@ function ComTable() {
|
|||
title="主机名称"
|
||||
render={info => <Action.Button onClick={() => store.showDetail(info)}>{info.name}</Action.Button>}
|
||||
sorter={(a, b) => a.name.localeCompare(b.name)}/>
|
||||
<Table.Column title="连接地址" dataIndex="hostname" sorter={(a, b) => a.name.localeCompare(b.name)}/>
|
||||
<Table.Column hide width={100} title="端口" dataIndex="port"/>
|
||||
<Table.Column title="备注信息" dataIndex="desc"/>
|
||||
<Table.Column title="IP地址" render={info => (
|
||||
<div>
|
||||
<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') && (
|
||||
<Table.Column width={160} title="操作" render={info => (
|
||||
<Action>
|
||||
<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>
|
||||
)}/>
|
||||
)}
|
||||
|
|
After Width: | Height: | Size: 4.0 KiB |
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 1.9 KiB |
|
@ -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,
|
||||
}
|
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 1.4 KiB |
|
@ -12,6 +12,7 @@ import Group from './Group';
|
|||
import ComTable from './Table';
|
||||
import ComForm from './Form';
|
||||
import ComImport from './Import';
|
||||
import CloudImport from './CloudImport';
|
||||
import Detail from './Detail';
|
||||
import Selector from './Selector';
|
||||
import store from './store';
|
||||
|
@ -40,6 +41,7 @@ export default observer(function () {
|
|||
<Detail/>
|
||||
{store.formVisible && <ComForm/>}
|
||||
{store.importVisible && <ComImport/>}
|
||||
{store.cloudImport && <CloudImport/>}
|
||||
{store.selectorVisible &&
|
||||
<Selector oneGroup={!store.addByCopy} onCancel={() => store.selectorVisible = false} onOk={store.updateGroup}/>}
|
||||
</AuthDiv>
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
.steps {
|
||||
width: 350px;
|
||||
margin: 0 auto 30px;
|
||||
}
|
|
@ -20,6 +20,7 @@ class Store {
|
|||
@observable isFetching = false;
|
||||
@observable formVisible = false;
|
||||
@observable importVisible = false;
|
||||
@observable cloudImport = null;
|
||||
@observable detailVisible = false;
|
||||
@observable selectorVisible = false;
|
||||
|
||||
|
|