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.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'
|
||||||
|
|
||||||
|
|
|
@ -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),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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.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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
})
|
|
@ -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>
|
||||||
)
|
)
|
||||||
})
|
})
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
)}/>
|
)}/>
|
||||||
)}
|
)}
|
||||||
|
|
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 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>
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
.steps {
|
||||||
|
width: 350px;
|
||||||
|
margin: 0 auto 30px;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
||||||
|
|