v0.3.3 k8s webssh

This commit is contained in:
何全
2019-06-10 17:12:53 +08:00
parent 12741ec789
commit a164b048a4
11 changed files with 302 additions and 18 deletions

View File

@@ -32,7 +32,8 @@
* 一期: 基础模板 (已完成)
* 二期: k8s管理平台 (开发中)
* pod 列表 (已完成)
* pod 列表 (已完成)
* pod webssh (已完成)
## DEMO

100
k8s/consumers.py Normal file
View 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)

View File

@@ -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

View File

@@ -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')
]

View File

@@ -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})

View File

@@ -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
View 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
View 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),
])
)
})

View File

@@ -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"

View File

@@ -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 %}#}

View 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>