From a164b048a46a64508e6473b75676687a4ebd1647 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=95=E5=85=A8?= Date: Mon, 10 Jun 2019 17:12:53 +0800 Subject: [PATCH] v0.3.3 k8s webssh --- README.md | 3 +- k8s/consumers.py | 100 ++++++++++++++++++++++++++++++ k8s/k8sApi/core.py | 57 ++++++++++++++--- k8s/urls.py | 1 + k8s/views.py | 15 ++++- requirements.txt | 39 +++++++++++- seal/asgi.py | 7 +++ seal/routing.py | 13 ++++ seal/settings.py | 27 +++++++- templates/k8s/k8s-pod-list.html | 5 +- templates/k8s/k8s-pod-webssh.html | 53 ++++++++++++++++ 11 files changed, 302 insertions(+), 18 deletions(-) create mode 100644 k8s/consumers.py create mode 100644 seal/asgi.py create mode 100644 seal/routing.py create mode 100644 templates/k8s/k8s-pod-webssh.html diff --git a/README.md b/README.md index 78dde99..4bbf0e7 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,8 @@ * 一期: 基础模板 (已完成) * 二期: k8s管理平台 (开发中) - * pod 列表 (已完成) + * pod 列表 (已完成) + * pod webssh (已完成) ## DEMO diff --git a/k8s/consumers.py b/k8s/consumers.py new file mode 100644 index 0000000..a381427 --- /dev/null +++ b/k8s/consumers.py @@ -0,0 +1,100 @@ +from asgiref.sync import async_to_sync +from channels.generic.websocket import WebsocketConsumer + +import paramiko +import threading +import time +from seal import settings + +from channels.layers import get_channel_layer + +channel_layer = get_channel_layer() + + +class MyThread(threading.Thread): + def __init__(self, chan): + threading.Thread.__init__(self) + self.chan = chan + self.number = 0 + + def run(self): + + while not self.chan.chan.exit_status_ready(): + time.sleep(0.1) + try: + data = self.chan.chan.recv(1024) + str_data = data.decode(encoding='utf-8') + if getattr(settings, 'webssh_name') in data.decode(encoding='utf-8'): + self.number += 1 + + if "kubectl exec -it" in str_data: + #不返回内容 + pass + else: + if "rpc error" in str_data: + async_to_sync(self.chan.channel_layer.group_send)( + self.chan.scope['user'].username, + { + "type": "user.message", + "text": "连接错误,已断开连接! 此 pod 不支持sh 或者其他未知错误!\r" + }, + ) + self.chan.sshclient.close() + elif self.number > 1: + async_to_sync(self.chan.channel_layer.group_send)( + self.chan.scope['user'].username, + { + "type": "user.message", + "text": "程序退出,已断开连接!\r" + }, + ) + self.chan.sshclient.close() + else: + async_to_sync(self.chan.channel_layer.group_send)( + self.chan.scope['user'].username, + { + "type": "user.message", + "text": bytes.decode(data) + }, + ) + + except Exception as ex: + pass + self.chan.sshclient.close() + return False + + +class EchoConsumer(WebsocketConsumer): + + def connect(self): + # 创建channels group, 命名为:用户名 (最好不要中文名字),并使用channel_layer写入到redis + async_to_sync(self.channel_layer.group_add)(self.scope['user'].username, self.channel_name) + + self.sshclient = paramiko.SSHClient() + self.sshclient.load_system_host_keys() + self.sshclient.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + self.sshclient.connect(getattr(settings, 'webssh_ip'), getattr(settings, 'webssh_port'), + getattr(settings, 'webssh_username'), getattr(settings, 'webssh_password')) + self.chan = self.sshclient.invoke_shell(term='xterm') + self.chan.settimeout(0) + t1 = MyThread(self) + t1.setDaemon(True) + t1.start() + path = self.scope['path'].split('/') + cmd = f"kubectl exec -it {path[2]} -n {path[3]} sh \r" + self.chan.send(cmd) + + self.accept() + + def receive(self, text_data): + try: + self.chan.send(text_data) + except Exception as ex: + pass + # print(str(ex)) + + def user_message(self, event): + self.send(text_data=event["text"]) + + def disconnect(self, close_code): + async_to_sync(self.channel_layer.group_discard)(self.scope['user'].username, self.channel_name) diff --git a/k8s/k8sApi/core.py b/k8s/k8sApi/core.py index 24e8d41..7f55e59 100644 --- a/k8s/k8sApi/core.py +++ b/k8s/k8sApi/core.py @@ -1,15 +1,54 @@ from kubernetes import client, config from seal import settings import urllib3 +from kubernetes.stream import stream -def K8sApi(): - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - configuration = client.Configuration() - configuration.host = getattr(settings, 'APISERVER') - configuration.verify_ssl = False - configuration.api_key = {"authorization": "Bearer " + getattr(settings, 'Token'), } - client.Configuration.set_default(configuration) - v1 = client.CoreV1Api() +class K8sApi(object): - return v1 + def __init__(self): + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + def get_client(self): + baseurl = getattr(settings, 'APISERVER') + token = getattr(settings, 'Token') + aConfiguration = client.Configuration() + aConfiguration.host = baseurl + aConfiguration.verify_ssl = False + aConfiguration.api_key = {"authorization": "Bearer " + token} + aApiClient = client.ApiClient(aConfiguration) + v1 = client.CoreV1Api(aApiClient) + return v1 + + def get_podlist(self): + client_v1 = self.get_client() + ret_pod = client_v1.list_pod_for_all_namespaces(watch=False) + return ret_pod + + def get_namespacelist(self): + client_v1 = self.get_client() + ret_namespace = client_v1.list_namespace() + return ret_namespace + + def test_pods_connect(self, podname, namespace, command, container=None): + client_v1 = self.get_client() + if stream(client_v1.connect_get_namespaced_pod_exec, podname, namespace, command=command, + container=container, + stderr=True, stdin=False, + stdout=True, tty=False): + return True + else: + return False + + def get_pods_exec(self, podname, namespace, command, container=None): + client_v1 = self.get_client() + if container: + rest = stream(client_v1.connect_get_namespaced_pod_exec, podname, namespace, command=command, + container=container, + stderr=True, stdin=False, + stdout=True, tty=False) + else: + rest = stream(client_v1.connect_get_namespaced_pod_exec, podname, namespace, command=command, + stderr=True, stdin=False, + stdout=True, tty=False) + return rest diff --git a/k8s/urls.py b/k8s/urls.py index 872c771..002a540 100644 --- a/k8s/urls.py +++ b/k8s/urls.py @@ -4,4 +4,5 @@ app_name = "k8s" urlpatterns = [ path('k8s-pod-list', views.K8sPodListView.as_view(), name='k8s-pod-list'), + path('k8s-pod-webssh', views.K8sPodWebssh.as_view(), name='k8s-pod-webssh') ] diff --git a/k8s/views.py b/k8s/views.py index fd37ba1..34a6932 100644 --- a/k8s/views.py +++ b/k8s/views.py @@ -10,6 +10,7 @@ from system.decorator.get_list import get_list from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin from django.views.generic import ListView, View, DetailView, CreateView, UpdateView from assets.models import Ecs +from seal import settings from k8s.k8sApi.core import K8sApi logger = logging.getLogger('k8s') @@ -17,11 +18,21 @@ logger = logging.getLogger('k8s') class K8sPodListView(LoginRequiredMixin, PermissionRequiredMixin, View): permission_required = ('k8s.view_ecs',) - template_name = 'k8s/k8s-pod-list.html' def get(self, request): - ret = K8sApi().list_pod_for_all_namespaces(watch=False) + obj = K8sApi() + ret = obj.get_podlist() data = {} for i in ret.items: data[i.metadata.name] = {"ip": i.status.pod_ip, "namespace": i.metadata.namespace} return render(request, "k8s/k8s-pod-list.html", {"data": data}) + + +class K8sPodWebssh(LoginRequiredMixin, PermissionRequiredMixin, View): + permission_required = ('k8s.view_ecs',) + + def get(self, request): + name = self.request.GET.get("name") + namespace = self.request.GET.get("namespace") + return render(request, "k8s/k8s-pod-webssh.html",{"name":name,"namespace":namespace}) + diff --git a/requirements.txt b/requirements.txt index fc52f9e..d5eb9f5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,30 @@ +aioredis==1.2.0 amqp==2.4.2 aniso8601==3.0.2 +anyjson==0.3.3 argh==0.26.2 +asgi-redis==1.4.3 +asgiref==3.1.2 +asn1crypto==0.24.0 +async-timeout==3.0.1 +attrs==19.1.0 +autobahn==19.6.2 +Automat==0.7.0 backcall==0.1.0 +bcrypt==3.1.6 billiard==3.6.0.0 +cachetools==3.1.1 celery==4.3.0 certifi==2018.11.29 +cffi==1.12.3 +channels==2.2.0 +channels-redis==2.4.0 chardet==3.0.4 +constantly==15.1.0 coreapi==2.3.3 coreschema==0.0.4 +cryptography==2.7 +daphne==2.3.0 decorator==4.3.0 Django==2.2.2 django-bootstrap4==0.0.7 @@ -22,12 +39,16 @@ django-timezone-field==3.0 djangorestframework==3.9.2 dramatiq==1.5.0 gevent==1.4.0 +google-auth==1.6.3 graphene==2.1.3 graphene-django==2.2.0 graphql-core==2.1 graphql-relay==0.4.5 greenlet==0.4.15 +hiredis==1.0.0 +hyperlink==19.0.0 idna==2.8 +incremental==17.5.0 ipython==6.4.0 ipython-genutils==0.2.0 itypes==1.1.0 @@ -35,9 +56,14 @@ jedi==0.12.0 Jinja2==2.10.1 jsonfield==2.0.2 kombu==4.4.0 +kubernetes==9.0.0 MarkupSafe==1.1.1 +msgpack==0.6.1 +msgpack-python==0.5.6 mysqlclient==1.4.2.post1 +oauthlib==3.0.1 openapi-codec==1.3.2 +paramiko==2.4.2 parso==0.2.1 pathtools==0.1.2 pexpect==4.6.0 @@ -46,13 +72,20 @@ prometheus-client==0.2.0 promise==2.2.1 prompt-toolkit==1.0.15 ptyprocess==0.5.2 +pyasn1==0.4.5 +pyasn1-modules==0.2.5 +pycparser==2.19 Pygments==2.2.0 +PyHamcrest==1.9.0 +PyNaCl==1.3.0 python-crontab==2.3.6 python-dateutil==2.8.0 pytz==2018.4 PyYAML==5.1 -redis==3.2.0 +redis==2.10.6 requests==2.21.0 +requests-oauthlib==1.2.0 +rsa==4.0 Rx==1.6.1 simplegeneric==0.8.1 simplejson==3.16.0 @@ -60,9 +93,13 @@ singledispatch==3.4.0.3 six==1.11.0 sqlparse==0.3.0 traitlets==4.3.2 +Twisted==19.2.1 +txaio==18.8.1 uritemplate==3.0.0 urllib3==1.24.1 vine==1.3.0 watchdog==0.8.3 watchdog-gevent==0.1.0 wcwidth==0.1.7 +websocket-client==0.56.0 +zope.interface==4.6.0 diff --git a/seal/asgi.py b/seal/asgi.py new file mode 100644 index 0000000..0981386 --- /dev/null +++ b/seal/asgi.py @@ -0,0 +1,7 @@ +import os +import django +from channels.routing import get_default_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "seal.settings") +django.setup() +application = get_default_application() \ No newline at end of file diff --git a/seal/routing.py b/seal/routing.py new file mode 100644 index 0000000..5005d0c --- /dev/null +++ b/seal/routing.py @@ -0,0 +1,13 @@ +from channels.auth import AuthMiddlewareStack +from channels.routing import URLRouter, ProtocolTypeRouter +from django.urls import path + +from k8s.consumers import EchoConsumer + +application = ProtocolTypeRouter({ + "websocket": AuthMiddlewareStack( + URLRouter([ + path(r"ws//", EchoConsumer), + ]) + ) +}) \ No newline at end of file diff --git a/seal/settings.py b/seal/settings.py index 0f865f5..d31c358 100644 --- a/seal/settings.py +++ b/seal/settings.py @@ -47,6 +47,7 @@ INSTALLED_APPS = [ 'corsheaders', 'django_filters', 'graphene_django', + 'channels', ] GRAPHENE = { @@ -230,7 +231,7 @@ REST_FRAMEWORK = { ), 'DEFAULT_RENDERER_CLASSES': ( 'rest_framework.renderers.JSONRenderer', - 'rest_framework.renderers.BrowsableAPIRenderer' #注释掉 可以关闭 api web界面 + 'rest_framework.renderers.BrowsableAPIRenderer' # 注释掉 可以关闭 api web界面 ), 'DEFAULT_PERMISSION_CLASSES': ( # 'rest_framework.permissions.AllowAny', @@ -248,7 +249,27 @@ CORS_ORIGIN_WHITELIST = ( ) MIDDLEWARE_CLASSES = ('system.views.DisableCSRFCheck',) - ## K8S Token = "eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJkYXNoYm9hcmQtYWRtaW4tdG9rZW4tZGhobWMiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZGFzaGJvYXJkLWFkbWluIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiOThkMDcwZWItODc1Yy0xMWU5LWE1MzgtMDAwYzI5N2I0ZmU3Iiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50Omt1YmUtc3lzdGVtOmRhc2hib2FyZC1hZG1pbiJ9.XDFpez2E84R_zlopt_uEHPvVGUtSavypyix6UcYJO3J4imHdJy7MEkfV-wltBA1H8x0TT2AW64rLlXaRJ8OkFWJ0myedfKdjnf7i0oLQ8j-7lw6rT3A0e2pKmpnOaBQfgzRm83-t2I5MMp3Iu9VNUiAbqQpjql4AKwRuJEEGCs99tKStUxzIsJKusmUHh9KAK4BAxySn9h16T2URZ7czLP4mty2crYWNV4KwSwFPthGhFPsl8mnet_hiV5k4me5a8frmXytOy64MmGW8w3TBgiM-7hBYSxt84QGGnyi84LU0EFgtLwBWEOTZeUKKQ6IkoAprMmNcSxX8WUJFlx_uJg" -APISERVER = 'https://192.168.100.111:6443' \ No newline at end of file +APISERVER = 'https://192.168.100.111:6443' + +## k8s webssh 有权限执行 kubectl exec -it 的主机 + +webssh_ip = "192.168.100.111" +webssh_port = "22" +webssh_username = "root" +webssh_password = "1qaz.2wsx" +webssh_name = "root@k8s-master" # 终端显示的名字 是为了判断终端退出用 + +# django-channels配置 +CHANNEL_LAYERS = { + "default": { + "BACKEND": "channels_redis.core.RedisChannelLayer", + "CONFIG": { + "hosts": [("127.0.0.1", 6379)], + }, + }, +} + +# 配置ASGI +ASGI_APPLICATION = "seal.routing.application" diff --git a/templates/k8s/k8s-pod-list.html b/templates/k8s/k8s-pod-list.html index b8d004e..934add7 100644 --- a/templates/k8s/k8s-pod-list.html +++ b/templates/k8s/k8s-pod-list.html @@ -53,7 +53,7 @@
- +
@@ -88,7 +88,8 @@
{{ v.namespace}}
- + webssh {# {% if perms.assets.view_ecs %}#} diff --git a/templates/k8s/k8s-pod-webssh.html b/templates/k8s/k8s-pod-webssh.html new file mode 100644 index 0000000..6d33141 --- /dev/null +++ b/templates/k8s/k8s-pod-webssh.html @@ -0,0 +1,53 @@ + + + + + django webssh 例子 + + + + + + + + +
+ + + + + + + + + \ No newline at end of file