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>
 | 
			
		||||
  );
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -27,6 +27,28 @@ export default observer(function () {
 | 
			
		|||
          </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;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||