diff --git a/Dockerfile b/Dockerfile
index 9f4c5c22b..303835360 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -22,4 +22,5 @@ ENV LANG=zh_CN.UTF-8
ENV LC_ALL=zh_CN.UTF-8
EXPOSE 8080
+EXPOSE 8081
ENTRYPOINT ["./entrypoint.sh"]
diff --git a/apps/assets/templates/assets/_asset_user_list.html b/apps/assets/templates/assets/_asset_user_list.html
index d39c57fb3..a85a3056d 100644
--- a/apps/assets/templates/assets/_asset_user_list.html
+++ b/apps/assets/templates/assets/_asset_user_list.html
@@ -1,9 +1,14 @@
{% load i18n %}
@@ -137,8 +142,7 @@ $(document).ready(function(){
}
var success = function (data) {
var task_id = data.task;
- var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
- window.open(url, '', 'width=800,height=600,left=400,top=400')
+ showCeleryTaskLog(task_id);
};
requestApi({
url: the_url,
@@ -149,4 +153,4 @@ $(document).ready(function(){
})
-
\ No newline at end of file
+
diff --git a/apps/assets/templates/assets/admin_user_assets.html b/apps/assets/templates/assets/admin_user_assets.html
index 9ca433930..a2a52e0e2 100644
--- a/apps/assets/templates/assets/admin_user_assets.html
+++ b/apps/assets/templates/assets/admin_user_assets.html
@@ -85,8 +85,7 @@ $(document).ready(function () {
var the_url = "{% url 'api-assets:admin-user-connective' pk=admin_user.id %}";
var success = function (data) {
var task_id = data.task;
- var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
- window.open(url, '', 'width=800,height=600,left=400,top=400')
+ showCeleryTaskLog(task_id);
};
requestApi({
url: the_url,
diff --git a/apps/assets/templates/assets/asset_asset_user_list.html b/apps/assets/templates/assets/asset_asset_user_list.html
index bf3cba583..c9c8e9aaa 100644
--- a/apps/assets/templates/assets/asset_asset_user_list.html
+++ b/apps/assets/templates/assets/asset_asset_user_list.html
@@ -81,8 +81,7 @@ $(document).ready(function () {
var the_url = "{% url 'api-assets:asset-user-connective' %}" + "?asset_id={{ asset.id }}";
var success = function (data) {
var task_id = data.task;
- var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
- window.open(url, '', 'width=800,height=600,left=400,top=400')
+ showCeleryTaskLog(task_id);
};
requestApi({
url: the_url,
@@ -92,4 +91,4 @@ $(document).ready(function () {
});
})
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/apps/assets/templates/assets/asset_detail.html b/apps/assets/templates/assets/asset_detail.html
index dea949bce..c086e83f1 100644
--- a/apps/assets/templates/assets/asset_detail.html
+++ b/apps/assets/templates/assets/asset_detail.html
@@ -276,8 +276,7 @@ function refreshAssetHardware() {
var success = function(data) {
console.log(data);
var task_id = data.task;
- var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
- window.open(url, '', 'width=800,height=600')
+ showCeleryTaskLog(task_id);
};
requestApi({
url: the_url,
@@ -355,8 +354,7 @@ $(document).ready(function () {
var success = function(data) {
var task_id = data.task;
- var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
- window.open(url, '', 'width=800,height=600')
+ showCeleryTaskLog(task_id);
};
requestApi({
diff --git a/apps/assets/templates/assets/asset_list.html b/apps/assets/templates/assets/asset_list.html
index 3c8f93822..b9892790d 100644
--- a/apps/assets/templates/assets/asset_list.html
+++ b/apps/assets/templates/assets/asset_list.html
@@ -523,8 +523,7 @@ $(document).ready(function(){
function success(data) {
rMenu.css({"visibility" : "hidden"});
var task_id = data.task;
- var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
- window.open(url, '', 'width=800,height=600')
+ showCeleryTaskLog(task_id);
}
requestApi({
url: the_url,
@@ -538,8 +537,7 @@ $(document).ready(function(){
function success(data) {
rMenu.css({"visibility" : "hidden"});
var task_id = data.task;
- var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
- window.open(url, '', 'width=800,height=600')
+ showCeleryTaskLog(task_id);
}
requestApi({
url: the_url,
@@ -552,4 +550,4 @@ $(document).ready(function(){
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/apps/assets/templates/assets/system_user_assets.html b/apps/assets/templates/assets/system_user_assets.html
index 4cad3fd4a..d4346003a 100644
--- a/apps/assets/templates/assets/system_user_assets.html
+++ b/apps/assets/templates/assets/system_user_assets.html
@@ -202,8 +202,7 @@ $(document).ready(function () {
};
var success = function (data) {
var task_id = data.task;
- var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
- window.open(url, '', 'width=800,height=600,left=400,top=400')
+ showCeleryTaskLog(task_id);
};
requestApi({
url: the_url,
@@ -219,8 +218,7 @@ $(document).ready(function () {
the_url = the_url.replace("{{ DEFAULT_PK }}", asset_id);
var success = function (data) {
var task_id = data.task;
- var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
- window.open(url, '', 'width=800,height=600,left=400,top=400')
+ showCeleryTaskLog(task_id);
};
var error = function (data) {
alert(data)
@@ -239,8 +237,7 @@ $(document).ready(function () {
};
var success = function (data) {
var task_id = data.task;
- var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
- window.open(url, '', 'width=800,height=600,left=400,top=400')
+ showCeleryTaskLog(task_id);
};
requestApi({
url: the_url,
diff --git a/apps/assets/templates/assets/system_user_detail.html b/apps/assets/templates/assets/system_user_detail.html
index d9ff1a641..9c0557fde 100644
--- a/apps/assets/templates/assets/system_user_detail.html
+++ b/apps/assets/templates/assets/system_user_detail.html
@@ -251,8 +251,7 @@ $(document).ready(function () {
var the_url = "{% url 'api-assets:system-user-push' pk=system_user.id %}";
var success = function (data) {
var task_id = data.task;
- var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
- window.open(url, '', 'width=800,height=600,left=400,top=400')
+ showCeleryTaskLog(task_id);
};
requestApi({
url: the_url,
@@ -265,8 +264,7 @@ $(document).ready(function () {
var the_url = "{% url 'api-assets:system-user-connective' pk=system_user.id %}";
var success = function (data) {
var task_id = data.task;
- var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
- window.open(url, '', 'width=800,height=600')
+ showCeleryTaskLog(task_id);
};
requestApi({
url: the_url,
diff --git a/apps/common/api.py b/apps/common/api.py
index d69540cfd..4c5d28254 100644
--- a/apps/common/api.py
+++ b/apps/common/api.py
@@ -83,6 +83,8 @@ class LogTailApi(generics.RetrieveAPIView):
return Response({"data": data, 'end': end, 'mark': new_mark})
+
+
class ResourcesIDCacheApi(APIView):
def post(self, request, *args, **kwargs):
spm = str(uuid.uuid4())
diff --git a/apps/jumpserver/asgi.py b/apps/jumpserver/asgi.py
new file mode 100644
index 000000000..a71974685
--- /dev/null
+++ b/apps/jumpserver/asgi.py
@@ -0,0 +1,7 @@
+import os
+import django
+from channels.routing import get_default_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "jumpserver.settings")
+django.setup()
+application = get_default_application()
diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py
index 687a1637c..5fb1444df 100644
--- a/apps/jumpserver/conf.py
+++ b/apps/jumpserver/conf.py
@@ -335,6 +335,7 @@ defaults = {
'REDIS_DB_CELERY': 3,
'REDIS_DB_CACHE': 4,
'REDIS_DB_SESSION': 5,
+ 'REDIS_DB_WS': 6,
'CAPTCHA_TEST_MODE': None,
'TOKEN_EXPIRATION': 3600 * 24,
'DISPLAY_PER_PAGE': 25,
diff --git a/apps/jumpserver/routing.py b/apps/jumpserver/routing.py
new file mode 100644
index 000000000..d76f1ccee
--- /dev/null
+++ b/apps/jumpserver/routing.py
@@ -0,0 +1,13 @@
+from channels.auth import AuthMiddlewareStack
+from channels.routing import ProtocolTypeRouter, URLRouter
+
+from ops.urls.ws_urls import urlpatterns as ops_urlpatterns
+
+urlpatterns = []
+urlpatterns += ops_urlpatterns
+
+application = ProtocolTypeRouter({
+ 'websocket': AuthMiddlewareStack(
+ URLRouter(urlpatterns)
+ ),
+})
diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py
index d726b4bcb..894e4a2c5 100644
--- a/apps/jumpserver/settings.py
+++ b/apps/jumpserver/settings.py
@@ -74,6 +74,7 @@ INSTALLED_APPS = [
'rest_framework',
'rest_framework_swagger',
'drf_yasg',
+ 'channels',
'django_filters',
'bootstrap3',
'captcha',
@@ -140,7 +141,8 @@ TEMPLATES = [
},
]
-# WSGI_APPLICATION = 'jumpserver.wsgi.applications'
+WSGI_APPLICATION = 'jumpserver.wsgi.application'
+ASGI_APPLICATION = 'jumpserver.routing.application'
LOGIN_REDIRECT_URL = reverse_lazy('index')
LOGIN_URL = reverse_lazy('authentication:login')
@@ -624,3 +626,19 @@ BACKEND_ASSET_USER_AUTH_VAULT = False
PERM_SINGLE_ASSET_TO_UNGROUP_NODE = CONFIG.PERM_SINGLE_ASSET_TO_UNGROUP_NODE
WINDOWS_SSH_DEFAULT_SHELL = CONFIG.WINDOWS_SSH_DEFAULT_SHELL
FLOWER_URL = CONFIG.FLOWER_URL
+
+
+# Django channels support websocket
+CHANNEL_REDIS = "redis://:{}@{}:{}/{}".format(
+ CONFIG.REDIS_PASSWORD, CONFIG.REDIS_HOST, CONFIG.REDIS_PORT,
+ CONFIG.REDIS_DB_WS,
+)
+
+CHANNEL_LAYERS = {
+ 'default': {
+ 'BACKEND': 'channels_redis.core.RedisChannelLayer',
+ 'CONFIG': {
+ "hosts": [CHANNEL_REDIS],
+ },
+ },
+}
diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py
index 80105743c..82773a51a 100644
--- a/apps/jumpserver/urls.py
+++ b/apps/jumpserver/urls.py
@@ -66,6 +66,7 @@ urlpatterns = [
re_path('api/(?P\w+)/(?Pv\d)/.*', views.redirect_format_api),
path('api/health/', views.HealthCheckView.as_view(), name="health"),
path('luna/', views.LunaView.as_view(), name='luna-view'),
+ re_path('ws/.*', views.WsView.as_view(), name='ws-view'),
path('i18n//', views.I18NView.as_view(), name='i18n-switch'),
path('settings/', include('settings.urls.view_urls', namespace='settings')),
diff --git a/apps/jumpserver/views.py b/apps/jumpserver/views.py
index f3365b7ff..f8c8c1f02 100644
--- a/apps/jumpserver/views.py
+++ b/apps/jumpserver/views.py
@@ -226,4 +226,11 @@ class HealthCheckView(APIView):
return JsonResponse({"status": 1, "time": int(time.time())})
+class WsView(APIView):
+ ws_port = settings.CONFIG.HTTP_LISTEN_PORT + 1
+
+ def get(self, request):
+ msg = _("Websocket server run on port: {}, you should proxy it on nginx"
+ .format(self.ws_port))
+ return JsonResponse({"msg": msg})
diff --git a/apps/ops/templates/ops/adhoc_detail.html b/apps/ops/templates/ops/adhoc_detail.html
index 8b340b808..e3a13b548 100644
--- a/apps/ops/templates/ops/adhoc_detail.html
+++ b/apps/ops/templates/ops/adhoc_detail.html
@@ -196,8 +196,7 @@ $(document).ready(function () {
alert("没有运行历史");
return
}
- var url = '{% url 'ops:celery-task-log' pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', history_pk);
- window.open(url, '', 'width=800,height=600,left=400,top=400')
+ showCeleryTaskLog(history_pk);
})
{% endblock %}
diff --git a/apps/ops/templates/ops/adhoc_history_detail.html b/apps/ops/templates/ops/adhoc_history_detail.html
index 5091470d5..ccc4fce4c 100644
--- a/apps/ops/templates/ops/adhoc_history_detail.html
+++ b/apps/ops/templates/ops/adhoc_history_detail.html
@@ -145,8 +145,8 @@
diff --git a/apps/ops/templates/ops/celery_task_log.html b/apps/ops/templates/ops/celery_task_log.html
index 3cdcb008d..d7b48e267 100644
--- a/apps/ops/templates/ops/celery_task_log.html
+++ b/apps/ops/templates/ops/celery_task_log.html
@@ -20,50 +20,13 @@
diff --git a/apps/ops/templates/ops/task_adhoc.html b/apps/ops/templates/ops/task_adhoc.html
index a31397c14..866ef4255 100644
--- a/apps/ops/templates/ops/task_adhoc.html
+++ b/apps/ops/templates/ops/task_adhoc.html
@@ -130,8 +130,7 @@ $(document).ready(function () {
alert("没有运行历史");
return
}
- var url = '{% url 'ops:celery-task-log' pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', history_pk);
- window.open(url, '', 'width=800,height=600,left=400,top=400')
+ showCeleryTaskLog(history_pk);
})
{% endblock %}
diff --git a/apps/ops/templates/ops/task_detail.html b/apps/ops/templates/ops/task_detail.html
index bbeaad660..c29fc2c4e 100644
--- a/apps/ops/templates/ops/task_detail.html
+++ b/apps/ops/templates/ops/task_detail.html
@@ -174,8 +174,7 @@ $(document).ready(function () {
alert("没有运行历史");
return
}
- var url = '{% url 'ops:celery-task-log' pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', history_pk);
- window.open(url, '', 'width=800,height=600,left=400,top=400')
+ showCeleryTaskLog(history_pk);
})
diff --git a/apps/ops/templates/ops/task_history.html b/apps/ops/templates/ops/task_history.html
index d9c5a0dfa..dea4c9324 100644
--- a/apps/ops/templates/ops/task_history.html
+++ b/apps/ops/templates/ops/task_history.html
@@ -155,8 +155,7 @@ $(document).ready(function () {
alert("没有运行历史");
return
}
- var url = '{% url 'ops:celery-task-log' pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', history_pk);
- window.open(url, '', 'width=800,height=600,left=400,top=400')
+ showCeleryTaskLog(history_pk);
})
diff --git a/apps/ops/templates/ops/task_list.html b/apps/ops/templates/ops/task_list.html
index f97b83630..83da62071 100644
--- a/apps/ops/templates/ops/task_list.html
+++ b/apps/ops/templates/ops/task_list.html
@@ -98,8 +98,7 @@ $(document).ready(function () {
};
var success = function(data) {
var task_id = data.task;
- var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
- window.open(url, '', 'width=800,height=600,left=400,top=400')
+ showCeleryTaskLog(task_id);
};
requestApi({
url: the_url,
diff --git a/apps/ops/urls/ws_urls.py b/apps/ops/urls/ws_urls.py
new file mode 100644
index 000000000..d3148982e
--- /dev/null
+++ b/apps/ops/urls/ws_urls.py
@@ -0,0 +1,9 @@
+from django.urls import path
+
+from .. import ws
+
+app_name = 'ops'
+
+urlpatterns = [
+ path('ws/ops/tasks//log/', ws.CeleryLogWebsocket, name='task-log-ws'),
+]
diff --git a/apps/ops/ws.py b/apps/ops/ws.py
new file mode 100644
index 000000000..fbc46c1db
--- /dev/null
+++ b/apps/ops/ws.py
@@ -0,0 +1,41 @@
+import time
+import threading
+
+from .celery.utils import get_celery_task_log_path
+from channels.generic.websocket import JsonWebsocketConsumer
+
+
+class CeleryLogWebsocket(JsonWebsocketConsumer):
+ task = ''
+ task_log_f = None
+ disconnected = False
+
+ def connect(self):
+ task_id = self.scope['url_route']['kwargs']['task_id']
+ log_path = get_celery_task_log_path(task_id)
+ try:
+ self.task_log_f = open(log_path)
+ except OSError:
+ self.send({'message': "Task {} log not found".format(task_id)})
+ self.disconnect(None)
+ return
+
+ self.accept()
+ self.send_log_to_client()
+
+ def disconnect(self, close_code):
+ self.disconnected = True
+ if self.task_log_f and not self.task_log_f.closed:
+ self.task_log_f.close()
+ self.close()
+
+ def send_log_to_client(self):
+ def func():
+ while not self.disconnected:
+ data = self.task_log_f.read(4096)
+ if data:
+ data = data.replace('\n', '\r\n')
+ self.send_json({'message': data})
+ time.sleep(0.2)
+ thread = threading.Thread(target=func)
+ thread.start()
diff --git a/apps/static/css/jumpserver.css b/apps/static/css/jumpserver.css
index 489279189..02e738d24 100644
--- a/apps/static/css/jumpserver.css
+++ b/apps/static/css/jumpserver.css
@@ -88,7 +88,7 @@ table.dataTable tbody td.selected a,
table.dataTable tbody tr.selected td i.text-navy,
table.dataTable tbody th.selected td i.text-navy,
table.dataTable tbody td.selected td i.text-navy {
- color: white !important;
+ color: white;
}
.m-0 {
@@ -473,4 +473,4 @@ span.select2-selection__placeholder {
.p-r-5 {
padding-right: 5px;
-}
\ No newline at end of file
+}
diff --git a/apps/static/js/jumpserver.js b/apps/static/js/jumpserver.js
index 4ec64fbbb..f7f7b655c 100644
--- a/apps/static/js/jumpserver.js
+++ b/apps/static/js/jumpserver.js
@@ -1201,3 +1201,7 @@ function nodesSelect2Init(selector, url) {
})
}
+function showCeleryTaskLog(taskId) {
+ var url = '/ops/celery/task/taskId/log/'.replace('taskId', taskId);
+ window.open(url, '', 'width=900,height=600')
+}
diff --git a/jms b/jms
index d67c2fa35..a6eaddeb5 100755
--- a/jms
+++ b/jms
@@ -47,6 +47,7 @@ LOG_DIR = os.path.join(BASE_DIR, 'logs')
TMP_DIR = os.path.join(BASE_DIR, 'tmp')
HTTP_HOST = CONFIG.HTTP_BIND_HOST or '127.0.0.1'
HTTP_PORT = CONFIG.HTTP_LISTEN_PORT or 8080
+WS_PORT = HTTP_PORT + 1
DEBUG = CONFIG.DEBUG or False
LOG_LEVEL = CONFIG.LOG_LEVEL or 'INFO'
@@ -201,12 +202,15 @@ def is_running(s, unlink=True):
def parse_service(s):
all_services = [
- 'gunicorn', 'celery_ansible', 'celery_default', 'beat', 'flower'
+ 'gunicorn', 'celery_ansible', 'celery_default',
+ 'beat', 'flower', 'daphne',
]
if s == 'all':
return all_services
elif s == "web":
- return ['gunicorn', 'flower']
+ return ['gunicorn', 'flower', 'daphne']
+ elif s == 'ws':
+ return ['daphne']
elif s == "task":
return ["celery_ansible", "celery_default", "beat"]
elif s == 'gunicorn':
@@ -225,10 +229,8 @@ def parse_service(s):
def get_start_gunicorn_kwargs():
print("\n- Start Gunicorn WSGI HTTP Server")
prepare()
- service = 'gunicorn'
bind = '{}:{}'.format(HTTP_HOST, HTTP_PORT)
log_format = '%(h)s %(t)s "%(r)s" %(s)s %(b)s '
- pid_file = get_pid_file_path(service)
cmd = [
'gunicorn', 'jumpserver.wsgi',
@@ -238,7 +240,6 @@ def get_start_gunicorn_kwargs():
'-w', str(WORKERS),
'--max-requests', '4096',
'--access-logformat', log_format,
- '-p', pid_file,
'--access-logfile', '-'
]
@@ -247,6 +248,16 @@ def get_start_gunicorn_kwargs():
return {'cmd': cmd, 'cwd': APPS_DIR}
+def get_start_daphne_kwargs():
+ print("\n- Start Daphne ASGI WS Server")
+ cmd = [
+ 'daphne', 'jumpserver.asgi:application',
+ '-b', HTTP_HOST,
+ '-p', str(WS_PORT),
+ ]
+ return {'cmd': cmd, 'cwd': APPS_DIR}
+
+
def get_start_celery_ansible_kwargs():
print("\n- Start Celery as Distributed Task Queue: Ansible")
return get_start_worker_kwargs('ansible', 4)
@@ -362,6 +373,7 @@ def start_service(s):
"celery_default": get_start_celery_default_kwargs,
"beat": get_start_beat_kwargs,
"flower": get_start_flower_kwargs,
+ "daphne": get_start_daphne_kwargs,
}
kwargs = services_kwargs.get(s)()
diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index ad0dd119b..030e8dd16 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -86,3 +86,6 @@ httpsig==1.3.0
treelib==1.5.3
django-proxy==1.2.1
flower==0.9.3
+channels-redis==2.4.0
+channels==2.3.0
+daphne==2.3.0