diff --git a/spug_api/apps/host/__init__.py b/spug_api/apps/host/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/spug_api/apps/host/models.py b/spug_api/apps/host/models.py new file mode 100644 index 0000000..66283a3 --- /dev/null +++ b/spug_api/apps/host/models.py @@ -0,0 +1,28 @@ +from django.db import models +from libs import ModelMixin, human_time +from apps.account.models import User +from libs.ssh import SSH + + +class Host(models.Model, ModelMixin): + name = models.CharField(max_length=50) + zone = models.CharField(max_length=50) + hostname = models.CharField(max_length=50) + port = models.IntegerField() + username = models.CharField(max_length=50) + desc = models.CharField(max_length=255, null=True) + + created_at = models.CharField(max_length=20, default=human_time) + created_by = models.ForeignKey(User, models.PROTECT, related_name='+') + deleted_at = models.CharField(max_length=20, null=True) + deleted_by = models.ForeignKey(User, models.PROTECT, related_name='+', null=True) + + def get_ssh(self, pkey): + return SSH(self.hostname, self.port, self.username, pkey) + + def __repr__(self): + return '' % self.name + + class Meta: + db_table = 'hosts' + ordering = ('-id',) diff --git a/spug_api/apps/host/urls.py b/spug_api/apps/host/urls.py new file mode 100644 index 0000000..2741e52 --- /dev/null +++ b/spug_api/apps/host/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import url + +from .views import * + +urlpatterns = [ + url(r'^$', HostView.as_view()), +] diff --git a/spug_api/apps/host/views.py b/spug_api/apps/host/views.py new file mode 100644 index 0000000..dd8e181 --- /dev/null +++ b/spug_api/apps/host/views.py @@ -0,0 +1,70 @@ +from django.views.generic import View +from libs import json_response, JsonParser, Argument +from apps.setting.utils import AppSetting +from apps.host.models import Host +from libs.ssh import SSH, AuthenticationException +from libs import human_time + + +class HostView(View): + def get(self, request): + hosts = Host.objects.filter(deleted_by_id__isnull=True) + return json_response(hosts) + + def post(self, request): + form, error = JsonParser( + Argument('id', type=int, required=False), + Argument('zone', help='请输入主机类型'), + Argument('name', help='请输主机名称'), + Argument('username', help='请输入登录用户名'), + Argument('hostname', help='请输入主机名或IP'), + Argument('port', type=int, help='请输入SSH端口'), + Argument('desc', required=False), + Argument('password', required=False), + ).parse(request.body) + if error is None: + if valid_ssh(form.hostname, form.port, form.username, form.pop('password')) is False: + return json_response('auth fail') + + if form.id: + Host.objects.filter(pk=form.pop('id')).update(**form) + else: + form.created_by = request.user + Host.objects.create(**form) + return json_response(error=error) + + def delete(self, request): + form, error = JsonParser( + Argument('id', type=int, help='请指定操作对象') + ).parse(request.GET) + if error is None: + Host.objects.filter(pk=form.id).update( + deleted_at=human_time(), + deleted_by=request.user, + ) + return json_response(error=error) + + +def valid_ssh(hostname, port, username, password): + try: + private_key = AppSetting.get('private_key') + public_key = AppSetting.get('public_key') + except KeyError: + private_key, public_key = SSH.generate_key() + AppSetting.set('private_key', private_key, 'ssh private key') + AppSetting.set('public_key', public_key, 'ssh public key') + if password: + cli = SSH(hostname, port, username, password=password) + code, stdout, stderr = cli.exec_command('mkdir -p -m 700 ~/.ssh && \ + echo %r >> ~/.ssh/authorized_keys && \ + chmod 600 ~/.ssh/authorized_keys' % public_key) + if code != 0: + raise Exception('add public key error: ' + ''.join(x for x in stderr)) + else: + cli = SSH(hostname, port, username, private_key) + + try: + cli.ping() + except AuthenticationException: + return False + return True diff --git a/spug_api/apps/setting/__init__.py b/spug_api/apps/setting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/spug_api/apps/setting/models.py b/spug_api/apps/setting/models.py new file mode 100644 index 0000000..f62565f --- /dev/null +++ b/spug_api/apps/setting/models.py @@ -0,0 +1,13 @@ +from django.db import models + + +class Setting(models.Model): + key = models.CharField(max_length=50, unique=True) + value = models.TextField() + desc = models.CharField(max_length=255, null=True) + + def __repr__(self): + return '' % self.key + + class Meta: + db_table = 'settings' diff --git a/spug_api/apps/setting/utils.py b/spug_api/apps/setting/utils.py new file mode 100644 index 0000000..fdf3e80 --- /dev/null +++ b/spug_api/apps/setting/utils.py @@ -0,0 +1,21 @@ +from functools import lru_cache +from apps.setting.models import Setting + + +class AppSetting: + keys = ('public_key', 'private_key') + + @classmethod + @lru_cache(maxsize=64) + def get(cls, key): + info = Setting.objects.filter(key=key).first() + if not info: + raise KeyError(f'no such key for {key!r}') + return info.value + + @classmethod + def set(cls, key, value, desc=None): + if key in cls.keys: + Setting.objects.update_or_create(key=key, defaults={'value': value, 'desc': desc}) + else: + raise KeyError('invalid key') diff --git a/spug_api/libs/ssh.py b/spug_api/libs/ssh.py index 8f98579..af4e9c1 100644 --- a/spug_api/libs/ssh.py +++ b/spug_api/libs/ssh.py @@ -1,20 +1,21 @@ from paramiko.client import SSHClient, AutoAddPolicy from paramiko.config import SSH_PORT from paramiko.rsakey import RSAKey +from paramiko.ssh_exception import AuthenticationException from io import StringIO class SSH: - def __init__(self, host, port=SSH_PORT, username='root', pkey=None, password=None, connect_timeout=10): + def __init__(self, hostname, port=SSH_PORT, username='root', pkey=None, password=None, connect_timeout=10): if pkey is None and password is None: raise Exception('public key and password must have one is not None') self.client = None self.arguments = { - 'hostname': host, + 'hostname': hostname, 'port': port, 'username': username, 'password': password, - 'pkey': pkey, + 'pkey': RSAKey.from_private_key(StringIO(pkey)) if isinstance(pkey, str) else pkey, 'timeout': connect_timeout, } diff --git a/spug_api/requirements.txt b/spug_api/requirements.txt index 7be5945..6495ab2 100644 --- a/spug_api/requirements.txt +++ b/spug_api/requirements.txt @@ -1,2 +1,3 @@ Django==2.2.7 -channels==2.3.1 \ No newline at end of file +channels==2.3.1 +paramiko==2.6.0 \ No newline at end of file diff --git a/spug_api/spug/settings.py b/spug_api/spug/settings.py index 61d0392..6f55d02 100644 --- a/spug_api/spug/settings.py +++ b/spug_api/spug/settings.py @@ -31,6 +31,8 @@ ALLOWED_HOSTS = [] INSTALLED_APPS = [ 'channels', 'apps.account', + 'apps.host', + 'apps.setting', ] MIDDLEWARE = [ diff --git a/spug_api/spug/urls.py b/spug_api/spug/urls.py index c39743a..a47d477 100644 --- a/spug_api/spug/urls.py +++ b/spug_api/spug/urls.py @@ -16,5 +16,6 @@ Including another URLconf from django.urls import path, include urlpatterns = [ - path('account/', include('apps.account.urls')) + path('account/', include('apps.account.urls')), + path('host/', include('apps.host.urls')), ]