mirror of
https://github.com/hequan2017/seal.git
synced 2026-05-05 03:24:34 +08:00
v0.3.3 k8s webssh
This commit is contained in:
@@ -32,7 +32,8 @@
|
||||
|
||||
* 一期: 基础模板 (已完成)
|
||||
* 二期: k8s管理平台 (开发中)
|
||||
* pod 列表 (已完成)
|
||||
* pod 列表 (已完成)
|
||||
* pod webssh (已完成)
|
||||
|
||||
|
||||
## DEMO
|
||||
|
||||
100
k8s/consumers.py
Normal file
100
k8s/consumers.py
Normal file
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
]
|
||||
|
||||
15
k8s/views.py
15
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})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
7
seal/asgi.py
Normal file
7
seal/asgi.py
Normal file
@@ -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()
|
||||
13
seal/routing.py
Normal file
13
seal/routing.py
Normal file
@@ -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/<slug:name>/<slug:namespace>", EchoConsumer),
|
||||
])
|
||||
)
|
||||
})
|
||||
@@ -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'
|
||||
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"
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="table-responsive">
|
||||
|
||||
@@ -88,7 +88,8 @@
|
||||
<div class="">{{ v.namespace}}</div>
|
||||
</td>
|
||||
<td class="center">
|
||||
|
||||
<a class="btn btn-success btn-xs "
|
||||
href="{% url "k8s:k8s-pod-webssh" %}?namespace={{ v.namespace }}&name={{ k }}">webssh</a>
|
||||
</td>
|
||||
|
||||
{# {% if perms.assets.view_ecs %}#}
|
||||
|
||||
53
templates/k8s/k8s-pod-webssh.html
Normal file
53
templates/k8s/k8s-pod-webssh.html
Normal file
@@ -0,0 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>django webssh 例子</title>
|
||||
|
||||
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/xterm/3.13.2/xterm.css" rel="stylesheet" type="text/css"/>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
||||
<div id="terms"></div>
|
||||
</body>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/xterm/3.13.2/xterm.js"></script>
|
||||
|
||||
|
||||
<script>
|
||||
var socket = new WebSocket('ws://' + window.location.host + '/ws/{{ name }}/{{ namespace }}');
|
||||
|
||||
socket.onopen = function () {
|
||||
|
||||
var term = new Terminal();
|
||||
term.open(document.getElementById('terms'));
|
||||
|
||||
|
||||
term.on('data', function (data) {
|
||||
console.log(data);
|
||||
socket.send(data);
|
||||
});
|
||||
|
||||
socket.onmessage = function (msg) {
|
||||
console.log(msg);
|
||||
console.log(msg.data);
|
||||
term.write(msg.data);
|
||||
};
|
||||
socket.onerror = function (e) {
|
||||
console.log(e);
|
||||
};
|
||||
|
||||
socket.onclose = function (e) {
|
||||
console.log(e);
|
||||
term.destroy();
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user